分布式锁的实现场景

在平时的开发中,对于高并发的开发场景,我们不可避免要加锁进行处理,当然redis中也是不可避免的,下面是我总结出来的几种锁的场景

Redis分布式锁方案一

使用Redis实现分布式锁最简单的方案是在获取锁之前先查询一下以该锁为key对应的value存不存在,如果存在,则说明该锁被其他客户端获取了,否则的话就尝试获取锁,获取锁的方法很简单,只要以该锁为key,设置一个随机的值就行了。比如,我们现在有个秒杀的场景,并发量可能是3000,但是我们商品的库存数量是一定的,为了防止超卖,我们就需要在减库存的时候加上锁,当第一个请求过来的时候,先判断锁时候存在,不存在就加锁,然后去处理秒杀的业务,并且在处理完成的时候,释放锁,如果判断锁存在,就轮训等待锁被释放。

缺点

1、如果我们处理业务的时候报错了,那么加上的锁就不能及时被释放了,这时候我们需要加上一个异常的捕获,在finally的时候释放锁。

2、同时我们也要主要set写入key,是会出现覆盖操作的,我们要要注意使用setnx(只要锁被加上,后面的写入操作不会覆盖前面的写入)

2、但是,有可能我们在处理业务的时候,整个服务器、宕机了,那么异常的捕获显然是不管用了,这时候我们的这种设计显然是存在缺陷的。

Redis分布式锁方案二

上一节的方案缺点就是锁有时候没有办法释放,造成死锁。那么对于setnx我们应该怎样处理呢?

考虑一种情况,如果进程获得锁后,断开了与 Redis 的连接(可能是进程挂掉,或者网络中断),如果没有有效的释放锁的机制,那么其他进程都会处于一直等待的状态,即出现“死锁”。

上面在使用 SETNX 获得锁时,我们将键 lock.foo 的值设置为锁的有效时间,进程获得锁后,其他进程还会不断的检测锁是否已超时,如果超时,那么等待的进程也将有机会获得锁。

然而,锁超时时,我们不能简单地使用 DEL 命令删除键 lock.foo 以释放锁。考虑以下情况,进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。进程P2,P3正在不断地检测锁是否已释放或者已超时,执行流程如下:

1、P2和P3进程读取键 lock.foo 的值,检测锁是否已超时(通过比较当前时间和键 lock.foo 的值来判断是否超时)

2、P2和P3进程发现锁 lock.foo 已超时

3、P2执行 DEL lock.foo命令

4、P2执行 SETNX lock.foo命令,并返回1,即P2获得锁

5、P3执行 DEL lock.foo命令将P2刚刚设置的键 lock.foo 删除(这步是由于P3刚才已检测到锁已超时)

6、P3执行 SETNX lock.foo命令,并返回1,即P3获得锁

7、P2和P3同时获得了锁

从上面的情况可以得知,在检测到锁超时后,进程不能直接简单地执行 DEL 删除键的操作以获得锁。

为了解决上述算法可能出现的多个进程同时获得锁的问题,我们再来看以下的算法。

我们同样假设进程P1已经首先获得了锁 lock.foo,然后进程P1挂掉了。接下来的情况:

1、进程P4执行 SETNX lock.foo 以尝试获取锁

2、由于进程P1已获得了锁,所以P4执行 SETNX lock.foo 返回0,即获取锁失败

3、P4执行 GET lock.foo 来检测锁是否已超时,如果没超时,则等待一段时间,再次检测

4、如果P4检测到锁已超时,即当前的时间大于键 lock.foo 的值,P4会执行以下操作 GETSET lock.foo <current Unix timestamp + lock timeout + 1>

5、由于 GETSET 操作在设置键的值的同时,还会返回键的旧值,通过比较键 lock.foo 的旧值是否小于当前时间,可以判断进程是否已获得锁

6、假如另一个进程P5也检测到锁已超时,并在P4之前执行了 GETSET 操作,那么P4的 GETSET 操作返回的是一个大于当前时间的时间戳,这样P4就不会获得锁而继续等待。注意到,即使P4接下来将键 lock.foo 的值设置了比P5设置的更大的值也没影响。

