基于 redis 的分布式锁是最常见的分布式锁实现方式, 但是要实现一个严谨的没有并发安全问题 redis 分布式锁也有很多细节需要注意;

不可重入锁

lock

lock 语义需要实现资源的互斥获取, 在 redis 体系下可以使用 set 命令实现; 当某一个请求成功针对目标 lockKey set 了指定的值, 表明抢锁成功, lockKey 对应的 value 是当前请求占据该锁的唯一标识;
set 命令需要确保用 NX 和 EX/PX 实现锁的独占性和可过期, 以 java 为例:

1
2
3
4
public boolean lock(String lockKey, String value, int expireTime) {
final String result = jedis.set(lockKey, value, "NX" "EX", expireTime);
return "OK".equals(result);
}

unlock

unlock 语义需要实现资源的释放, 在 redis 体系下即需要当 value 为期望值时删除 lockKey; 但是相比于 lock 阶段仅使用 set 命令的原子性, unlock 阶段存在两个逻辑: getdel, 我们必须要使用具有原子效果的方式实现以上两条命令;
如果不考虑原子性, 就会存在如下图所示的线程安全问题:

存在线程安全问题的错误实现
存在线程安全问题的错误实现

当一个线程执行完 get 命令时, lockKey 恰好过期删除, 紧接着另外一个线程 lock 抢锁成功, 再次写入 lockKey; 接着第一个线程继续执行 del 命令, 就错误释放了别的线程的锁;

基于 watch / multi

为了解决这个问题, 可以使用 redis 的另外两个命令: watchmulti:

  • watch: 监听指定的 key, 当其对应的 value 在事务 exec 执行之前被修改了, 事务将被打断;
  • multi: 开启事务, 保证被提交的一组命令一定被执行, 但不保证一定执行成功 (运行时报错不回滚), 也没有隔离级别;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean unlock(String lockKey, String expectValue) throws Exception {
jedis.watch(lockKey);
try {
// 这里判断 lockKey 的值是否是 value 和下面的 del 虽然不是原子性
// 但加了 watch 可以保证不会误删别人的锁
final String value = jedis.get(lockKey);
if (StringUtils.equals(value, expectValue)) {
final Transaction transaction = jedis.multi();
transaction.del(lockKey);
// 开启 watch 之后, 如果 key 的值被修改, 则事务失败, exec 方法返回 null
final List<Object> exec = transaction.exec();
if (exec != null && exec.get(0).equals("OK")) {
transaction.close();
return true;
}
}
return false;
} finally {
// 取消监听
jedis.unwatch();
}
}

使用 lua 脚本

redis lua 脚本执行的原子性保证:

  • redis 会将整个 lua 脚本作为一个整体, 执行过程中不会被其他 redis 命令打断;
  • 同时 redis 使用单个 lua 环境来运行所有 lua 脚本, 确保脚本在执行过程中不会被其他 lua 脚本影响;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def unlock(redis_client, lock_key, lock_value):
    # 使用 lua 脚本确保原子性释放锁
    lua_script = """
    if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
    else
    return 0
    end
    """
    redis_client.eval(lua_script, 1, lock_key, lock_value)
    return True

可重入锁

lock

unlock

参考链接