redis 分布式锁

Redisson 是 redis 官方推荐的Java分布式锁第三方框架。

高效分布式锁

当我们在设计分布式锁的时候,我们应该考虑分布式锁至少要满足的一些条件,同时考虑如何高效的设计分布式锁,这里我认为以下几点是必须要考虑的。

1、互斥

在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。

2、防止死锁

在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。

所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

3、性能

对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。

所以在锁的设计时,需要考虑两点。

1、锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。

2、锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。

4、重入

我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。关于这点之后会做演示。

针对以上Redisson都能很好的满足,下面就来分析下它。

Redisson原理分析

为了更好的理解分布式锁的原理,我这边自己画张图通过这张图来分析。

1、加锁机制

线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。

线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

2、watch dog自动延期机制

这个比较难理解,找了些许资料感觉也并没有解释的很清楚。这里我自己的理解就是:

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

但在实际开发中会有下面一种情况:

      //设置锁1秒过期redissonLock.lock("redisson", 1);/*** 业务逻辑需要咨询2秒*/redissonLock.release("redisson");/*** 线程1 进来获得锁后,线程一切正常并没有宕机,但它的业务逻辑需要执行2秒,这就会有个问题,在 线程1 执行1秒后,这个锁就自动过期了,* 那么这个时候 线程2 进来了。那么就存在 线程1和线程2 同时在这段业务逻辑里执行代码,这当然是不合理的。* 而且如果是这种情况,那么在解锁时系统会抛异常,因为解锁和加锁已经不是同一线程了。*/

所以这个时候看门狗就出现了,它的作用就是 线程1 业务还没有执行完,时间就过了,线程1 还想持有锁的话,就会启动一个watch dog后台线程,不断的延长锁key的生存时间。

注意 正常这个看门狗线程是不启动的,还有就是这个看门狗启动后对整体性能也会有一定影响,所以不建议开启看门狗。

3、为啥要用lua脚本呢?

这个不用多说,主要是如果你的业务逻辑复杂的话,通过封装在lua脚本中发送给redis,而且redis是单线程的,这样就保证这段复杂业务逻辑执行的原子性

4、可重入加锁机制

Redisson可以实现可重入加锁机制的原因,我觉得跟两点有关:

1、Redis存储锁的数据类型是 Hash类型
2、Hash数据类型的key值包含了当前线程信息。

下面是redis存储的数据

这里表面数据类型是Hash类型,Hash类型相当于我们java的 <key,<key1,value>> 类型,这里key是指 ‘redisson’

它的有效期还有9秒,我们再来看里们的key1值为078e44a3-5f95-4e24-b6aa-80684655a15a:45它的组成是:

guid + 当前线程的ID。后面的value是就和可重入加锁有关。

举图说明

上面这图的意思就是可重入锁的机制,它最大的优点就是相同线程不需要在等待锁,而是可以直接进行相应操作。

具体demo 参考 java分布式锁终极解决方案之 redisson

5、Redis分布式锁的缺点

Redis分布式锁会有个缺陷,就是在Redis哨兵模式下:

客户端1 对某个master节点写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机,主备切换,slave节点从变为了 master节点。

这时客户端2 来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。

这时系统在业务语义上一定会出现问题,导致各种脏数据的产生

缺陷在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。

zk 分布式锁

zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