另外,值得注意的是,在进程释放锁,即执行 DEL lock.foo 操作前,需要先判断锁是否已超时。如果锁已超时,那么锁可能已由其他进程获得,这时直接执行 DEL lock.foo 操作会导致把其他进程已获得的锁释放掉。

缺点

1、其实这种方案还存在缺陷,我们知道对于锁设置过期的时间,虽然可以解决锁的及时释放,但是我们知道我们需要处理的业务场景的时间的不可控,可能网络动荡,我们原来0.01秒的业务现在就需要3秒钟,所以简单的对锁设置过期的时间,还是存在缺陷的。

Redis分布式锁方案三

那么对于方案二的场景的缺点我们应该怎么去处理呢?有一个简单的方法就是当给一个锁加上过期的时间的时候,我们另外开启一个线程,去监测业务处理的时间,如果锁的时间快到了,并且业务还没有执行完毕,就给锁的时间延长,实现自旋的加锁。

下面是解决方案(不过只处理到了方案二,自旋锁没完成)

  1. // Lock 加锁
  2. func Lock(lockKey string) int64 {
  3. redisConn := GetRedisHandle().RedisClientHandle
  4.  
  5. expire := int64(8e3) // 锁有效期
  6. lt := // 获取锁时间
  7. i :=
  8. sleep := * time.Microsecond
  9. for {
  10. nowUnix := time.Now().UnixNano() / 1e6 // 取毫秒
  11. nv := nowUnix + expire
  12. s := redisConn.SetNX(lockKey, nv, ).Val()
  13. if s {
  14. return nv
  15. }
  16.  
  17. if lv := redisConn.Get(lockKey).Val(); lv != "" {
  18. e := String2Int64(lv)
  19. if e < nowUnix {
  20. // 锁已超时
  21.  
  22. t := String2Int64(redisConn.GetSet(lockKey, nv).Val())
  23. if e == t {
  24. return nv
  25. }
  26. }
  27. }
  28.  
  29. i +=
  30.  
  31. if i >= lt {
  32. // 不再一直等待
  33.  
  34. //return 0
  35. }
  36.  
  37. time.Sleep(sleep)
  38. }
  39. }
  40.  
  41. // Unlock 解锁
  42. func Unlock(lk string, lv int64) bool {
  43. redisConn := GetRedisHandle().RedisClientHandle
  44.  
  45. if ov := redisConn.Get(lk).Val(); ov != "" {
  46. if ovi := String2Int64(ov); ovi == lv { // 只对本线程解锁
  47. if d := redisConn.Del(lk); d.Err() == nil {
  48. return true
  49. }
  50. }
  51. }
  52.  
  53. return false
  54. }

上面三种方案还存在问题

上面的这类锁的最大缺点就是只作用在一个节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因放生了主从切换,那么就会出现锁丢失的情况:

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

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

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

4、导致锁丢失。

正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式Redlock。

Redlock实现

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

为了取到锁,客户端营该执行以下操作:

1、获取当前Unix时间,以毫秒为单位。

2、依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

3、客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。

4、如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。

5、如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

参考:https://mp.weixin.qq.com/s/9F6dor1p_j-nmNInBwJfCA

参考:https://redis.io/topics/distlock

  

