1、基于set命令的分布式锁
加锁:使用setnx进行加锁,该指令返回1时,加锁成功。
解锁:使用del释放,以便其他线程可以继续获取锁
存在问题:A线程获取锁后还没释放就挂了,死锁。
解决方案:设置超时时间
2、加锁时带上超时时间
set <key> <value> nx ex <expireTime>
存在问题:线程A设置锁的过期时间是30s,但是线程A还没有执行完,锁过期自动释放,线程B拿到了。等A执行完想删除锁时,会删掉线程B加的锁。
解决方案:锁的value设置为自己的的线程ID,删除前做判断。但是查询、判断、删除锁不是原子性的,对非原子性的问题,可以使用Lua脚本确保操作的原子性。
3、锁续期
虽然删除前做判断,但依旧不太合理,线程A使用过程中就应该是锁没有过期的。
解决方案:获得锁的线程开一个守护线程,自动为线程A持有的锁续期(「看门狗」机制,Redisson)。有2种情况释放锁。
- 线程A结束,结束守护线程,显示释放锁或者等锁自动过期。
- 线程A所在的服务器挂了,守护线程跟着没了,锁自动过期。
4、如果单个redis挂了咋办?RedLock
如果是主从模式,主从服务器数据并不是强一致性,主节点挂了之后从节点上位,新的主节点并不存在旧主节点的锁,会导致多个线程都可以操作一把锁,则该锁不安全。
解决方案:每次对半数以上的Redis节点加锁,这样基本保证不会同时挂掉。假设有5个redis节点,则加锁步骤如下
- 拿到当前时间,并且设置一个过期时间TTL。
- 依次对5个redis节点加锁,此时需要设置一个超时时间,避免请求失败等异常。超时时间内还无法获取锁,则放弃,找下一个。
- 获取半数以上(3个以上)的锁,才算成功。
- 获取锁的有效时间 = TTL - 获取锁的时间。
- 如果获取锁失败,则需要一一解锁。(如果是加锁成功但是返回消息丢失,也需要解锁)
- 失败重试:获取锁失败,应该在随机时间后重试,同时要有重试次数的限制。