1.目前主流的分布式锁实现方案有两种:
(1)基于redis:有开源redisson的jar包供使用
(2)基于zookeeper:有开源的curator的jar包供使用
(3)redis和zookeeper作分布式锁的区别:
<1>zookeeper可靠性比redis强太多,只是效率低了点
<2>如果并发量不是特别大,追求可靠性,首选zookeeper
<3>为了效率,则首选redis实现
2.为什么使用分布式锁
(1)使用分布式锁的目的
<1>保证同一时间只有一个客户端可以对共享资源进行操作
(2)根据锁的用途还可以细分为以下两类
<1>允许多个客户端操作共享资源
1.1.这种情况下,对共享资源的操作一定是幂等性操作
1.2.无论操作多少次都不会出现不同结果
1.3.在这里使用锁,就是为了避免重复操作共享资源从而提高效率
<2>只允许一个客户端操作共享资源
2.1.这种情况下,对共享资源的操作一般是非幂等性操作
2.2.如果出现多个客户端操作共享资源,就可能意味着数据不一致,数据丢失
3.单机模式下的分布式锁比较
(1)Redis
<1>使用下面的命令加锁:SET resource_name my_random_value NX PX 30000
1.1.my_random_value是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志
1.2.NX表示只有当resource_name对应的key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁
1.3.PX 30000表示这个锁有一个30秒的自动过期时间
<2>解锁:
2.1.为了防止客户端1获得的锁,被客户端2给释放,采用下面的Lua脚本来释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1]) else return 0 end
2.2.KEYS[1]的值为resource_name,ARGV[1]的值为my_random_value
2.3.原理就是先获取锁对应的value值,保证和客户端穿进去的my_random_value值相等
2.4.这样就能避免自己的锁被其他人释放,采取Lua脚本操作保证了原子性
(2)Zookeeper
<1>zookeeper的分布式锁原理是利用了临时节点(EPHEMERAL)的特性
1.1.当znode被声明为EPHEMERAL的后,如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除
1.2.这样就避免了设置过期时间的问题
1.3.客户端尝试创建一个znode节点,比如/lock
1.3.1.第一个客户端就创建成功了,相当于拿到了锁
1.3.2.而其它的客户端会创建失败(znode已存在),获取锁失败
4.集群模式比较
(1)Redis
<1>为了redis的高可用,一般都会给redis的节点挂一个slave,然后采用哨兵模式进行主备切换
1.1.但由于Redis的主从复制(replication)是异步的,这可能会出现在数据同步过程中,master宕机
1.2.slave来不及同步数据就被选为master,从而数据丢失
<2>导致数据丢失具体流程如下所示:
2.1.客户端1从Master获取了锁
2.2.Master宕机了,存储锁的key还没有来得及同步到Slave上,Slave升级为Master
2.3.客户端2从新的Master获取到了对应同一个资源的锁
<3>为了应对因Master宕机造成的数据丢失问题,提出了RedLock算法,步骤如下:
3.1.假设有5个master节点,获取当前时间(单位毫秒)
3.2.客户端轮流用相同的key和随机值在5个节点上请求锁
3.3.请求锁时,会有一个和总的释放锁的时间相比小很多的超时时间
3.4.可以防止一个客户端在某个宕掉宕master节点上阻塞过长时间
3.5.在获取锁上所花费的时间,只有当客户端在大多数master节点上成功获取到锁
3.6.并且总消耗时间不超过锁释放时间,这个锁就认为是获取成功了
3.7.如果获取锁失败,客户端都会到每个master节点上释放锁
<4>RedLock不足:节点崩溃重启后引发锁失效,针对同一资源会出现多个客户端持有锁
4.1.解决方案:采用延迟重启
4.2.一个节点崩溃后,先不立即重启,而是等待一段时间在重启
4.3.等待的时间大于锁的有效时间
<5>时间跳跃问题:引发的锁失效问题
5.1.解决方案:禁止认为修改系统时间,使用一个不会进行“跳跃”式调整系统时钟的ntpd程序
5.2.这是通过认为补偿措施,降低不一致发生的概率
<6>超时导致锁失效问题
6.1.RedLock算法并没有解决操作共享资源超时时,导致锁失效的问题
(2)Zookeeper
<1>zookeeper在集群部署中,节点数量一般是奇数,且一定大于3
<2>zookeeper的写数据原理:
2.1.在client向follwer发出一个写的请求
2.2.follwer把请求发送给leader
2.3.leader接收到以后发起投票并通知follwer进行投票
2.4.follwer把投票结果发送给leader,只要半数以上返回ACK信息,就认为通过
2.5.leader将结果汇总后如果需要写入,开始写入同时把写入操作通知给leader,然后commit
2.6.follwer把求情结果返回给client,zookeeper采用的是全局串行化操作
<3>zookeeper做分布式锁的优点
3.1.采用zookeeper作为分布式锁,要么获取不到锁,一旦获取,节点数据一定是一致的
3.2.不会出现redis那种异步同步导致数据丢失的问题
3.3.不存在时间跳跃问题:因为不依赖全局时间
3.4.不存在超时导致锁失效问题:不依赖有效时间
<4>缺点:效率没有redis作为分布式锁的效率高
5.锁的其他特性比较
(1)redis的读写性能比zookeeper强大,高并发场景中,使用zookeeper会出现获取锁失败的情况
(2)zookeeper可以实现读写锁,redis不可以
(3)zookeeper的watch机制:
<1>客户端试图创建zonde的时侯发现它已经不存在了,这时候创建失败
<2>进入一种等待状态,当znode节点被删除的时候,zookeeper通过watch机制通知他
<3>这样它就可以继续完才创建操作获取锁
3.1.这可以让分布式锁在客户端用起来就像一个本地的锁一样
3.2.加锁失败就阻塞,直到获取到锁为止
3.3.这套机制,redis无法实现
(4)三种方案的比较
<1>从理解的难易程度角度(从低到高): 数据库 > 缓存 > Zookeeper
<2>从实现的复杂性角度(从低到高): Zookeeper >= 缓存 > 数据库
<3>从性能角度(从高到低): 缓存 > Zookeeper >= 数据库
<4>从可靠性角度(从高到低): Zookeeper > 缓存 > 数据库