1 缘起
曾经在看分布式锁的时候,还是处于了解阶段,
回头总结时,发现有很多细节没有探究到,
本文以-看图说话的方式分析不同的分布式锁方案,
分布式锁需要保证:
(1)互斥性:任意时刻,只有一个客户端可以持有锁;
(2)锁超时释放:保证资源循环利用,避免死锁(长生锁);
(3)可重入性:一个线程可重复申请锁;
(4)安全性:锁只能被持有锁的客户端释放(删除);
(5)高性能和高可用:加锁和释放锁低时延,高可用,避免单机锁失效。
因此,在设计分布式锁方案时,根据高性能要求,选择Redis作为存储介质,
同时,将设计方案细分为非原子方案和原子方案,
梳理成果分享如下,帮助读者系统地了解分布式锁方案,轻松应对知识分享与考核。
2 非原子性方案
非原子性即获取锁和添加过期时间分成两个步骤进行,
先使用setnx获取锁,然后setex为锁添加过期时间,
完整流程图如下图所示。
由图可知,当setnx成功执行后,服务突然出现异常,没有正常进入setex这个步骤,
这就导致:公共资源添加的锁没有配置过期时间,如果配置了持久化,这个资源锁会常驻,
当再次获取该资源时,由于该资源锁已经存在,所有线程均无法获取该资源锁,导致,资源不可用。
3 原子性方案
原子性方案即把资源锁和配置相关数据封装在一个原子操作中,
可以使用Redis的set扩展命令、setnx+过期时钟以及Lua脚本实现原子性。
不过,这里分成单机版Redis和多机版Redis两种方案。
3.1 单机Redis
单机版Redis是指只有一个Master生效,所有的请求都是在这一个Master中操作,
无论是使用哨兵模式还是集群模式,生效的都是一个Master。
3.1.1 setnx+过期时钟
上面的非原子性操作,会导致“长生锁”,需要将操作合成一个操作,
同样使用setnx可以将配置的值置为过期时钟,即:currentTimeMilliseconds+expire,
当前系统毫秒时间+过期时间(毫秒),
这样,就变相地实现了原子操作,
完整的流程如下图所示,
由图可知,此方案虽然可以保证一个服务的线程安全,但是,分布式服务而言,会出现,服务1将服务2的锁释放,
因为,在Redis存储的数据,没有做物理上的隔离,任何客户端都可以操作这个公共的数据,只是做了逻辑上(代码级别)的隔离。
3.1.2 set扩展命令
为实现添加数据和配置过期时间的原子性,
Redis提供了set的扩展指令,同时实现添加不存在的数据(nx)、配置过期属性(ex[秒]或px[毫秒])、过期值(expire),
完整的流程如下图所示,由图可知,
此方案,保证了操作的原子性,但是,存在多线程安全问题。
多线程安全示意图如下图所示,
由图可知,多线程情况下,线程1会释放线程2的锁,
打了一个时间差,原因还是同一个原因,数据没有物理隔离,只做了逻辑(代码级)的隔离。
3.1.3 set扩展命令+随机唯一值
为了保证线程安全,在set扩展命令基础上,添加唯一随机值,
保证多线程操作安全,完成流程图如下图所示,由图可知,
此方案,保证了多线程逻辑层面安全,服务间的安全仍未保证。
同时,存在,业务未执行结束,锁过期的问题。
3.1.4 Redisson
Redisson解决了业务未执行结束,锁过期的问题。
Reidsson方案添加了一个后台看门狗线程,监控当前锁是否被占有,
如果被占用,则延长锁过期时间,保障当前业务执行过程中,锁不会过期,
完整流程图如下图所示,
虽然此方案,解决了业务时间与过期时间的问题,仍存在单机故障问题,
当主节点故障时,数据还未同步到从节点,从节点升级为主节点后,锁就丢失了。
3.2 多机Redis
多机Redis是指多个Redis主节点,这些主节点是独立运行的集群或哨兵。
RedLock
多机Redis方案即RedLock,操作时,服务依次向各个主节点获取锁,
当成功获取锁的数量大于Redis主节点半数时,获取锁成功,
完整的流程如下图所示。
该方案,可以结合Redisson实现,各个主节点中采用Redisson方案,
总的框架是RedLock思想。
4 小结
分布式锁:任意时刻,有且仅有一个线程可操作公共资源。
分布式锁特征:
(1)互斥性:任意时刻,只有一个客户端可以持有锁;
(2)锁超时释放:保证资源循环利用,避免死锁(长生锁);
(3)可重入性:一个线程可重复申请锁;
(4)安全性:锁只能被持有锁的客户端释放(删除);
(5)高性能和高可用:加锁和释放锁低时延,高可用,避免单机锁失效。
分布式锁方案:单机:Redisson和多机:RedLock等。