分布式锁
解决问题:
保证一段代码在同一时间只在一个线程执行, 如果是单机应用的情况, 那我们用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, 没有考虑这个内存锁计数的过期时间
- 利用threadlocal, threadlocal存一个Map, key是本线程加的锁的key, 叫lockKey, value是锁加了的次数
- lock时, 从 threadlocal存的map根据锁的key获取到对应的value, 如果能获取到, 说明获取到了锁, 那么value更新为value+1, 返回加锁成功, 如果获取不到对应的value,代表没获取到锁, 那么去执行setNxEx, 如果成功, 则将value设为1, 否则直接返回加锁失败
- unlock时, 如果能根据锁的key获取到对应的value, 那么value更新为value-1, 如果value为0了, 那么去释放锁,
集群问题
问题描述
如果使用了redis集群, 如果在主节点申请了一把锁, 但是这把锁还没来得及同步到从节点, 主节点挂掉了, 这个时候从节点转换为主节点, 这个时候新的主节点没有这个锁,就会导致一把锁被两个客户端同时持有,
问题解决
redisson的红锁
要求集群节点数量是奇数, 加锁时, 向过半节点发送set指令, 只有过半节点都返回true, 才认为加锁成功, 释放锁时, 需要向所有节点发送del指令.
只能非阻塞的
问题描述
现在这把锁是非阻塞的, 如果获取不到锁, 会直接返回结果, 当然在大部分情况下非阻塞的这种处理才是符合预期的
问题解决
自旋锁思路, 加个while循环, 发现已经有锁, 就休息一会儿, 然后不停循环直到拿到锁