redis中的分布式锁的更多相关文章

  1. 基于redis实现的分布式锁

    基于redis实现的分布式锁 我们知道,在多线程环境中,锁是实现共享资源互斥访问的重要机制,以保证任何时刻只有一个线程在访问共享资源.锁的基本原理是:用一个状态值表示锁,对锁的占用和释放通过状态值来标 ...

  2. 一个Redis实现的分布式锁

    import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.conne ...

  3. 基于Redis的简单分布式锁的原理

    参考资料:https://redis.io/commands/setnx 加锁是为了解决多线程的资源共享问题.Java中,单机环境的锁可以用synchronized和Lock,其他语言也都应该有自己的 ...

  4. redis客户端、分布式锁及数据一致性

    Redis Java客户端有很多的开源产品比如Redission.Jedis.lettuce等. Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持:Redis ...

  5. 如何用redis正确实现分布式锁?

    先把结论抛出来:redis无法正确实现分布式锁!即使是redis单节点也不行!redis的所谓分布式锁无法用在对锁要求严格的场景下,比如:同一个时间点只能有一个客户端获取锁. 首先来看下单节点下一般r ...

  6. redis系列:分布式锁

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

  7. Redis如何实现分布式锁

    今天我们来聊一聊分布式锁的那些事. 相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行加锁操作.在计算机科学中,锁(lock ...

  8. Redis高并发分布式锁详解

    为什么需要分布式锁 1.为了解决Java共享内存模型带来的线程安全问题,我们可以通过加锁来保证资源访问的单一,如JVM内置锁synchronized,类级别的锁ReentrantLock. 2.但是随 ...

  9. Redis系列(二)--分布式锁、分布式ID简单实现及思路

    分布式锁: Redis可以实现分布式锁,只是讨论Redis的实现思路,而真的实现分布式锁,Zookeeper更加可靠 为什么使用分布式锁: 单机环境下只存在多线程,通过同步操作就可以实现对并发环境的安 ...

随机推荐

  1. qt creator源码全方面分析(3-5)

    目录 qtcreatorlibrary.pri 使用实例 上半部 下半部 结果 qtcreatorlibrary.pri 上一章节,我们介绍了src.pro,这里乘此机会,把src目录下的所有项目文件 ...

  2. XSS-Labs(Level1-10)

    Level-1 简单尝试 使用基础poc<script>alert(1)</script> 代码审计 <?php ini_set("display_errors ...

  3. [Docker6] Docker compose多容器运行与管理

    六.Docker compose docker compose就是通过yml文件来定义和运行多个容器docker应用程序的工具,三步过程就能跑起一个compose: 定义应用程序的环境(yml中) 定 ...

  4. Mysql性能优化:为什么要用覆盖索引?

    导读 相信读者看过很多MYSQL索引优化的文章,其中有很多优化的方法,比如最佳左前缀,覆盖索引等方法,但是你真正理解为什么要使用最佳左前缀,为什么使用覆盖索引会提升查询的效率吗? 本篇文章将从MYSQ ...

  5. [模板] LCA-最近公共祖先-倍增法

    2019-11-07 09:25:45 C.树之呼吸-叁之型-树上两点路径长度 Time Limit: 1000 MS Memory Limit: 32768 K Total Submit: 7 (4 ...

  6. leetcode 签到 面试题 17.16. 按摩师 动态规划

    题目: 一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接.在每次预约服务之间要有休息时间,因此她不能接受相邻的预约.给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长 ...

  7. 大型Java进阶专题(五) 设计模式之单例模式与原型模式

    前言 ​ 今天开始我们专题的第四课了,最近公司项目忙,没时间写,今天抽空继续.上篇文章对工厂模式进行了详细的讲解,想必大家对设计模式合理运用的好处深有感触.本章节将介绍:单例模式与原型模式.本章节参考 ...

  8. PYTHON数据类型(基础)

    PYTHON数据类型(基础) 一.列表.字典.元祖.集合的基本操作 列表 创建 l1=[] l1=list() l1=list(['你好',6]) 增 l1.append('hu') l1.inser ...

  9. GitLab → 搭建中常遇的问题与日常维护

    开心一刻 隔壁有一个80多岁的老大爷,昨天在小区的一棵树下发现一条黑色的蛇,冻僵了,大爷善心大发,就把蛇揣在了怀里,想给它一点温暖. 今天一大早看到大爷在树上挂了一个牌子,写到:不准随地大小便! 搭建 ...

  10. OpenCV-Python 直方图-4:直方图反投影 | 二十九

    目标 在本章中,我们将学习直方图反投影. 理论 这是由Michael J. Swain和Dana H. Ballard在他们的论文<通过颜色直方图索引>中提出的. 用简单的话说是什么意思? ...