深入探讨redis:分布式锁

目录

什么是分布式锁

分布式锁的基础实现

基于setnx的分布式锁

引入校验id

引入lua脚本

过期时间续约问题

redlock算法


什么是分布式锁

分布式锁,也就是在分布式系统中用的锁,在分布式系统中也会遇见像多个节点访问同一个公共资源的情况,随机产生类似线程安全资源这样的问题,和java中的synchronized以及C++中的std::mutex这种锁不同的是,分布式系统中竞争的公共资源的从线程升级为进程。

分布式锁的基础实现

本质上就是使用⼀个公共的服务器,来记录加锁状态。(Redis是一种典型的可以用来实现分布式锁的方案,但是并不是唯一的一种,像mysql/zookeeper这样的组件也可以实现分布式锁)

  1. 假如此时服务器1,想要对数据库进行操作,就需要先访问redis,在redis上设置一个键值对key,value(可以自定)。
  2. 如果这个键值对设置成功就说明当前没有服务器对数据库进行这个操作,那么服务器1就可以继续自己的操作,操作完之后再把redis上的这个键值对删掉。
  3. 如果在服务器1对数据库操作的过程中,服务器2也想进行该操作,那么服务器2也会先尝试在redis上写下相同的key,但是此时redis上已经有了,服务器2就知道已经有其他服务器正在持有锁(操作数据库),于是服务器2就需要等待或者暂时放弃操作。

这个过程就相当于是上厕所,当我们上厕所时发现测试门没有关,就说明里面没人,于是我们就会进去并把门锁住,如果我们还没上完又有人想来上厕所,他发现厕所门是锁着的就知道里面有人,这是他就会选择等一等或者暂时放弃先憋一会。

基于setnx的分布式锁

当有服务器对数据库进行操作时使用setnx加锁,再有服务器过来设置相同的键值对就会失败,操作完后再给这个键值对使用del命令删掉。

但是这样也会存在一个问题,就是如果服务器成功加锁,但是在后续执行操作的过程中宕机了,这个操作没有执行完也就不会执行del操作,redis上的锁也就无法解开了 。分布式锁和线程锁不同,java中即使出现错误也可以使用finally来解决就算程序崩了也会自动释放锁,但是这里虽然服务器宕机了,但是redis是好好的也就不会自动删除key

解决方法:

我们可以使用set ex nx设置一个过期时间,一旦时间到key就自动被删除了,这样就算没有人来解锁redis也会自己将锁释放掉。

注意,加锁并设置过期时间最好使用一条语句,尽量避免先setnx 然后再来一个expire,因为redis上的多个命令之间无法保证原子性,就可能出现一条语句成功一条语句失败,redis的事务机制功能不像mysql那样强大,一个事务里有一个出错就会将所有命令回滚,而redis里就只能保证多个命令一起执行。

引入校验id

对于分布式锁,会不会出现这种情况,就算服务器1加锁,服务器2解锁,虽然正常的情况下是不会出现这种情况的,但是避免不了会有人恶意破坏,而且想要解锁也十分简单只需要把键值对删掉就行。

针对上述问题,就需要引入一点校验机制不能让锁随随便便就被解开

给每个服务器进行编号,并且在加锁的时候设置key value,key要对应要加锁的资源,value就可以存储刚才的服务器编号,后续在解锁时就先验证,服务器编号是否一致,通过这个方法就可以有效避免误解锁。

引入lua脚本

虽然通过刚刚引入校验id的方法有效避免了误解锁,但是还有一个问题,上面的校验过程并不是原子的,而是要先校验再删除

  1. 一个服务器上可能会有多个线程同时访问,既然这样那么就有可能服务器A的线程1执行解锁操作操作,get1,del1,而线程B也执行解锁操作,get2,del2。
  2. 假如线程A先执行get1校验通过,然后线程B也执行get2,因为key还没有删所以线程2也可以校验成功,此时两个线程因为在一台服务器上所以都校验成功。
  3. 然后线程A执行del1,此时key就被删除了,就算线程2后面再执行del2也没用。但是,如果线程2还没来的及执行del2,就又来了了一个服务器2的线程C,执行了加锁set nx ex操作。结果刚加完锁线程B就执行了del2,给新的key删除了,那么服务器2这锁就白加了。

//因为线程B之前已经通过了校验只是还没执行del2,所以对服务器2的加锁不用再次校验,而且虽然服务器1,和服务器2的id不同,但是争夺的资源相同,所以加锁的key也是一样,只是value不同。

