REDIS分布式锁

锁的基本实现

单Redis节点分布式锁

加锁:

使用 SET key value [EX seconds ][PX milliseconds][NX|XX] 命令

  • EX second :设置键的过期时间为 second SET key value EX second

  • PX millisecond :设置键的过期时间为 millisecond 毫秒SET key value PX millisecond

  • NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value

  • XX :只在键已经存在时,才对键进行设置操作。

正确代码:

/**
* 尝试获取分布式锁
* @param lockKey 锁key
* @param lockvalueRandomStr 唯一性字符串(解锁时客户端要验证是否是自己持有的字符串,防止误解锁)
* @param expireTime 超期时间 (PX-毫秒)
*/
public boolean tryLock(String lockKey, String lockvalueRandomStr, int expireTime) {
   String result = jedis.set(lockKey, lockvalueRandomStr, "NX", "PX", expireTime);
   return "OK".equals(result);
}
  • 加锁value要具有唯一性,解锁时客户端要验证是否是自己持有的字符串,防止误解锁

错误代码:

public boolean tryLock(String lockKey, String lockvalueRandomStr, int expireTime) {
	Long result = jedis.setnx(lockKey, lockvalueRandomStr);
    if (result == 1) {// 若在这里程序突然崩溃,则无法设置过期时间,锁将无法消除,即发生死锁
        jedis.expire(lockKey, expireTime);
    }
}

解锁:

正确代码:

public boolean releaseLock(String lockKey, String lockvalueRandomStr) {
	String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, 
                               Collections.singletonList(lockKey),
                               Collections.singletonList(lockvalueRandomStr));
	return "OK".equals(result);
}
  • 编写lua脚本语法,逻辑为:先调用get方法验证value是否为参数value,如果是才执行删除(解锁本质上包含三步操作:‘GET’、判断和’DEL’,用Lua脚本来实现能保证这三步的原子性。 概括为:解锁时需判断是否是锁的持有者发起的操作)
  • 使用redis.eval()函数执行lua脚本,命令集的执行具有原子性

错误代码:

1、不判断,直接del
jedis.del(lockKey);

2、判断持有者与解锁分开进行
public void releaseLock(String lockKey, String lockvalueRandomStr) {
    // 判断加锁与解锁是不是同一个客户端
    if (lockvalueRandomStr.equals(jedis.get(lockKey))) {
        // 刚刚判断完是锁持有者来解锁,但在此时锁正好过期了并被其他客户端持有,此时的del操作会把其他客户端正常持有的锁给释放掉!
        jedis.del(lockKey);
    }
}

小结

  • 加锁、解锁必须使用原子操作命令,不能分步执行
  • 锁的值应该具有唯一性,解锁时通过验证唯一性来判断是否为锁持有者来解锁
  • 锁必须设置过期时间,否则可能导致锁无法解除而发生死锁现象

单Redis节点的分布式锁存在的问题:

1、锁只作用于一个节点,单节点失效导致锁无法使用,即使主从、高可用集群依然存在异常情况:

  1. 在Redis的master节点上拿到了锁;

  2. 但是这个加锁的key还没有同步到slave节点;

  3. master故障,发生故障转移,slave节点升级为master节点;

  4. 锁丢失,其他客户端可获取锁,导致多客户端持有锁。

2、锁失效时间设置影响锁的效果

  1. 锁时间短,客户端A没有执行完,锁过期,客户端B持有了锁
  2. 锁时间长,客户端A崩溃,锁没释放,其他客户端迟迟获取不到锁

分布式锁算法Redlock

基于单Redis节点的分布式锁在failover的时候会产生解决不了的安全性问题,Redlock算法基于N个完全独立的Redis节点(通常情况下N可以设置成5)。

  1. 获取当前时间(毫秒数)。
  2. 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
  5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。

由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。单Redis节点的分布式锁节点失效不可用问题,在Redlock中可以很大程度上避免。

注意一个问题,即锁释放时候要向所有节点发出请求,因为可能存在获取锁时,服务器已经SET了,但返回时网络异常,导致客户端不知道该节点已经成功,如果不去释放这个节点,会导致这个节点过期时间内不可用。

RedLock算法存在的问题:

1、节点挂掉并重启,仍然可能导致多客户端持有锁

假设共5个Redis节点:A, B, C, D, E

  1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。(持久化有延迟)
  3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功。

(此类异常的解决办法是延迟重启,即节点挂了不马上重启,而是等一会(锁过期时间),这样在重启时候,之前其他节点获取到的锁都会过期,重启后对现有锁就不会有影响。)

2、锁过期时间依赖于时钟,时钟可能发生跳跃,导致个别节点锁快速过期,可能导致多客户端持有锁

假设共5个Redis节点:A, B, C, D, E

  1. 客户端1从Redis节点A, B, C成功获取了锁(多数节点)。由于网络问题,与D和E通信失败。
  2. 节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。
  3. 客户端2从Redis节点C, D, E成功获取了同一个资源的锁(多数节点)。
  4. 客户端1和客户端2现在都认为自己持有了锁。

3、客户端长时间阻塞导致锁过期问题仍无法解决

对于这个问题有人提出一种方案,获取锁时同时生成一个递增(或唯一)token,此token存在公共存储上,同时返回给客户端,当客户端每次想操作共享资源时,要用token去公共存储上判断是否是当前这个,如果不是则认为是锁已经过期,正被其他客户端持有,拒绝访问。

  1. 客户端A获取到锁,产生token,等于33
  2. 客户端A阻塞至锁过期
  3. 客户端B获取到锁,产生token,等于34
  4. 客户端A恢复过来,请求共享资源,发现已经不是33了,拒绝,从而避免冲突

但这个只能解决一部分问题,一是看阻塞到了哪里二是不可能所有操作都能检查token有效性,比如发短信操作是一个最小单元,且只能有一个客户端去发,客户端A判断是否能发短信,刚判断完就阻塞,客户端B获取锁,发了短信,客户端A恢复,由于短信服务是最小操作单元,已经不能在判断了,然后就发了,导致发了两次。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值