【Redis】分布式锁


1 🍑什么是分布式锁?🍑

在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况,此时就需要通过 来做互斥控制,避免出现类似于 “线程安全” 的问题。
javasynchronized 或者 C++std::mutex, 这样的锁都是只能在当前进程中⽣效,在分布式的这种多个进程多个主机的场景下就⽆能为⼒了,此时就需要使⽤到分布式锁。

分布式锁本质上就是使⽤⼀个公共的服务器来记录加锁状态,这个公共的服务器可以是 Redis, 也可以是其他组件(⽐如 MySQL 或者 ZooKeeper 等), 还可以是我们⾃⼰写的⼀个服务等。


2 🍑分布式锁的基础实现🍑

思路⾮常简单,本质上就是通过⼀个键值对来标识锁的状态。
举个例⼦: 考虑买票的场景,现在⻋站提供了若⼲个⻋次,每个⻋次的票数都是固定的,现在存在多个服务器节点, 都可能需要处理这个买票的逻辑: 先查询指定⻋次的余票, 如果余票 > 0, 则设置余票值 -= 1。

显然上面就可能出现 “超卖” 的情况,此时如何进⾏加锁呢? 我们可以在上述架构中引⼊⼀个 Redis , 作为分布式锁的管理器。

此时, 如果买票服务器1尝试买票,就需要先访问 Redis,在 Redis 上设置⼀个键值对。⽐如 key 就是⻋次, value 随便设置个值 。
如果这个操作设置成功,就视为当前没有节点对该 001 ⻋次加锁, 就可以进⾏数据库的读写操作。操作完成之后, 再把 Redis 上刚才的这个键值对给删除掉。如果在买票服务器1操作数据库的过程中, 买票服务器2也想买票, 也会尝试给 Redis 上写⼀个键值对,key 同样是⻋次,但是此时设置的时候发现该⻋次的 key 已经存在了, 则认为已经有其他服务器正在持有锁, 此时服务器2就需要等待或者暂时放弃。

Redis 中提供了 setnx 操作, 正好适合这个场景。即key 不存在就设置, 存在则直接失败

但是上述方案还存在着一个问题:当服务器1加锁之后,开始处理买票的过程中, 如果服务器1意外宕机了, 就会导致解锁操作 (删除key) 不能执⾏,就可能引起其他服务器始终⽆法获取到锁的情况。

那么如何解决呢?


3 🍑引入过期时间🍑

为了解决上面存在的问题,我们可以引入过期时间,即这个锁最多持有多久, 就应该被释放。

可以使⽤ set ex nx 的⽅式, 在设置锁的同时把过期时间设置进去。

但是要注意,此处的过期时间只能使用一个命令的⽅式设置。如果分开多个操作, ⽐如 setnx 之后, 再来⼀个单独的 expire,由于 Redis 的多个指令之间不存在关联,并且即使使⽤了事务也不能保证这两个操作都⼀定成功, 因此就可能出现 setnx 成功, 但是 expire 失败的情况,此时仍然会出现⽆法正确释放锁的问题。

我们引入了过期时间后可以解决锁不被释放的问题,但是现在新的问题又来了,⽐如服务器1写⼊⼀个 “001”: 1 这样的键值对,服务器2是完全可以把 “001” 给删除掉的,这样的操作就可能会导致一些问题,有什么办法可以解决吗?


4 🍑引入校验 id🍑

为了解决其他服务器可能会误删键值对的问题,我们引入了一个校验ID,我们就可以将键值对的val设置成服务器的身份编号,这样当要进行del时,要先进行判断,只有该服务器的身份编号与键值对的val相同时才允许删除。

逻辑⽤伪代码描述如下:

String key = [要加锁的资源 id];
String serverId = [服务器的编号];
// 加锁, 设置过期时间为 10s
redis.set(key, serverId, "NX", "EX", "10s");
doSomeThing();
// 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配. 
if (redis.get(key) == serverId) 
{
 redis.del(key);
}

通过伪代码我们不难发现当前设计还存在着一些问题: 解锁逻辑是两步操作 getdel, 这样做并⾮是原⼦的。那应该怎么解决呢?


5 🍑引入 lua 脚本🍑

为了使解锁操作原⼦,可以使⽤ Redis 的 Lua 脚本功能。

Lua 也是⼀个编程语⾔,读作 “撸啊”,是葡萄⽛语中的 “⽉亮” 的意思。Lua 的语法类似于 JS,是⼀个动态弱类型的语⾔。Lua 的解释器⼀般使⽤ C 语⾔实现,Lua 语法简单精炼, 执⾏速度快, 解释器也⽐较轻量(Lua 解释器的可执⾏程序体积只有 200KB 左右)。

因此 Lua 经常作为其他程序内部嵌⼊的脚本语⾔。Redis 本⾝就⽀持 Lua 作为内嵌脚本,很多程序都⽀持内嵌脚本, ⽐如 MySQL 8 ⽀持 JS 作为内嵌脚本, ⽐如 Vim ⽀持 VimScript和 Python 作为内嵌脚本… 通过内嵌脚本来实现更复杂的功能, 提供更强的扩展性。Lua 除了和 Redis 搭伙之外, 在很多场景也会作为内嵌脚本,⽐如在游戏开发领域常常作为编写逻辑的语⾔。 (⽐如魔兽世界, ⼤话西游等)

使⽤ Lua 脚本完成上述解锁功能:

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

上述代码可以编写成⼀个 .lua 后缀的⽂件, 由 redis-cli 或者 redis-plus-plus 或者 jedis 等客⼾端加载,并发送给 Redis 服务器,由 Redis 服务器来执⾏这段逻辑。

⼀个 lua 脚本会被 Redis 服务器以原⼦的⽅式来执行

引入了 lua 脚本脚本后还有一个问题: 当我们设置了 key 过期时间之后,仍然存在⼀定的可能性当任务还没执⾏完,key 就先过期了,这就导致锁提前失效。那么过期时间应该如何设置呢?


6 🍑引入 watch dog🍑

watch dog 也被称作看门狗。所谓 watch dog,本质上是加锁的服务器上的⼀个单独的线程, 通过这个线程来对锁过期时间进⾏ “续约”。

注意, 这个线程是业务服务器上的, 不是 Redis 服务器的。

举个具体的例⼦:
假设初始情况下设置过期时间为 10s,同时设定看⻔狗线程每隔 3s 检测⼀次,那么当 3s 时间到的时候, 看⻔狗就会判定当前任务是否完成。

  • 如果任务已经完成,则直接通过 lua 脚本的⽅式,释放锁。(删除 key)
  • 如果任务未完成,则把过期时间重写设置为 10s。(即 “续约”)

注意上面动态调整过期时间要根据实际的业务场景进行设置,我这里只是随便设置了个数据方便大家理解而已。

这样就不担⼼锁提前失效的问题了,⽽且另⼀⽅⾯如果该服务器挂了,看⻔狗线程也就随之挂了,此时⽆⼈续约, 这个 key ⾃然就可以迅速过期,让其他服务器能够获取到锁了。

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值