/*** ZooKeeperSession*/
public class ZooKeeperSession {private static CountDownLatch connectedSemaphore = new CountDownLatch(1);private ZooKeeper zookeeper;private CountDownLatch latch;public ZooKeeperSession() {try {this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher());try {connectedSemaphore.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("ZooKeeper session established......");} catch (Exception e) {e.printStackTrace();}}/*** 获取分布式锁* * @param productId*/public Boolean acquireDistributedLock(Long productId) {String path = "/product-lock-" + productId;try {zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);return true;} catch (Exception e) {while (true) {try {// 相当于是给node注册一个监听器,去看看这个监听器是否存在Stat stat = zk.exists(path, true);if (stat != null) {this.latch = new CountDownLatch(1);this.latch.await(waitTime, TimeUnit.MILLISECONDS);this.latch = null;}zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);return true;} catch (Exception ee) {continue;}}}return true;}/*** 释放掉一个分布式锁* * @param productId*/public void releaseDistributedLock(Long productId) {String path = "/product-lock-" + productId;try {zookeeper.delete(path, -1);System.out.println("release the lock for product[id=" + productId + "]......");} catch (Exception e) {e.printStackTrace();}}/*** 建立zk session的watcher* **/private class ZooKeeperWatcher implements Watcher {public void process(WatchedEvent event) {System.out.println("Receive watched event: " + event.getState());if (KeeperState.SyncConnected == event.getState()) {connectedSemaphore.countDown();}if (this.latch != null) {this.latch.countDown();}}}/*** 封装单例的静态内部类**/private static class Singleton {private static ZooKeeperSession instance;static {instance = new ZooKeeperSession();}public static ZooKeeperSession getInstance() {return instance;}}/*** 获取单例* * @return*/public static ZooKeeperSession getInstance() {return Singleton.getInstance();}/*** 初始化单例的便捷方法*/public static void init() {getInstance();}}

也可以采用另一种方式,创建临时顺序节点:

如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。

public class ZooKeeperDistributedLock implements Watcher {private ZooKeeper zk;private String locksRoot = "/locks";private String productId;private String waitNode;private String lockNode;private CountDownLatch latch;private CountDownLatch connectedLatch = new CountDownLatch(1);private int sessionTimeout = 30000;public ZooKeeperDistributedLock(String productId) {this.productId = productId;try {String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";zk = new ZooKeeper(address, sessionTimeout, this);connectedLatch.await();} catch (IOException e) {throw new LockException(e);} catch (KeeperException e) {throw new LockException(e);} catch (InterruptedException e) {throw new LockException(e);}}public void process(WatchedEvent event) {if (event.getState() == KeeperState.SyncConnected) {connectedLatch.countDown();return;}if (this.latch != null) {this.latch.countDown();}}public void acquireDistributedLock() {try {if (this.tryLock()) {return;} else {waitForLock(waitNode, sessionTimeout);}} catch (KeeperException e) {throw new LockException(e);} catch (InterruptedException e) {throw new LockException(e);}}public boolean tryLock() {try {// 传入进去的locksRoot + “/” + productId// 假设productId代表了一个商品id,比如说1// locksRoot = locks// /locks/10000000000,/locks/10000000001,/locks/10000000002lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);// 看看刚创建的节点是不是最小的节点// locks:10000000000,10000000001,10000000002List<String> locks = zk.getChildren(locksRoot, false);Collections.sort(locks);if(lockNode.equals(locksRoot+"/"+ locks.get(0))){//如果是最小的节点,则表示取得锁return true;}//如果不是最小的节点,找到比自己小1的节点int previousLockIndex = -1;for(int i = 0; i < locks.size(); i++) {if(lockNode.equals(locksRoot + “/” + locks.get(i))) {previousLockIndex = i - 1;break;}}this.waitNode = locks.get(previousLockIndex);} catch (KeeperException e) {throw new LockException(e);} catch (InterruptedException e) {throw new LockException(e);}return false;}private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {Stat stat = zk.exists(locksRoot + "/" + waitNode, true);if (stat != null) {this.latch = new CountDownLatch(1);this.latch.await(waitTime, TimeUnit.MILLISECONDS);this.latch = null;}return true;}public void unlock() {try {// 删除/locks/10000000000节点// 删除/locks/10000000001节点System.out.println("unlock " + lockNode);zk.delete(lockNode, -1);lockNode = null;zk.close();} catch (InterruptedException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();}}public class LockException extends RuntimeException {private static final long serialVersionUID = 1L;public LockException(String e) {super(e);}public LockException(Exception e) {super(e);}}
}

redis 分布式锁和 zk 分布式锁的对比

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。

另外一点就是,如果是 redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等……zk 的分布式锁语义清晰实现简单。

所以先不分析太多的东西,就说这两点,我个人实践认为 zk 的分布式锁比 redis 的分布式锁牢靠、而且模型简单易用。zk唯一的缺点就是就是需要额外部署zk server节点,引入了新的依赖。

java分布式锁解决方案 redisson or ZooKeeper相关推荐

  1. Java分布式锁解决方案

    目录 1.多线程并发问题 2.普通锁 3.分布式锁 4.分布式锁解决方案 4.1.Redis分布式锁(常用) 版本一:setnx key value 版本二:set key value nx ex s ...

  2. java分布式锁终极解决方案之 redisson