上面的问题究其原因还是校验操作不是原子的导致中间会被插入其他操作,这里虽然可以使用redis的事务来解决,但是还有更好的方法,就算引入lua脚本(redis官方也说明lua就属于是事务的替代方案)。

lua官方文档

Lua也是⼀个编程语言.读作"撸啊".是葡萄牙语中的"月亮"的意思。Lua的语法类似于JS,是⼀个动态弱类型的语言.Lua的解释器⼀般使用C语言实现.Lua语法简单精炼,执行速度快,解释器也比较轻量(Lua解释器的可执行程序体积只有200KB左右).因此Lua经常作为其他程序内部嵌入的脚本语言.Redis本⾝就支持Lua作为内嵌脚本

Lua 复制代码
//调用脚本给指定参数,此处传入一个服务器id
if redis.call('get',KEYS[1]) == ARGV[1] then 
//id匹配就执行删除
    return redis.call('del',KEYS[1]) 
else 
    return 0 
end;

通过上述逻辑就可以实现校验和删除操作,并且redis执行lua脚本的过程是原子性的相当于一条指令。

过期时间续约问题

我们在加锁的时候,给key设置过期时间,但是设置多少合适呢?

  • 如果设置短了,可能操作还没执行完就把锁释放了
  • 如果设置长了,可能就会导致锁释放不及时,而出现占用redis空间等问题,而且如果加锁服务器中间挂了的话,其他服务器也无法即使获取到锁

所以更好的方法就是进行"动态续约",我们引入一个线程,初始情况下设置一个过期时间(1s),假如再还剩300ms时这个线程就会判定当前任务是否完成,如果完成就直接释放锁,如果没有完成就把过期时间再续约1s,直到操作执行完。

这个线程就是**"看门狗"** ,而且这个看门狗线程是在业务服务器上,不是在redis服务器。所以就算服务器挂了,那么看门狗线程也随之挂了,也就没人复杂续约时间了,key一到过期时间自己就释放了。

redlock算法

刚刚我们说的问题都是业务服务器挂了而引起的,如果是redis服务器直接挂了呢。

假如此时服务器给主节点加锁(加锁就是把key加到主节点,然后再同步给从节点),但是刚加完锁主节点立马挂了,而主节点和从节点之间的数据同步是有延时的,此时key还没来得及同步给从节点 ,加锁的数据也就没有了,之后哨兵选出新的主节点,但是这个主节点上没有锁信息,整个加锁过程也就形同虚设

针对这个问题redis作者给出了一种算法也就是redlock算法

算法的核心就是加锁操作不能只写在一个主节点上,而要写在多个主节点上

我们引入几组redis节点,每一组都有自己的主节点,而且各个节点之间的数据是同步的 ,而不是整体数据的一部分(不同于redis集群)。当我们要加锁时,需要按照顺序给每一组redis节点都加上锁 ,如果有节点挂了或者超过设置的加锁超时时间就认为加锁失败直接跳过去对下一个节点加锁,反之加锁成功,当成功加锁的节点个数大于总数的一半时,就认为最终加锁成功,否则加锁失败。同理解锁时也要给每个节点的锁都解开。

⼀个分布式系统不至于大部分节点都同时出现故障,因此这样的可靠性要比单个节点来说靠谱不

少 。

这里我介绍的只是一个简单的互斥锁,基于redis还可以实现可重入锁,公平锁等不同性质的锁。

相关推荐
Fency咖啡2 小时前
Redis进阶 - 数据结构底层机制
数据结构·数据库·redis
gggg远2 小时前
Redis 高级篇(未完结1/3)
数据库·redis·缓存
hzk的学习笔记2 小时前
Redis分布式锁的最佳实践:基于Redisson的实现方案
数据库·redis·分布式·缓存
稻香味秋天2 小时前
Redis 在项目中的常见使用场景
数据库·redis·缓存
做运维的阿瑞2 小时前
Redis 高可用集群部署实战:单Docker实现1主2从3
java·redis·docker
Vaclee2 小时前
Redis进阶
数据库·redis·缓存
诗9趁年华2 小时前
Cache-Aside模式下Redis与MySQL数据一致性问题分析
数据库·redis·mysql
L.EscaRC2 小时前
Redis 底层运行机制与原理浅析
数据库·redis·缓存
爱吃烤鸡翅的酸菜鱼2 小时前
Java【缓存设计】定时任务+分布式锁实战:Redis vs Redisson实现状态自动扭转以及全量刷新预热机制
java·redis·分布式·缓存·rabbitmq