redis应⽤场景:分布式锁
参考:
RedLock
什么是 RedLock
Redis 官⽅站这篇⽂章提出了⼀种权威的基于 Redis 实现分布式锁的⽅式名叫 Redlock,此种⽅式⽐原先的单节点的⽅法更安全。它可以保证以下特性:
安全特性:互斥访问,即永远只有⼀个 client 能拿到锁
避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了⽹络分区
容错性:只要⼤部分 Redis 节点存活就可以正常提供服务
怎么在单节点上实现分布式锁
SET resource_name my_random_value NX PX 30000
主要依靠上述命令,该命令仅当 Key 不存在时(NX保证)set 值,并且设置过期时间 3000ms (PX保证),值 my_random_value 必须是所有 client 和所有锁请求发⽣期间唯⼀的,释放锁的逻辑是:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
View Code
上述实现可以避免释放另⼀个client创建的锁,如果只有 del 命令的话,那么如果 client1 拿到 lock1 之后因为某些操作阻塞了很长时间,
此时 Redis 端 lock1 已经过期了并且已经被重新分配给了 client2,那么 client1 此时再去释放这把锁就
会造成 client2 原本获取到的锁被 client1 ⽆故释放了,
但现在为每个 client 分配⼀个 unique 的 string 值可以避免这个问题。⾄于如何去⽣成这个 unique string,⽅法很多随意选择⼀种就⾏了。
Redlock 算法
算法很易懂,起 5 个 master 节点,分布在不同的机房尽量保证可⽤性。为了获得锁,client 会进⾏如下操作:
1. 得到当前的时间,微秒单位
2. 尝试顺序地在 5 个实例上申请锁,当然需要使⽤相同的 key 和 random value,这⾥⼀个 client 需要合理设置与 master 节点沟通的 timeout ⼤
⼩,避免长时间和⼀个 fail 了的节点浪费时间
3. 当 client 在⼤于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采⽤获得锁的当下时间减去第
⼀步获得的时间戳得到,如果锁的持续时长(lock validity time)⽐流逝的时间多的话,那么锁就真正获取到了。
4. 如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间
5. 如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执⾏释放锁的操作,重置状态
失败重试
如果⼀个 client 申请锁失败了,那么它需要稍等⼀会在重试避免多个 client 同时申请锁的情况,最好的情况是⼀个 client 需要⼏乎同时向 5 个 master 发起锁申请。
另外就是如果 client 申请锁失败了它需要尽快在它曾经申请到锁的 master 上执⾏ unlock 操作,便于其他 client 获得这把锁,避免这些锁过期造成的时间浪费,
当然如果这时候⽹络分区使得 client ⽆法联系上这些 master,那么这种浪费就是不得不付出的代价了。
放锁
放锁操作很简单,就是依次释放所有节点上的锁就⾏了
性能、崩溃恢复和 fsync
如果我们的节点没有持久化机制,client 从 5 个 master 中的 3 个处获得了锁,然后其中⼀个重启了,这是注意整个环境中⼜出现了 3 个 master 可供另⼀个 client 申请同⼀把锁!违反了互斥性。
如果我们开启了 AOF 持久化那么情况会稍微好转⼀些,因为 Redis 的过期机制是语义层⾯实现的,所以在 server 挂了的时候时间依旧在流逝,重启之后锁状态不会受到污染。
但是考虑断电之后呢,AOF部分命令没来得及刷回磁盘直接丢失了,除⾮我们配置刷回策略为 fsnyc = always,但这会损伤性能。
解决这个问题的⽅法是,当⼀个节点重启之后,我们规定在 max TTL 期间它是不可⽤的,这样它就不会⼲扰原本已经申请到的锁,
等到它 crash 前的那部分锁都过期了,环境不存在历史锁了,那么再把这个节点加进来正常⼯作。
Redlock(redis分布式锁)原理分析
Redlock:全名叫做 Redis Distributed Lock;即使⽤redis实现的分布式锁;
使⽤场景:多个服务间保证同⼀时刻同⼀时间段内同⼀⽤户只能有⼀个请求(防⽌关键业务出现并发攻击);
官⽹⽂档地址如下:redis.io/topics/distlock
这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,
优点在于防⽌了单节点故障造成整个服务停⽌运⾏的情况;并且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有⾃⼰独特的设计⽅法;
此博客或者官⽅⽂档的相关概念:
1.TTL:Time To Live;只 redis key 的过期时间或有效⽣存时间
2.clock drift:时钟漂移;指两个电脑间时间流速基本相同的情况下,两个电脑(或两个进程间)时间的差值;如果电脑距离过远会造成时钟漂移值过⼤
最低保证分布式锁的有效性及安全性的要求如下:
1.互斥;任何时刻只能有⼀个client获取锁
2.释放死锁;即使锁定资源的服务崩溃或者分区,仍然能释放锁
3.容错性;只要多数redis节点(⼀半以上)在使⽤,client就可以获取和释放锁
⽹上讲的基于故障转移实现的redis主从⽆法真正实现Redlock:
因为redis在进⾏主从复制时是异步完成的,⽐如在clientA获取锁后,主redis复制数据到从redis过程中崩溃了,
导致没有复制到从redis中,然后从redis选举出⼀个升级为主redis,造成新的主redis没有clientA 设置的锁,这是clientB尝试获取锁,并且能够成功获取锁,导致互斥失效;
思考题:这个失败的原因是因为从redis⽴刻升级为主redis,如果能够过TTL时间再升级为主redis(延迟升级)后,
或者⽴刻升级为主redis但是过TTL的时间后再执⾏获取锁的任务,就能成功产⽣互斥效果;是不是这样就能实现基于redis主从的Redlock;
redis单实例中实现分布式锁的正确⽅式(原⼦性⾮常重要):
1.设置锁时,使⽤set命令,因为其包含了setnx,expire的功能,起到了原⼦操作的效果,给key设置随机值,并且只有在key不存在时才设置成功返回True,并且设置key的过期时间(最好⽤毫秒)
SET key_name my_random_value NX PX 30000 # NX 表⽰if not exist
2.在获取锁后,并完成相关业务后,需要删除⾃⼰设置的锁(必须是只能删除⾃⼰设置的锁,不能删除他⼈设置的锁);
删除原因:保证服务器资源的⾼利⽤效率,不⽤等到锁⾃动过期才删除;
删除⽅法:最好使⽤Lua脚本删除(redis保证执⾏此脚本时不执⾏其他操作,保证操作的原⼦性),代码如下;逻辑是先获取key,如果存在并且值是⾃⼰设置的就删除此key;否则就跳过;
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
View Code
算法流程图如下:
多节点redis实现的分布式锁算法(RedLock):有效防⽌单点故障
假设有5个完全独⽴的redis主服务器
1.获取当前时间戳
2.client尝试按照顺序使⽤相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间⽐锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下⼀个redis实例。
⽐如:TTL为5s,设置获取锁最多⽤1s,所以如果⼀秒内⽆法获取锁,就放弃获取这个锁,从⽽尝试获取下个锁
3.client通过获取所有能获取的锁后的时间减去第⼀步的时间,这个时间差要⼩于TTL时间并且⾄少有3
个redis实例成功获取锁,才算真正的获取锁成功
4如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差的时间;⽐如:TTL 是5s,获取所有锁⽤了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了⼩于3个锁,必须释放,否则影响其他client获取锁
算法⽰意图如下:
RedLock算法是否是异步算法?
可以看成是同步算法;因为即使进程间(多个电脑间)没有同步时钟,但是每个进程时间流速⼤致相同;
并且时钟漂移相对于TTL叫⼩,可以忽略,所以可以看成同步算法;(不够严谨,算法上要算上时钟漂移,因为如果两个电脑在地球两端,则时钟漂移⾮常⼤)
RedLock失败重试
当client不能获取锁时,应该在随机时间后重试获取锁;并且最好在同⼀时刻并发的把set命令发送给所有redis实例;
⽽且对于已经获取锁的client在完成任务后要及时释放锁,这是为了节省时间;
RedLock释放锁
由于释放锁时会判断这个锁的value是不是⾃⼰设置的,如果是才删除;所以在释放锁时⾮常简单,只要向所有实例都发出释放锁的命令,不⽤考虑能否成功释放锁;
RedLock注意点(Safety arguments):
1.先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,
第⼀个set命令之前是T1,最后⼀个set命令后为T2,则此client有效获取锁的最⼩时间为TTL-(T2-T1)-时钟漂移;
2.对于以N/2+ 1(也就是⼀半以上)的⽅式判断获取锁成功,是因为如果⼩于⼀半判断为成功的话,有可能出现多个client都成功获取锁的情况,从⽽使锁失效
springboot其实就是spring3.⼀个client锁定⼤多数事例耗费的时间⼤于或接近锁的过期时间,就认为锁⽆效,并且解锁这个redis实例(不执⾏业务) ;只要在TTL时间内成功获取⼀半以上的锁便是有效锁;否则⽆效
系统有活性的三个特征
1.能够⾃动释放锁
2.在获取锁失败(不到⼀半以上),或任务完成后能够⾃动释放锁,不⽤等到其⾃动过期
3.在client重试获取哦锁前(第⼀次失败到第⼆次重试时间间隔)⼤于第⼀次获取锁消耗的时间;
4.重试获取锁要有⼀定次数限制
RedLock性能及崩溃恢复的相关解决⽅法
1.如果redis没有持久化功能,在clientA获取锁成功后,所有redis重启,clientB能够再次获取到锁,这样违法了锁的排他互斥性;
2.如果启动AOF永久化存储,事情会好些,举例:当我们重启redis后,由于redis过期机制是按照unix时间戳⾛的,所以在重启后,然后会按照规定的时间过期,
不影响业务;但是由于AOF同步到磁盘的⽅式默认是每秒-次,如果在⼀秒内断电,会导致数据丢失,
⽴即重启会造成锁互斥性失效;但如果同步磁盘⽅式使⽤Always(每⼀个写命令都同步到硬盘)造成性能急剧下降;所以在锁完全有效性和性能⽅⾯要有所取舍;
3.有效解决既保证锁完全有效性及性能⾼效及即使断电情况的⽅法是redis同步到磁盘⽅式保持默认的每秒,
在redis⽆论因为什么原因停掉后要等待TTL时间后再重启(学名:延迟重启) ;缺点是在TTL时间内服务相当于暂停状态;
SpringBoot+Redis分布式锁:模拟抢单
stjedis;
import org.springframework.beans.factory.annotation.Autowired;
import org.st.context.SpringBootTest;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
/**
* @ClassName: Test
* @Description: 测试分布式锁
*/
@SpringBootTest
public class Test {
@Autowired
private JedisPool jedisPool;
/
**
* @Description: 获取锁
* @parameter: key
* @parameter: val
* @return: boolean
*/
public boolean setnx(String key, String val) {
Jedis jedis = null;
try {
jedis = Resource();
if (jedis == null) {
return false;
}
SetParams setParams=new SetParams();
//NX:是否存在key,存在就不set成功
//PX:key过期时间单位设置为毫秒(EX:单位秒)
<().px(1000*60);
return jedis.set(key, val,setParams).
equalsIgnoreCase("ok");
} catch (Exception ex) {
} finally {
if (jedis != null) {
jedis.close();
}
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论