Skip to content
文档章节

Redis分布式锁

分布式锁

加锁

Redis 对资源加锁,使用 setnx 指令,表示 set if not exists,比如 setnx lock:key 200 ,就对 lock:key 加锁了。

过期时间

setnx 对资源已经加锁,如果不执行 del 命令,则资源不会释放,会陷入死锁,解决办法是对锁资源加上过期时间

bash
expire lock:key 5 # 表示资源过期时间为5s

上面两个命令,是分两步操作,不是原子命令操作,如果 expire 操作没有执行,又会陷入死锁

Redis方案

官方方案原子操作

bash
set lock:key 200 ex 5 nx

其中nodejs 方案

javascript
const Redis = require('ioredis');
const redis = new Redis({
  port: 6379, // Redis port
  host: '127.0.0.1', // Redis host
  family: 4, // 4 (IPv4) or 6 (IPv6)
  password: 'abcd1234',
  db: 0,
});

redis.set('lock:key', 200, 'EX', 10, 'NX');

超时问题

线程A 获取了锁,设置时间10s,然后执行30s任务。 待A开始执行10s后,锁资源超时,被Redis释放。 此时 线程B来获取锁,系统没有,因此获得锁资源,此时系统中存在两个线程对同一锁资源都获取到了,得到执行任务,锁的独享性不存在了。同时,在B执行任务过程中,A执行完毕,执行 删除锁操作,则B占有的锁,被A 释放了。

解决方案

可以设置 lock:key 的 value 值为独有的值,当要删除的 value 与 redis中的 value 一致时,才允许删除操作

javascript
let tag = Math.floor(Math.random() * 1000);

redis.set(`lock:key`, tag, 'EX', 10, 'NX')

.....
.....

let value = redis.get(`lock:key`)

if(value ==== tag) redis.del(`lock:key`)

注意,上面的操作也不是原子性的,需要使用Lua 脚本来执行操作

javascript
const script = `if redis.call("get", KEYS[1] == ARGV[1] then 
    return redis.call("del", KEYS[1]))
    else 
        return 0
    end`;
redis.eval(script, 1, `lock:key`, tag);

执行上面的代码来释放锁

代码

javascript
'use strict';

const Redis = require('ioredis');
const sleep = require('mz-modules/sleep');
const redis = new Redis({
  port: 6379, // Redis port
  host: '127.0.0.1', // Redis host
  family: 4, // 4 (IPv4) or 6 (IPv6)
  password: 'abcd1234',
  db: 0,
});

class RedisLock {
  constructor(options) {
    this.expireMode = options.expireMode || 'EX'; // 过期时间策略
    this.setMode = options.setMode || 'NX'; // 锁策略
    this.expire = options.expire || 5; // 默认过期时间
    this.maxtime = options.maxtime || 10; // 加锁重试时间最大值
  }

  /**
   *  lock key
   * @param {string} key key to lock
   * @param {string} value value to set
   * @param {number} expire key expire time
   * @param {number} startTime 加锁开始时间
   */
  async lock(key, value, expire, startTime = Date.now()) {
    const result = await redis.set(key, value, this.expireMode, expire, this.setMode);
    if (result === 'OK') {
      console.log(`${key} ${value} 加锁成功`);
      return true;
    }
    // 如果加锁等待超时后,仍然不成功,停止加锁
    if ((Date.now() - startTime) > this.maxtime) {
      console.log(`${key} ${value} 加锁失败,不再加锁`);
      return false;
    }
    // 加锁不成功等待重试
    await sleep(3000);
    return this.lock(key, value, expire);
  }

  /**
   * 解锁操作
   * @param {string} key key
   * @param {string} value value
   */
  async unlock(key, value) {
    const script = `if redis.call("get", KEYS[1] == ARGV[1] then 
    return redis.call("del", KEYS[1]))
    else 
        return 0
    end`;
    try {
      const result = redis.eval(script, 1, key, value);
      if (result === 1) return true;
      return false;
    } catch (error) {
      console.log(`解锁 ${key} ${value}失败`);
      return false;
    }
  }
}


const redislock = new RedisLock();

async function testlock(name) {
  const value = Math.floor(Math.random() * 10000);
  await redislock.lock(name, value);

  await sleep(3000);
  // 解锁
  await redis.unlock(name, value);
}

testlock('name1');
testlock('name2');