前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了。乐观锁和悲观锁最根本的区别在于线程之间是否相互阻塞。

那么,本文主要来讨论基于redis的分布式锁算法问题。

从2.6.12版本开始,redis为SET命令增加了一系列选项(set [key] NX/XX EX/PX [expiration]):

  • EX seconds – 设置键key的过期时间,单位时秒

  • PX milliseconds – 设置键key的过期时间,单位时毫秒

  • NX – 只有键key不存在的时候才会设置key的值

  • XX – 只有键key存在的时候才会设置key的值

原文地址:https://redis.io/commands/set
中文地址:http://redis.cn/commands/set.html

注意: 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。

这里简单提一下,在旧版本的redis中(指2.6.12版本之前),使用redis实现分布式锁一般需要setNX、expire、getSet、del等命令。而且会发现这种实现有很多逻辑判断的原子操作以及本地时间等并没有控制好。

而在旧版本的redis中,redis的超时时间很难控制,用户迫切需要把setNX和expiration结合为一体的命令,把他们作为一个原子操作,这样新版本的多选项set命令诞生了。然而这并没有完全解决复杂的超时控制带来的问题。

接下来,我们的一切讨论都基于新版redis。

在这里,我先提出几个在实现redis分布式锁中需要考虑的关键问题

1、死锁问题;

1.1、为了防止死锁,redis至少需要设置一个超时时间;

1.2、由1.1引申出来,当锁自动释放了,但是程序并没有执行完毕,这时候其他线程又获取到锁执行同样的程序,可能会造成并发问题,这个问题我们需要考虑一下是否归属于分布式锁带来问题的范畴。

2、锁释放问题,这里会有两个问题;

2.1、每个获取redis锁的线程应该释放自己获取到的锁,而不是其他线程的,所以我们需要在每个线程获取锁的时候给锁做上不同的标记以示区分;

2.2、由2.1带来的问题是线程在释放锁的时候需要判断当前锁是否属于自己,如果属于自己才释放,这里涉及到逻辑判断语句,至少是两个操作在进行,那么我们需要考虑这两个操作要在一个原子内执行,否者在两个行为之间可能会有其他线程插入执行,导致程序紊乱。

3、更可靠的锁;

单实例的redis(这里指只有一个master节点)往往是不可靠的,虽然实现起来相对简单一些,但是会面临着宕机等不可用的场景,即使在主从复制的时候也显得并不可靠(因为redis的主从复制往往是异步的)。

关于Martin Kleppmann的Redlock的分析

原文地址:https://redis.io/topics/distlock
中文地址:http://redis.cn/topics/distlock.html

文章分析得出,这种算法只需具备3个特性就可以实现一个最低保障的分布式锁。

  • 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。

  • 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。

  • 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.

我们来分析一下:

第一点安全属性意味着悲观锁(互斥锁)是我们做redis分布式锁的前提,否者将可能造成并发;

第二点表明为了避免死锁,我们需要设置锁超时时间,保证在一定的时间过后,锁可以重新被利用;

第三点是说对于客户端来说,获取锁和手动释放锁可以有更高的可靠性。

更进一步分析,结合上文提到的关键问题,这里可以引申出另外的两个问题:

  • 怎么才能合理判断程序真正处理的有效时间范围?(这里有个时间偏移的问题)

  • redis Master节点宕机后恢复(可能还没有持久化到磁盘)、主从节点切换,(N/2)+1这里的N应该怎么动态计算更合理?

接下来再看,redis之父antirez对Redlock的评价

原文地址:http://antirez.com/news/101

文中主要提到了网络延迟和本地时钟的修改(不管是时间服务器或人为修改)对这种算法可能造成的影响。

最后,来点实践吧

I、传统的单实例redis分布式锁实现(关键步骤)

获取锁(含自动释放锁):

  1. SET resource_name my_random_value NX PX 30000
  2.  手动删除锁(Lua脚本):
  3. if redis.call("get",KEYS[1]) == ARGV[1] then
  4.     return redis.call("del",KEYS[1])
  5. else
  6.     return 0
  7. end

