分布式结构系列:分布式锁

常用的分布式锁

1、redis 对应的开源jar包:redisson
2、zookeeper 对应的开源jar包:curator

使用锁的目的

保证共享资源在同一时间只有只有一个客户端对共性资源进行操作,在高并发的环境下保证同一时间只有一个线程操作共享数据,
根据我自身所阅读的文章总结使用锁or分布式锁
1. 提升效率
采取锁定可以避免不必要的执行相同的工作
2. 提升正确性
采取锁可以更好的规范排列线程之间的关系

分布式锁设计原则
  1. 互斥性,同一时间只有一个线程持有锁
  2. 容错性,即使某一个持有锁的线程,异常退出,其他线程任然可以获得锁
  3. 隔离性,线程只能解自己的锁不能解其他线程的锁

单机分布式锁对比

redis

先说加锁,根据redis官网文档的描述,使用下面的命令加锁

SET resource_name my_random_value NX PX 30000
my_random_value是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志

NX表示只有当resource_name对应的key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁

PX 30000表示这个锁有一个30秒的自动过期时间。

至于解锁,为了防止客户端1获得的锁,被客户端2给释放,采用下面的Lua脚本来释放锁

if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])else
return 0end
在执行这段LUA脚本的时候,KEYS[1]的值为resource_name,ARGV[1]的值为my_random_value。原理就是先获取锁对应的value值,保证和客户端穿进去的my_random_value值相等,这样就能避免自己的锁被其他人释放。另外,采取Lua脚本操作保证了原子性

zookpeer

先简单说下原理,根据网上文档描述,zookpeer的分布式锁原理是利用了临时节点(EPHEMERAL)的特性。

当znode被声明为EPHEMERAL的后,如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这样就避免了设置过期时间的问题。

客户端尝试创建一个znode节点,比如/lock。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。

集群分布式锁对比

(1)redis
为了redis的高可用,一般都会给redis的节点挂一个slave,然后采用哨兵模式进行主备切换。但由于Redis的主从复制(replication)是异步的,这可能会出现在数据同步过程中,master宕机,slave来不及同步数据就被选为master,从而数据丢失。具体流程如下所示:

(1)客户端1从Master获取了锁。

(2)Master宕机了,存储锁的key还没有来得及同步到Slave上。

(3)Slave升级为Master。

(4)客户端2从新的Master获取到了对应同一个资源的锁。

为了应对这个情形, redis的作者antirez提出了RedLock算法,步骤如下(该流程出自官方文档),假设我们有N个master节点(官方文档里将N设置成5,其实大等于3就行)

(1)获取当前时间(单位是毫秒)。

(2)轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。

(3)客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。

(4)如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。

(5)如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。

zookpeer

那么写数据流程步骤如下
1.在Client向Follwer发出一个写的请求
2.Follwer把请求发送给Leader
3.Leader接收到以后开始发起投票并通知Follwer进行投票
4.Follwer把投票结果发送给Leader,只要半数以上返回了ACK信息,就认为通过
5.Leader将结果汇总后如果需要写入,则开始写入同时把写入操作通知给Leader,然后commit;
6.Follwer把请求结果返回给Client
还有一点,zookpeer采取的是全局串行化操作
OK,现在开始分析
集群同步
client给Follwer写数据,可是Follwer却宕机了,会出现数据不一致问题么?不可能,这种时候,client建立节点失败,根本获取不到锁。
client给Follwer写数据,Follwer将请求转发给Leader,Leader宕机了,会出现不一致的问题么?不可能,这种时候,zookpeer会选取新的leader,继续上面的提到的写流程。
总之,采用zookpeer作为分布式锁,你要么就获取不到锁,一旦获取到了,必定节点的数据是一致的,不会出现redis那种异步同步导致数据丢失的问题。
时间跳跃问题
不依赖全局时间,怎么会存在这种问题
超时导致锁失效问题
不依赖有效时间,怎么会存在这种问题

两者对比

(1)redis的读写性能比zookpeer强太多,如果在高并发场景中,使用zookpeer作为分布式锁,那么会出现获取锁失败的情况,存在性能瓶颈。
(2)zookpeer可以实现读写锁,redis不行。
(3)ZooKeeper的watch机制,客户端试图创建znode的时候,发现它已经存在了,这时候创建失败,那么进入一种等待状态,当znode节点被删除的时候,ZooKeeper通过watch机制通知它,这样它就可以继续完成创建操作(获取锁)。这可以让分布式锁在客户端用起来就像一个本地的锁一样:加锁失败就阻塞住,直到获取到锁为止。这套机制,redis无法实现

总结

redis性能强大,可靠性有一点问题
zookeeper可靠性极高,存在性能瓶颈

架构小知识,扫码关注
参考文档:架构师小秘圈

猜你喜欢

转载自blog.csdn.net/Charles_s/article/details/81185061
今日推荐