    目前有很多项目还在使用redis的 setNx 充当分布式锁,然而这个锁是有问题的,redisson是java支持redis的redlock的唯一实现,.目前支持集群模式,云托管模式,单Redis节点 ...

  3. java如何保证redis设置过期时间的原子性_分布式锁用 Redis 还是 Zookeeper

    在讨论这个问题之前,我们先来看一个业务场景: 系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单. 由于系统有 ...

  4. 分布式锁用Redis还是Zookeeper?

    为什么用分布式锁?在讨论这个问题之前,我们先来看一个业务场景. 作者:jianfeng来源:石杉的架构笔记|2019-07-16 09:22 为什么用分布式锁?在讨论这个问题之前,我们先来看一个业务场 ...

  5. redis中有key但是删不掉_分布式锁用 Redis 还是 Zookeeper

    为什么用分布式锁? 在讨论这个问题之前,我们先来看一个业务场景:系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户 ...

  6. 高并发系统设计——分布式锁解决方案

    摘要 分布式应用进行逻辑处理时经常会遇到并发问题.比如一个操作要修改用户的状态,修改状态需要先读出用户的状态, 在内存里进行修改,改完了再存回去.如果这样的操作同时进行了,就会出现并发问题, 因为读取 ...

  7. 面试官:Redis分布式锁解决方案是什么?

    今天博主在这片文章中主要给大家讲下Redis分布式锁的原理以及解决方案 学到三连呦 1.Redis分布式锁原理 1.1.简述 我们知道分布式锁的特性是排他.避免死锁.高可用.分布式锁的实现可以通过数据 ...

  8. DB数据变更缓存分布式更新的zk分布式锁解决方案

    DB数据变更缓存分布式更新的zk分布式锁解决方案 KafkaConsumer kafak消费线程,DB数据变更后,将消息推送到kafka topic,由消费线程进行消费 属性  1.ConsumerC ...

  9. java分布式锁的三种实现方式

    分布式锁的核心思想,就是使用外部的一块共享的区域,来完成锁的实现. 一.使用mysql数据库实现(基本不用) 1.使用数据库悲观锁 可以使用select ... for update 来实现分布式锁. ...

最新文章

  1. Linux命令行与shell脚本编程大全:第2版
  2. Android Parcelable的介绍与使用
  3. 《压缩感知理论及其研究进展》读书笔记
  4. python socket练习
  5. IntelliJ IDEA 15发布
  6. 不使用GACUtil.exe,如何部署和卸载程序集到GAC中
  7. 2G的完整形式是什么?
  8. 路由代码WebApi设置namespace路由参数
  9. win10文件夹加密_在Windows10中轻松创建隐形文件夹,为数据安全加把锁
  10. SQL-Server使用点滴(二-系统表)
  11. 编程修养 阅读笔记二
  12. POJ 1981 Circle and Points 单位圆覆盖
  13. 分析一个文本文件中各个单词出现的频率,把频率最高的10个词打印出来
  14. Multisim 14.0安装包+详细安装步骤
  15. 【MATLAB】图像分割实验
  16. Canvas Api(全)
  17. 计算机科研立项应用类题目,省级课题计算机课题题目推荐
  18. 数据结构期末大题速成
  19. ECharts: 绘制立体柱状图【圆柱体】
  20. Chrome浏览器截屏插件的开发

热门文章

  1. R语言使用caret包构建随机森林模型(random forest)构建回归模型、通过method参数指定算法名称、通过ntree参数指定随机森林中树的个数
  2. R语言ggplot2可视化:使用geom_smooth函数基于lm方法为每个分组的部分数据(subset data)拟合趋势关系曲线、对指定范围的数据拟合曲线
  3. Python使用pandas设置数据列中float数据类型的有效小数位数、抑制科学计数法
  4. R语言使用ggplot2包使用geom_density()函数绘制分组密度图(改变图例位置、移除图例)实战(density plot)
  5. sklearn基于make_scorer函数为Logistic模型构建自定义损失函数并可视化误差图(lambda selection)和系数图(trace plot)+代码实战
  6. mysql buffer size_优化mysql之key_buffer_size设置
  7. [Google API](7)直接使用Web服务
  8. 【转载】Python的运行原理(编译过程及执行原理)
  9. 1/1+2/1+3/2+4/3+...20/19
  10. tensorflow 回归的例子,包括保存模型和重新预测