II、分布式环境的redis(多master节点)的分布式锁实现

为了保证在尽可能短的时间内获取到(N/2)+1个节点的锁,可以并行去获取各个节点的锁(当然,并行可能需要消耗更多的资源,因为串行只需要count到足够数量的锁就可以停止获取了);

另外,怎么动态实时统一获取redis master nodes需要更进一步去思考了。

QA,补充一下说明(以下为我与朋友沟通的情况,以说明文中大家可能不够明白的地方):

1、在关键问题2.1中,删除就删除了,会造成什么问题?

线程A超时,准备删除锁;但此时的锁属于线程B;线程B还没执行完,线程A把锁删除了,这时线程C获取到锁,同时执行程序;所以不能乱删。

2、在关键问题2.2中,只要在key生成时,跟线程相关就不用考虑这个问题了吗?

不同的线程执行程序,线程之间肯虽然有差异呀,然后在redis锁的value设置有线程信息,比如线程id或线程名称,是分布式环境的话加个机器id前缀咯(类似于twitter的snowflake算法!),但是在del命令只会涉及到key,不会再次检查value,所以还是需要lua脚本控制if(condition){xxx}的原子性。

3、那要不要考虑锁的重入性?

不需要重入;try…finally 没得重入的场景;对于单个线程来说,执行是串行的,获取锁之后必定会释放,因为finally的代码必定会执行啊(只要进入了try块,finally必定会执行)。

4、为什么两个线程都会去删除锁?(貌似重复的问题。不管怎样,还是耐心解答吧)

每个线程只能管理自己的锁,不能管理别人线程的锁啊。这里可以联想一下ThreadLocal。

5、如果加锁的线程挂了怎么办?只能等待自动超时?

看你怎么写程序的了,一种是问题3的回答;另外,那就自动超时咯。这种情况也适用于网络over了。

6、时间太长,程序异常就会蛋疼,时间太短,就会出现程序还没有处理完就超时了,这岂不是很尴尬?

是呀,所以需要更好的衡量这个超时时间的设置。

实践部分主要代码:

RedisLock工具类:

  1. package com.caiya.cms.web.component;
  2. import com.caiya.cache.CacheException;
  3. import com.caiya.cache.redis.JedisCache;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import java.util.Objects;
  7. import java.util.concurrent.TimeUnit;
  8. /**
  9.  * redis实现分布式锁
  10.  * 可实现特性:
  11.  * 1、使多线程无序排队获取和释放锁;
  12.  * 2、丢弃未成功获得锁的线程处理;
  13.  * 3、只释放线程本身加持的锁;
  14.  * 4、避免死锁
  15.  *
  16.  * @author wangnan
  17.  * @since 1.0
  18.  */
  19. public final class RedisLock {
  20.     private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
  21.     /**
  22.      * 尝试加锁(仅一次)
  23.      *
  24.      * @param lockKey       锁key
  25.      * @param lockValue     锁value
  26.      * @param expireSeconds 锁超时时间(秒)
  27.      * @return 是否加锁成功
  28.      * @throws CacheException
  29.      */
  30.     public static boolean tryLock(String lockKey, String lockValue, long expireSeconds) throws CacheException {
  31.         JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache();
  32.         try {
  33.             String response = jedisCache.set(lockKey, lockValue, "nx", "ex", expireSeconds);
  34.             return Objects.equals(response, "OK");
  35.         } finally {
  36.             jedisCache.close();
  37.         }
  38.     }
  39.     /**
  40.      * 加锁(指定最大尝试次数范围内)
  41.      *
  42.      * @param lockKey       锁key
  43.      * @param lockValue     锁value
  44.      * @param expireSeconds 锁超时时间(秒)
  45.      * @param tryTimes      最大尝试次数
  46.      * @param sleepMillis   每两次尝试之间休眠时间(毫秒)
  47.      * @return 是否加锁成功
  48.      * @throws CacheException
  49.      */
  50.     public static boolean lock(String lockKey, String lockValue, long expireSeconds, int tryTimes, long sleepMillis) throws CacheException {
  51.         boolean result;
  52.         int count = 0;
  53.         do {
  54.             count++;
  55.             result = tryLock(lockKey, lockValue, expireSeconds);
  56.             try {
  57.                 TimeUnit.MILLISECONDS.sleep(sleepMillis);
  58.             } catch (InterruptedException e) {
  59.                 logger.error(e.getMessage(), e);
  60.             }
  61.         } while (!result && count <= tryTimes);
  62.         return result;
  63.     }
  64.     /**
  65.      * 释放锁
  66.      *
  67.      * @param lockKey   锁key
  68.      * @param lockValue 锁value
  69.      */
  70.     public static void unlock(String lockKey, String lockValue) {
  71.         JedisCache jedisCache = JedisCacheFactory.getInstance().getJedisCache();
  72.         try {
  73.             String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
  74.             Object result = jedisCache.eval(luaScript, 1, lockKey, lockValue);
  75. //            Objects.equals(result, 1L);
  76.         } catch (Exception e) {
  77.             logger.error(e.getMessage(), e);
  78.         } finally {
  79.             jedisCache.close();
  80.         }
  81. //        return false;
  82.     }
  83.     private RedisLock() {
  84.     }
  85. }

