分布式锁实现

写在前面

我们知道在同一个JVM中,可以通过Volatile、Synchronized、ReentrantLock 三个关键字来实现线程的安全。那么在分布式系统中这些是无法保证的,所以要通过分布式锁来实现。

基于分布式锁的实现有多种方案,现针对Redis 和Zookeeper 这两种方式聊一聊功能具体实现方式、优缺点以及各自适用的业务场景。

基于Redis Cluster模式下的分布式锁的实现

基于setnx 锁实现

加锁

正常基于Redis 自带的setnx 命令就可以实现简单的加锁功能:

setnx key value

Key 作为锁的唯一标识,当线程setnx返回1时,说明原本key不存在,该线程成功得到了锁;如果返回的结果为0,则说明key已经存在,线程获取锁失败。

解锁

当线程获取锁,执行完任务后,需要释放锁,以便后续线程使用。可以通过del 这个key来实现:

del key

但是由于加锁和解锁是分为两步实现,不是原子操作,所以可能会出现中间状态:

  • 即加锁完成后,没有解锁(或解锁失败);导致资源锁住。

基于此有这种方案:

  1. 通过制定key的过期时间,让锁到期后自动释放:

expire key

显而易见,这边也有一个上述的风险点:就是非原子操作,可能存在中间状态。

所以我们引入第二种方案。

  1. 基于set 原子操作实现:

set key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX seconds:设置键key的过期时间,单位是秒;
  • PX milliseconds:设置键key的过期时间,单位是毫秒;
  • NX:只有键key不存在的时候才会设置key的值;
  • XX:只有键key存在的时候才会设置key的值。

通过 set (key, value, EX a,NX) 取代setnx 来实现原子加锁(自动释放锁)操作。(redis-client中的API setnx 即是基于此实现,所以在使用中直接调用API的setnx)

基于Redis 实现分布式锁的一些问题

  • 在高并发下的分布式锁实现中,key的过期肯定不能设置的太长,否则会影响后续线程持有该锁;
  • 但是如果设置过期时间很短,直到key过期,持有该锁的线程还未执行完任务;接着下一个线程获取到该锁,这时候前一个线程执行完成后触发del释放该锁,而这把锁这个时候其实是另外一个线程持有;
  • 获取锁是非阻塞的,无论成功还是失败就直接返回;
  • 锁公平问题,所有等待线程同时发起获取锁命令操作。

针对上述问题需要另外服务来保证实现:

  1. 守护线程:如果某个线程在expire时间内,还未执行完成,守护线程自动expire一个新过期时间,直到该线程执行完成或释放;
  2. 释放验证:线程加锁前通过线程ID和Key Value匹配,释放前通过两者判断是否一致,一致再释放该锁,避免错误释放其他线程执行时持有相同的锁(原子性可以通过lua脚本来实现);
  3. 阻塞锁: 通过while true之类的机制去阻塞代码实现;
  4. 公平锁:可以通过将所有等待线程放入同一个队列来实现。

可重入性

Java中的Lock对象以及Synchronized关键字语块都可具有可重入性,可以实现同一个线程共用同一把锁;避免死锁发生的可能。

而在Redis上述实现中则没有相应的功能,如果业务上需要,则需在业务代码中实现其逻辑。

安全性

Redis Cluster 在master异常情况下,会发生主从切换,而主从是异步复制,极大可能导致数据丢失,从而导致锁的失效。这块安全性方面Redis Cluster 无法保证。

但是Redis 作者实现了基于多节点的高可用分布式锁的算法 RedLock。有兴趣的可以了解一下。

基于Zookeeper 集群模式下的分布锁的实现

Zookeeper 通过临时有序(顺序)节点实现分布式锁。

所谓临时顺序节点就是Zookeeper根据创建的时间给该节点名称进行编号,当创建节点的客户端与Zookeeper 断开连接后,临时节点就被删除。

Zookeeper 通过创建临时有序节点实现上锁,只有序号最小(或顺序最靠前)的可以成功获取到锁;

如果该序号不是最小(或不是顺序最靠前)则向它前一个节点注册Watcher,通过watch来监听前一个节点是否存在,等待watch事件(即监听节点的状态变化),如果监听到watch事件发生,则再次判断该节点是否为序号最小节点,如果是则成功获取锁,否则,继续监听等待。

Zookeeper 实现分布式锁的功能

Zookeeper 实现锁的方式上较为简单。

  • 由于Zookeeper 使用集群模式,可以避免单点故障;
  • 临时有序节点,临时节点可以避免锁的中间状态(即永久持有该锁);有序可以保障锁的公平问题;
  • 通过watch实现阻塞。

Zookeeper 实现分布式锁的一些问题

每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。

所以性能开销这块比Redis 大,即并发性能不如Redis。

Redis 和 Zookeeper 分布锁实现比较

Redis Cluster

优点:实现指令性能较高

缺点:

  • 实现较为复杂;
  • 需要额外多个服务来保障;
  • 安全性较低。

适用场景:

高并发的分布式锁实现

Zookeeper

优点:

  • 现有的框架,实现简单;
  • 等待锁队列,提升抢占锁效率。

缺点:

添加和删除节点性能较低

适用场景:

并发量小,安全性要求较高的业务场景

本文分享自微信公众号 - 春哥叨叨(chungedaodao),作者:春哥大魔王

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java线程及生命周期

    在操作系统中线程是最小的调度单元,进程中可以创建多个线程,线程中有自己的栈,寄存器,本地存储,会和进程内其他线程共享文件描述符,虚拟地址。

    春哥大魔王
  • 线程池监控

    通过扩展线程池进行监控,通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后...

    春哥大魔王
  • 一张图了解RocketMQ的事务投递

    如果回查,那么一定要先查看当前事务的执行情况,再看是否需要重新执行本地事务。本地事务执行成功后,返回Commit进行消息二次确认的时候的服务挂了,在重启服务那么...

    春哥大魔王
  • draggable 属性 原

    昨天是在document上绑定mousedown ,mousemove ,mouseup事件,来实时计算,并设置相应元素的宽度,这是最直接想到的办法,就不再多说...

    申君健
  • Netty4 实战精华EventLoop 和线程模型(更新中!!!)1 线程模型概述2 EventLoop 接口3 任务调度

    简单地说,线程模型指定了操作系统、编程语言、框架或者应用程序的上下文中的线程管理的关键方面。 显而易见地,如何以及何时创建线程将对应用程序代码的执行产生显著的...

    JavaEdge
  • Java单例模式-懒汉式、恶汉式、静态内部类、枚举以及线程安全问题

    通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数额控制并节约系统资源。

    爱学习的孙小白
  • 分布式事务系列--SpringCloud整合byteTCC框架0.4.x版本

    版权声明:欢迎转载,请标明出处,如有问题,欢迎指正!谢谢!https://blog.csdn.net/weixin_39800144/article/detai...

    IT云清
  • LockSupport的源码实现原理以及应用

    如果只是LockSupport在使用起来比Object的wait/notify简单,

    小勇DW3
  • 《More Effective C++》——异常(Exceptions)

    main函数中首先抛出了异常,导致Session对象析构,logDestruction被调用,抛出异常21,而析构函数没有捕获这个异常,而是让它流出了destr...

    jackieluo
  • MySQL存储日志并使用Loganalyzer作为前端展示

    为什么要使用日志 在生产环境中我们可能需要一个较为完整的日志系统来查看运行中主机服务的状态和所作出的操作,我们可以在较大型的网络架构中使用ELK来实现对日志的收...

    小小科

扫码关注云+社区

领取腾讯云代金券