分布式锁入门

84 阅读4分钟

分布式锁

解决问题:

保证一段代码在同一时间只在一个线程执行, 如果是单机应用的情况, 那我们用Java提供的锁就可以实现.

但如果是分布式应用, 就需要用分布式锁来解决这个问题了.

锁的核心api:

lock(key, expire)

expire过期时间非常有必要, 一旦加锁后由于系统故障, 导致未执行释放锁的命令, 会导致其他线程无法获得锁, 造成死锁

unlock(key)

实现方案:

基于Redis做分布式锁

lock:

利用redis单线程以及setNx的特性, 同一时间, 只有一个setNx指令会设值成功, 其他指令都会返回false,起到锁的作用

还可以利用expire设置锁的过期时间

unlock:

方法执行完后, del掉对应的key即可

问题

setNx 和 expire分开写无法保证原子性

问题描述:

将setNx 和 expire 分开写, 这两个指令不是原子性的, 可能setNx成功了, 但这时候服务器进程断掉了, 会导致expire无法执行,那就会有死锁风险

问题解决:

redis提供了setNxEx来保证这两个操作的原子性

超时问题

问题描述

线程A获取到锁,然后开始执行逻辑, 如果逻辑执行时间比较长, 在线程A加的锁过期后, 线程B获取到这个锁, 这时候线程A的逻辑才执行完, 这个时候线程A去主动解锁的话, 会将线程B加的锁给释放掉.

问题解决

方案1: 避免在逻辑执行期间锁过期(压根不过期, 让其他线程拿不到锁)

Redisson的WatchDog机制

如果发现逻辑还没完全执行, 锁可能要过期, 就主动去将锁的过期时间延长

方案2: 避免释放掉其他线程的锁

在设置value的时候, 每个线程设置一个唯一值(例如 UUID + 线程Id)

采用这种方案时, 有额外的逻辑判断, 要判断拿到的value, 是不是自己线程设置的那个value, 是的话再去释放锁,但是匹配value和删除key不是一个原子操作, redis也没有提供delifequals这样的原子指令, 这个时候就需要lua脚本来处理了,因为lua脚本可能保证连续多个指令的原子性

if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end

不可重入

问题描述

可重入锁, 也叫递归锁, 是指在一个线程内可以多次获取同一把锁, 假设在一个方法中加了锁, 但是这个方法需要递归, 如果用普通锁, 就会死锁, 这个情况下就需要可重入锁

我们现在是不可重入的

问题解决

redisson 的可重入锁

简单实现:参考ReentrantLock, 没有考虑这个内存锁计数的过期时间

  1. 利用threadlocal, threadlocal存一个Map, key是本线程加的锁的key, 叫lockKey, value是锁加了的次数
  1. lock时, 从 threadlocal存的map根据锁的key获取到对应的value, 如果能获取到, 说明获取到了锁, 那么value更新为value+1, 返回加锁成功, 如果获取不到对应的value,代表没获取到锁, 那么去执行setNxEx, 如果成功, 则将value设为1, 否则直接返回加锁失败
  1. unlock时, 如果能根据锁的key获取到对应的value, 那么value更新为value-1, 如果value为0了, 那么去释放锁,

集群问题

问题描述

如果使用了redis集群, 如果在主节点申请了一把锁, 但是这把锁还没来得及同步到从节点, 主节点挂掉了, 这个时候从节点转换为主节点, 这个时候新的主节点没有这个锁,就会导致一把锁被两个客户端同时持有,

问题解决

redisson的红锁

要求集群节点数量是奇数, 加锁时, 向过半节点发送set指令, 只有过半节点都返回true, 才认为加锁成功, 释放锁时, 需要向所有节点发送del指令.

只能非阻塞的

问题描述

现在这把锁是非阻塞的, 如果获取不到锁, 会直接返回结果, 当然在大部分情况下非阻塞的这种处理才是符合预期的

问题解决

自旋锁思路, 加个while循环, 发现已经有锁, 就休息一会儿, 然后不停循环直到拿到锁