使用工具类的代码片段1:

  1.         ...
  2.         String lockKey = Constant.DEFAULT_CACHE_NAME + ":addItemApply:" + applyPriceDTO.getItemId() + "_" + applyPriceDTO.getSupplierId();// 跟业务相关的唯一拼接键
  3.         String lockValue = Constant.DEFAULT_CACHE_NAME + ":" + System.getProperty("JvmId") + ":" + Thread.currentThread().getName() + ":" + System.currentTimeMillis();// 生成集群环境中的唯一值
  4.         boolean locked = RedisLock.tryLock(lockKey, lockValue, 100);// 只尝试一次,在本次处理过程中直接拒绝其他线程的请求
  5.         if (!locked) {
  6.             throw new IllegalAccessException("您的操作太频繁了,休息一下再来吧~");
  7.         }
  8.         try {
  9.             // 开始处理核心业务逻辑
  10.             Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId());
  11.             ...
  12.             ...
  13.         } finally {
  14.             RedisLock.unlock(lockKey, lockValue);// 在finally块中释放锁
  15.         }

使用工具类的代码片段2:

  1.         ...
  2.         String lockKey = Constant.DEFAULT_CACHE_NAME + ":addItemApply:" + applyPriceDTO.getItemId() + "_" + applyPriceDTO.getSupplierId();
  3.         String lockValue = Constant.DEFAULT_CACHE_NAME + ":机器编号:" + Thread.currentThread().getName() + ":" + System.currentTimeMillis();
  4.         boolean locked = RedisLock.lock(lockKey, lockValue, 100, 20, 100);// 非公平锁,无序竞争(这里需要合理根据业务处理情况设置最大尝试次数和每次休眠时间)
  5.         if (!locked) {
  6.             throw new IllegalAccessException("系统太忙,本次操作失败");// 一般来说,不会走到这一步;如果真的有这种情况,并且在合理设置锁尝试次数和等待响应时间之后仍然处理不过来,可能需要考虑优化程序响应时间或者用消息队列排队执行了
  7.         }
  8.         try {
  9.             // 开始处理核心业务逻辑
  10.             Item item = itemService.queryItemByItemId(applyPriceDTO.getItemId());
  11.             ...
  12.             ...
  13.         } finally {
  14.             RedisLock.unlock(lockKey, lockValue);
  15.         }
  16.         ...

附加:

基于redis的分布式锁实现客户端Redisson:

https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

基于zookeeper的分布式锁实现

http://curator.apache.org/curator-recipes/shared-reentrant-lock.html

扩展阅读

Redis为何这么快--数据存储角度

如何搭建高可用 redis架构?

利用Redis实现分布式锁

Java 异常处理的误区和经验总结

HashMap是如何工作的

来源:https://my.oschina.net/wnjustdoit/blog/1606215

