首页 > 其他 > 详细

使用 Redis 如何设计分布式锁?

时间:2020-08-10 19:42:27      阅读:68      评论:0      收藏:0      [点我收藏+]

一、什么是分布式锁?

要使用redis来设计分布式锁,首先要了解什么是分布式锁,而要了解什么是分布式锁,先要提到与分布式锁相对应的线程锁和进程锁。
线程锁:线程锁主要是用来给方法和代码块加锁。当某个方法或者某段代码使用线程锁时,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一个JVM中有效果,因为线程锁的实现根本上是依靠线程之间共享内存来实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
进程锁:为了控制同一个操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁来实现进程锁。
分布式锁:当我们在某一生产环境中启动多个订单服务时,就是多个JVM,内存中的锁显然是不共享的,每个JVM进程都有自己的锁,自然无法保证线程的互斥了,这个时候我们就需要使用到分布式锁。也就是说,当多个进程不在同一个系统中时,我们用分布式锁来控制多个进程对资源的访问。

二、分布式锁的使用场景

线程间的并发问题和进程间的并发问题都是可以通过分布式锁来解决的,但是强烈不建议这么做!因为采用分布式锁来解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。

比如有这么一个场景,线程A和线程B都共享某个变量X。
如果是在单机情况下(即单JVM),线程之间共享内存,那么用线程锁就能解决并发问题。
如果是在分布式情况下(即多JVM),线程A和线程B很可能不是在同一个JVM中,这样线程锁就无法使用了,这时候就需要用分布式锁来解决。

实现分布式锁常用的有三种解决方案:1.基于数据库实现 2.基于zookeeper的临时序列化节点实现 3.redis实现。本文我们介绍的就是redis的实现方式。

三、分布式锁的实现

实现分布式锁有 3 点需要注意:

  1. 互斥(即同一时刻只能有一个线程获取到锁)
  2. 不能死锁,因此锁信息必须是会过期超时的,不能让一个线程长期占有锁而导致死锁
  3. 容错(只要大部分 Redis 节点创建了这把锁就可以)

几个要用到的redis命令:

  1. SETNX(key,value)
    技术分享图片
  2. GET(key)
    技术分享图片
  3. GETSET(key,value)
    技术分享图片
  4. EXPIRE(key,seconds)
    技术分享图片

Redis官方给出了两种基于Redis实现分布式锁的方法。(点击这里获取详情)

四、Redis最普通的分布式锁(单Redis实例实现分布式锁)

1. 加锁
加锁实际上就是,在redis中使用SET key value [EX seconds] [PX milliseconds] NX命令给Key键设置一个值,为了避免死锁,还要给定一个过期时间。如执行以下命令:

SET lock_key random_value NX PX 5000

其中:
lock_key为resource_name。
random_value 为key的值(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。可以用snowflake算法生成分布式唯一id
NX表示只在key不存在时,才对key进行设置操作; PX 5000表示设置key的过期时间为5000毫秒。 即这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个5秒的自动失效时间(PX属性)

这样,如果上面的命令执行成功,则证明客户端获取到了锁。

2. 解锁
解锁即释放锁,解锁的过程就是将key键给删除。但是也不能乱删,比如说有这么个场景,客户端1先获取到了锁,但是阻塞了很长时间才执行完,比如说超过了30秒,这个时候可能已经自动释放锁了,此时客户端2可能已经获取到了锁,这个时候如果直接删除key的话肯定会出问题的,不能用客户端1的请求来将客户端2的锁给删除掉。这时候random_value的作用就体现出来了。可以用随机值和LUA脚本对key进行删除避免上述情况,因为脚本仅会删除value等于客户端1的value的key(value相当于客户端的一个签名)。

首先要判断key是否存在并且存储的值是否和传入的值一样,若是则删除key,解锁成功。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

技术分享图片

单节点Redis的分布式锁的实现比较简单,但是也存在比较大的问题,最重要的一点是,锁不具有可重入性。如果是Redis普通主从,那Redis主从异步复制,若主节点挂了的话(也就是key没有了),此时key还没同步到从节点,此时从节点切换到主节点,别人就可以set key,从而拿到锁,会造成比较严重的安全问题。

五、RedLock算法

这个场景是假设有一个Redis集群,有5个Redis master节点,这些节点完全互相独立,不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。这里我们确保将在每(5)个实例上使用此方法获取和释放锁,我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。

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

  1. 获取当前的时间戳,以毫秒为单位。
  2. 与上面类似,依次轮流尝试从5个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,因此这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少(N/2+1)个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功),否则影响其他客户端获取锁。
    技术分享图片

这个算法是异步的吗?

算法基于这样一个假设:虽然多个进程之间没有时钟同步,但每个进程都以相同的时钟频率前进,时间差相对于失效时间来说几乎可以忽略不计。这种假设和我们的真实世界非常接近:每个计算机都有一个本地时钟,我们可以容忍多个计算机之间有较小的时钟漂移。
从这点来说,我们必须再次强调我们的互相排斥规则:只有在锁的有效时间(在步骤3计算的结果)范围内客户端能够做完它的工作,锁的安全性才能得到保证(锁的实际有效时间通常要比设置的短,因为计算机之间有时钟漂移的现象)。

失败时重试

当客户端无法取到锁时,应该在一个随机延迟后重试,防止多个客户端在同时抢夺同一资源的锁(这样会导致脑裂,没有人会取到锁)。同样,客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低(必要的重试),所以,理想情况一下,客户端应该同时(并发地)向所有Redis发送SET命令。
需要强调,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,这样其他的客户端就不必非得等到锁过完“有效时间”才能取到(然而,如果已经存在网络分裂,客户端已经无法和Redis实例通信,此时就只能等待key的自动释放了,等于被惩罚了)。

释放锁

释放锁比较简单,向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁。

详情查看:
Redis中国官方网站-Redis分布式锁

使用 Redis 如何设计分布式锁?

原文:https://www.cnblogs.com/xiaozhengtongxue/p/13471141.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!