文章有点长并且绕,先来个图片缓冲下!

前言

现在的业务场景越来越复杂,使用的架构也就越来越复杂,分布式、高并发已经是业务要求的常态。像腾讯系的不少服务,还有CDN优化、异地多备份等处理。

实现要点

  1. 互斥性,同一时刻,只能有一个客户端持有锁。

  2. 防止死锁发生,如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。

  3. 加锁和释放锁必须是同一个客户端。

  4. 容错性,只有redis还有节点存活,就可以进行正常的加锁解锁操作。

正确的redis分布式锁实现

错误加锁方式一

保证互斥和防止死锁,首先想到的使用redis的setnx命令保证互斥,为了防止死锁,锁需要设置一个超时时间。

    public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {Long result = jedis.setnx(key, uniqueId);if (1 == result) {//如果该redis实例崩溃,那就无法设置过期时间了jedis.expire(key, expireTime);}}

在多线程并发环境下,任何非原子性的操作,都可能导致问题。这段代码中,如果设置过期时间时,redis实例崩溃,就无法设置过期时间。如果客户端没有正确的释放锁,那么该锁(永远不会过期),就永远不会被释放。

错误加锁方式二

比较容易想到的就是设置值和超时时间为原子原子操作就可以解决问题。那使用setnx命令,将value设置为过期时间不就ok了吗?

    public static boolean wrongLock(Jedis jedis, String key, int expireTime) {long expireTs = System.currentTimeMillis() + expireTime;// 锁不存在,当前线程加锁成果if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {return true;}String value = jedis.get(key);//如果当前锁存在,且锁已过期if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {//锁过期,设置新的过期时间String oldValue = jedis.getSet(key, String.valueOf(expireTs));if (oldValue != null && oldValue.equals(value)) {// 多线程并发下,只有一个线程会设置成功// 设置成功的这个线程,key的旧值一定和设置之前的key的值一致return true;}}// 其他情况,加锁失败return true;}

乍看之下,没有什么问题。但仔细分析,有如下问题:

  1. value设置为过期时间,就要求各个客户端严格的时钟同步,这就需要使用到同步时钟。即使有同步时钟,分布式的服务器一般来说时间肯定是存在少许误差的。

  2. 锁过期时,使用 jedis.getSet虽然可以保证只有一个线程设置成功,但是不能保证加锁和解锁为同一个客户端,因为没有标志锁是哪个客户端设置的嘛。

错误解锁方式一

直接删除key

    public static void wrongReleaseLock(Jedis jedis, String key) {//不是自己加锁的key,也会被释放jedis.del(key);}

简单粗暴,直接解锁,但是不是自己加锁的,也会被删除,这好像有点太随意了吧!

错误解锁方式二

判断自己是不是锁的持有者,如果是,则只有持有者才可以释放锁。

    public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {if (uniqueId.equals(jedis.get(key))) {// 如果这时锁过期自动释放,又被其他线程加锁,该线程就会释放不属于自己的锁jedis.del(key);}}

看起来很完美啊,但是如果你判断的时候锁是自己持有的,这时锁超时自动释放了。然后又被其他客户端重新上锁,然后当前线程执行到jedis.del(key),这样这个线程不就删除了其他线程上的锁嘛,好像有点乱套了哦!

正确的加解锁方式

基本上避免了以上几种错误方式之外,就是正确的方式了。要满足以下几个条件:

  1. 命令必须保证互斥

  2. 设置的key必须要有过期时间,防止崩溃时锁无法释放

  3. value使用唯一id标志每个客户端,保证只有锁的持有者才可以释放锁

加锁直接使用set命令同时设置唯一id和过期时间;其中解锁稍微复杂些,加锁之后可以返回唯一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是否是自己,然后删除,这个需要redis的lua脚本保证两个命令的原子性执行。

@Slf4j
public class RedisDistributedLock {private static final String LOCK_SUCCESS = "OK";private static final Long RELEASE_SUCCESS = 1L;private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";// 锁的超时时间private static int EXPIRE_TIME = 5 * 1000;// 锁等待时间private static int WAIT_TIME = 1 * 1000;private Jedis jedis;private String key;public RedisDistributedLock(Jedis jedis, String key) {this.jedis = jedis;this.key = key;}// 不断尝试加锁public String lock() {try {// 超过等待时间,加锁失败long waitEnd = System.currentTimeMillis() + WAIT_TIME;String value = UUID.randomUUID().toString();while (System.currentTimeMillis() < waitEnd) {String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);if (LOCK_SUCCESS.equals(result)) {return value;}try {Thread.sleep(10);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}} catch (Exception ex) {log.error("lock error", ex);}return null;}public boolean release(String value) {if (value == null) {return false;}// 判断key存在并且删除key必须是一个原子操作// 且谁拥有锁,谁释放String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";Object result = new Object();try {result = jedis.eval(script, Collections.singletonList(key),Collections.singletonList(value));if (RELEASE_SUCCESS.equals(result)) {log.info("release lock success, value:{}", value);return true;}} catch (Exception e) {log.error("release lock error", e);} finally {if (jedis != null) {jedis.close();}}log.info("release lock failed, value:{}, result:{}", value, result);return false;}
}

单是一个redis的分布式锁就有这么多道道,不知道你是否看明白了?留言讨论下吧!

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

​Redis分布式锁,你真的用对了吗?相关推荐

  1. 面试难题:Redis 分布式锁,真的完美无缺吗?

    点击上方"码猿技术专栏"关注,选择"设为星标" 正文-开门见山 谈起redis锁,下面三个,算是出现最多的高频词汇: setnx redLock redisso ...

  2. 大家所推崇的Redis分布式锁真的就万无一失吗?

    在单实例JVM中,常见的处理并发问题的方法有很多,比如synchronized关键字进行访问控制.volatile关键字.ReentrantLock等常用方法.但是在分布式环境中,上述方法却不能在跨J ...

  3. Redis 分布式锁使用不当,酿成一个重大事故,超卖了100瓶飞天茅台!!!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 基于Redis使用分布式锁在当今已经不是什么新鲜事了. 本 ...

  4. Redis 分布式锁没这么简单,网上大多数都有 bug

    Redis 分布式锁这个话题似乎烂大街了,不管你是面试还是工作,随处可见,「码哥」为啥还写? 因为看过很多文章没有将分布式锁的各种问题讲明白,所以准备写一篇,也当做自己的学习总结. 在进入正文之前,我 ...

  5. 深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

    ‍‍‍‍‍‍‍‍‍‍‍‍阅读本文大约需要 20 分钟. 大家好,我是 Kaito. 这篇文章我想和你聊一聊,关于 Redis 分布式锁的「安全性」问题. Redis 分布式锁的话题,很多文章已经写烂了 ...

  6. 这才叫细:带你深入理解Redis分布式锁

    什么是分布式锁 说到Redis,我们第一想到的功能就是可以缓存数据,除此之外,Redis因为单进程.性能高的特点,它还经常被用于做分布式锁. 锁我们都知道,在程序中的作用就是同步工具,保证共享资源在同 ...

  7. **Java有哪些悲观锁的实现_80% 人不知道的 Redis 分布式锁的正确实现方式(Java 版)...

    点击上方"小哈学Java",选择"星标" 回复"资源",领取全网最火的Java核心知识总结 来源:http://sina.lt/gfZU 前 ...

  8. Redis分布式锁【正确实现方式】

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  9. Redis分布式锁的正确实现方式(Java版)

    转自:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/ 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于 ...

最新文章

  1. ORA-01081: cannot start already-running ORACLE - shut it down first
  2. 程序可以下载,在线调试disassembly窗口就出现了大片0x00000000 FFFFFFFF DCD 0xFFFFFFFF ; ? Undefined
  3. 图的数组(邻接矩阵)存储结构
  4. C++旋转二维MxN矩阵的算法(附完整源码)
  5. JAVA程序设计----多线程(下)
  6. java 获取注释_Java面试题Java语言有哪些注释的方式?
  7. 进程控制3--signal
  8. 6.6(java学习笔记)文件分割(IO综合例子)
  9. 掌握Android中的进程和线程
  10. 【LeetCode】【HOT】394. 字符串解码(栈)
  11. 很强势!因拒绝退回用户保证金,知名在线旅游平台成“老赖”,回应...
  12. 8个日志级别(OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL)
  13. AspNetForums 开发过程中的源代码管理
  14. Java 操作MySql Blob 字段
  15. GraphQL API 的查询语言
  16. [转]CG编程概念 ,及CG编译器与VC6.0集成方法
  17. 数组作为方法的参数实例和细节(Java)
  18. k2p华硕系统怎么设置_斐讯K2刷华硕固件教程
  19. 一款可以精准爬取网站的网络数据采集系统
  20. stm32f405rgt6芯片手册

热门文章

  1. docker 本地部署 mysql_Docker 部署Mysql 服务和Redis 服务的方法
  2. ncl如何添加线shp文件_NCL画图个例讲解
  3. c语言自动取款机贴吧,求助 简单atm机的循环操作
  4. plsql大字段保存类型_大揭秘,学习python,为什么数据类型有这么重要
  5. 一个优秀的软件测试工程师需具备的技能
  6. (数据库系统概论|王珊)第七章数据库设计-第二节:需求分析
  7. snmpwalk命令常用方法
  8. CC攻击(N个免费代理形成的DDOS)
  9. 八、栈的操作、栈空间
  10. C/C++:Windows编程—Windows RPC 传递自定义数据类型、自定义数据类型数组、指针数组