基于redis的分布式锁的分析与实践的更多相关文章

  1. 基于Redis的分布式锁安全性分析-转

    基于Redis的分布式锁到底安全吗(上)?  2017-02-11 网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词“Redis 分布式锁”随便到哪个搜索引擎上去搜索一下就知道了 ...

  2. 基于Redis的分布式锁真的安全吗?

    说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...

  3. 基于 Redis 的分布式锁

    前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...

  4. 基于redis的分布式锁(转)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  5. 基于redis的分布式锁(不适合用于生产环境)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  6. redis系列:基于redis的分布式锁

    一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

  7. 基于Redis的分布式锁到底安全吗(下)?

    2017-02-24 自从我写完这个话题的上半部分之后,就感觉头脑中出现了许多细小的声音,久久挥之不去.它们就像是在为了一些鸡毛蒜皮的小事而相互争吵个不停.的确,有关分布式的话题就是这样,琐碎异常,而 ...

  8. 基于Redis的分布式锁到底安全吗(上)?

    基于Redis的分布式锁到底安全吗(上)?  2017-02-11 网上有关Redis分布式锁的文章可谓多如牛毛了,不信的话你可以拿关键词“Redis 分布式锁”随便到哪个搜索引擎上去搜索一下就知道了 ...

  9. 不用找了,基于 Redis 的分布式锁实战来了!

    Java技术栈 www.javastack.cn 优秀的Java技术公众号 作者:菜蚜 my.oschina.net/wnjustdoit/blog/1606215 前言:在分布式环境中,我们经常使用 ...

随机推荐

  1. DoDataExchange函数,UpdateData(TRUE)和UpdateData(FALSE)的区别

    MFC控件(暂时为Edit控件)与数据的绑定,变量值可以在界面和后台之间传递. 我们在DoDataExchange(CDataExchange* pDX) 函数里,实现了MFC控件和变量的绑定.  若 ...

  2. mysql安装及基本操作(mysql作业)

    1 官网下载,链接  https://www.mysql.com/downloads/ Download MySQL Community Server 默认为你选好了Mac OS X 平台 选择的是. ...

  3. python's twenty-seventh day for me 模

    time模块: 1,计算执行代码的时间 time.time() 2,让程序停这里一段时间 time.sleep(时间(s)) 时间戳时间: import time print(time.time()) ...

  4. 使用GY89的BMP180模块获取温度和压强(海拔)

    最近要用一下GY89,GY89有三个模块,温度压强.加速度计.陀螺仪.通过不同的片选信号来选择. mbed库上都写好了,挺好的. 以下是自己的代码: #include "mbed.h&quo ...

  5. MSDE2000

    安装MSDE2000的时候,遇到的两个问题 sqlserver 小版本 SQL安装问题.系统说:为了安全,要求使用SA密码,请使用SAPWD开关提供同一密码

  6. IDEA中快速排除maven依赖

    选中该模块 点击show dependenties 切换试图 选中要排除的依赖,右击 选择Execlude,然后选择需要在哪个模块添加排除依赖 完成

  7. jquery-attr与prop

    问题:经常使用jQuery插件的attr方法获取checked属性值,获取的值的大小为未定义,此时可以用prop方法获取其真实值,下面介绍这两种方法的区别: 1.通过prop方法获取checked属性 ...

  8. MySQL的blob类型

    MySQL中的Blob类型 MySQL中存放大对象的时候,使用的是Blob类型.所谓的大对象指的就是图片,比如jpg.png.gif等格式的图片,文档,比如pdf.doc等,以及其他的文件.为了在数据 ...

  9. 如何搭建自己的SPRING INITIALIZR server

    这两天在慕课学Spring boot ,用idea通过spring initializr新建项目 即使用代理连不上.无奈. 参考了 GitHub - spring-io/initializr: A w ...

  10. java基础之JDBC六:DBCP 数据库连接池简介

    我们之前写的代码中的数据库连接每次都是自己创建,用完以后自己close()销毁的,这样是很耗费资源的,所以我们引入DBCP DBCP简介 概述: Data Base Connection Pool, ...