本文介绍了“如何在Redis中实现分布式锁”的知识。很多人在实际案例的操作中会遇到这样的困难。让边肖带领你学习如何处理这些情况。希望大家认真阅读,学点东西!
为什么需要分布式锁
为什么需要分布式锁
使用分布式锁的目的是确保只有一个客户端可以同时操作共享资源。
在分布式应用程序的逻辑处理中,我们经常会遇到并发问题。【相关推荐:Redis视频教程】
比如一个操作需要修改用户的状态,需要先读出用户的状态,然后在内存中进行修改,修改后再保存回来。如果同时执行这样的操作,就会出现并发问题,因为读取和保存状态这两个操作不是原子的。
此时,应该使用分布式锁来限制程序的并发执行。Redis作为缓存中间件系统,可以提供这种分布式锁定机制。
其本质就是在redis里面占一个坑,当别的进程也要来占坑时,发现已经被占领了,就只要等待稍后再尝试
一般来说,生产环境可用的分布式锁需要满足以下几点:
互斥,这是锁的基本特性,只有一个线程可以同时持有锁和执行关键操作。
超时释放是锁的另一个本质特性,可以与MySQL InnoDB引擎中的innodb_lock_wait_timeout配置进行对比,通过超时释放来防止不必要的线程等待和资源浪费。
再入,在分布式环境下,如果同一个节点上的同一个线程获取了锁,仍然可以再次成功请求;
实现方式
使用SETNX实现
SETNX的使用方式是:SETNX键值,只在key key不存在的时候将key key的值设置为value,如果key key存在,则SETNX不做任何事情。
boolean result=jedis . setnx(' lock-key ',true)==1L;
if(结果){ 0
尝试{
//dosomething
}最后{
jedis . del(' lock-key ');
}
}这个方案有一个致命的问题,就是一个线程获取锁后,由于一些异常因素(比如宕机)无法正常执行解锁操作,所以锁永远不会被释放。
为此,我们可以给这个锁添加一个超时。
执行SET键值EX秒的效果与执行SETEX键值秒的效果相同。
执行SET键值PX毫秒的效果相当于执行PSETEX键值毫秒。
string result=jedis . set(' lock-key ',true,5);
if('OK '。equals(result)){ 0
尝试{
//dosomething
}最后{
jedis . del(' lock-key ');
}
}方案看上去很完美,但实际上还是会有问题
假设线程a获得了锁,并将到期时间设置为10s。
然后在执行业务逻辑的时候耗费了15s,此时线程A获取的锁早已被Redis的过期机制自动释放了
在线程A获取锁并经过10s之后,改锁可能已经被其它线程获取到了。当线程A执行完业务逻辑准备解锁(DEL key
)的时候,有可能删除掉的是其它线程已经获取到的锁。
所以最好的方式是在解锁时判断锁是否是自己的,我们可以在设置key
的时候将value设置为一个唯一值uniqueValue
(可以是随机值、UUID、或者机器号+线程号的组合、签名等)。
当解锁时,也就是删除key的时候先判断一下key对应的value是否等于先前设置的值,如果相等才能删除key
String velue= String.valueOf(System.currentTimeMillis()) String result = jedis.set("lock-key",velue, 5); if ("OK".equals(result)) { try { // do something } finally { //非原子操作 if(jedis.get("lock-key")==value){ jedis.del("lock-key"); } } }
这里我们一眼就可以看出问题来:GET
和DEL
是两个分开的操作,在GET执行之后且在DEL执行之前的间隙是可能会发生异常的。
如果我们只要保证解锁的代码是原子性的就能解决问题了
这里我们引入了一种新的方式,就是Lua脚本,示例如下:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
其中ARGV[1]
表示设置key时指定的唯一值。
由于Lua脚本的原子性,在Redis执行该脚本的过程中,其他客户端的命令都需要等待该Lua脚本执行完才能执行。
确保过期时间大于业务执行时间
为了防止多个线程同时执行业务代码,需要确保过期时间大于业务执行时间
增加一个boolean类型的属性isOpenExpirationRenewal
,用来标识是否开启定时刷新过期时间
在增加一个scheduleExpirationRenewal
方法用于开启刷新过期时间的线程
加锁代码在获取锁成功后将isOpenExpirationRenewal置为true,并且调用scheduleExpirationRenewal
方法,开启刷新过期时间的线程
解锁代码增加一行代码,将isOpenExpirationRenewal属性置为false,停止刷新过期时间的线程轮询
Redisson实现
获取锁成功就会开启一个定时任务,定时任务会定期检查去续期
该定时调度每次调用的时间差是internalLockLeaseTime / 3
,也就10秒
默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20
秒的时候,就会进行一次续期,把锁重置成30秒
RedLock
在集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生
Redlock算法就是为了解决这个问题
使用 Redlock,需要提供多个 Redis
实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用大多数机制
加锁时,它会向过半节点发送 set指令,只要过半节点 set
成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock
需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些
Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。
假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作
-
客户端记录当前系统时间,以毫秒为单位;
-
依次尝试从 5 个 Redis 实例中,使用相同的 key 获取锁,当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间应该小于锁的失效时间,避免因为网络故障出现的问题;
-
客户端使用当前时间减去开始获取锁时间就得到了获取锁使用的时间,当且仅当从半数以上的 Redis 节点获取到锁,并且当使用的时间小于锁失效时间时,锁才算获取成功;
-
如果获取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,减少超时的几率;
-
如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁,即使是上一步操作请求失败的节点,防止因为服务端响应消息丢失,但是实际数据添加成功导致的不一致。
也就是说,假设锁30秒过期,三个节点加锁花了31秒,自然是加锁失败了
在 Redis 官方推荐的 Java 客户端 Redisson
中,内置了对 RedLock
的实现
RedLock问题:
RedLock 只是保证了锁的高可用性,并没有保证锁的正确性
RedLock 是一个严重依赖系统时钟的分布式系统
Martin 对 RedLock 的批评:
-
对于提升效率的场景下,RedLock 太重。
-
对于对正确性要求极高的场景下,RedLock 并不能保证正确性。
“Redis中怎么实现分布式锁”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/38080.html