分布式锁解决方案

分布式锁解决方案

一、概述

在分布式场景下需要保证一个方法(共享资源)在同一时间内只能被同一个线程访问执行。

常见的解决方案:

  • 基于数据库实现分布式锁-:乐观锁(基于版本号)和悲观锁(基于排它锁)
  • 基于 redis 实现分布式锁:setnx(key,当前时间+过期时间)和Redlock机制
  • 基于 zookeeper 实现分布式锁:临时有序节点来实现的分布式锁,Curator

二、数据库实现分布式锁

2.1、基于数据库表

最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。当我们想要获得锁的时候,就可以在该表中增加一条记录,想要释放锁的时候就删除这条记录。

2.2、基于悲观锁

在对任意记录进行修改前,先尝试为该记录加上排他锁(select…for update),如果成功加锁,那么就可以对记录做修改,事务提交后就会解锁了。如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。

需要注意MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引则行锁变表锁 。

2.3、基于乐观锁

乐观锁在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。一般使用版本号实现乐观锁。

2.4、总结

无法解决数据库单点、可重入和失效时间的问题,而且依赖数据库,受数据库性能影响,以及行锁变表锁问题等。

三、Redis实现分布式锁

3.1、基于Jedis方案

  • SETNX 日常指令是SETNX key value,如果 key 不存在则set成功返回 1,如果这个key已经存在了返回0。

  • SETEX key seconds value 表达的意思是 将值 value 关联到 key ,并将 key的生存时间设为多少秒。如果 key 已经存在,setex命令将覆写旧值。并且 setex是一个原子性(atomic)操作。

  • 加锁:

    • 一般就是用一个标识唯一性的字符串比如UUID 配合 SETNX 实现加锁。
  • 解锁:

    • LUA脚本,LUA可以保证是原子性的,思路就是判断一下Key和入参是否相等,是的话就删除,返回成功1,0就是失败。

3.2、基于RedLock实现分布式锁

比如有两个服务A、B都希望获得锁,有一个包含了5个redis master的Redis Cluster,执行过程大致如下:

  1. 客户端获取当前时间戳,单位: 毫秒
  2. 服务A轮寻每个master节点,尝试创建锁。(这里锁的过期时间比较短,一般就几十毫秒) RedLock算法会尝试在大多数节点上分别创建锁,假如节点总数为n,那么大多数节点指的是n/2+1。
  3. 客户端计算成功建立完锁的时间,如果建锁时间小于超时时间,就可以判定锁创建成功。如果锁创建失败,则依次遍历master节点删除锁。
  4. 只要有其它服务创建过分布式锁,那么当前服务就必须轮寻尝试获取锁。

3.3、基于Redisson实现分布式锁

  1. 线程去获取锁,获取成功:执行lua脚本,保存数据到redis数据库。
  2. 线程去获取锁,获取失败:订阅了解锁消息,然后再尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。
  3. 如果这个时候客户端B来尝试加锁,执行了同样的一段lua脚本。第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在。接着第二个if判断,判断myLock锁key的hash数据结构中,是否包含客户端B的ID,但明显没有,那么客户端B会获取到ttl myLock返回的一个数字,代表myLock这个锁key的剩余生存时间。此时客户端B会进入一个while循环,不断的尝试加锁。
  4. 可重入,每次lock会加一,每次unlock会减一。

加锁流程如下:
在这里插入图片描述
解锁流程如下:
在这里插入图片描述

watch dog自动延时机制?

只要客户端A一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间。

3.4、总结

  1. 借助Redis实现分布式锁时,有一个共同的缺点就是当获取锁被拒绝后,会不断的轮询,重新发送获取锁(创建key)的请求,直到请求成功。这就造成空转,浪费宝贵的CPU资源。
  2. RedLock算法本身有争议,并不能保证健壮性。
  3. Redisson实现分布式锁时,除了将key新增到某个指定的master节点外,还需要由master自动异步的将key和value等数据同步至绑定的slave节点上。那么问题来了,如果master没来得及同步数据,突然发生宕机,那么通过故障转移和主备切换slave节点被迅速升级为master节点,新的客户端加锁成功,旧的客户端的watch dog发现key存在,误以为旧客户端仍然持有这把锁,这就导致同时存在多个客户端持有同一把锁的问题。

四、zookeeper实现分布式锁

具体参考:zookeeper分布式锁

每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。

每个节点只需要监听比自己小的前一个节点,当比自己小的节点删除以后,客户端会收到watcher事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致羊群效应,因为每个客户端只需要监控前一个节点。

五、分布式总结

  • 一般在实际开发中使用基于Redisson的分布式锁就可以了,毕竟性能较高并且极少遇见极端复杂的场景。在高并发场景比较适合。
  • 对zookeeper来说它是一个分布式协调组件,获取不到锁, 只需要监听比自己小的前一个节点,不需要轮询浪费CPU性能。二者之间一定要选择的话,我比较推荐使用zk来实现,因为分布式锁本身就是CP模型的场景,redis是AP模型的场景,而zookeeper是CP模型,所以zookeeper比较适合。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值