Redis如何实现分布式锁(一)

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

简介

分布式锁就是用来控制同一时刻,只有一个JVM进程中的一个线程可以访问被保护的资源。

特性

  • 互斥:在同一时刻,一个资源只能有一把锁被一个客户端持有。
  • 无死锁:当持有锁的客户端奔溃后,锁仍然可以被其它客户端获取
  • 容错性:当部分节点失活之后,其余节点客户端依然可以获取和释放锁
  • 统一性:即释放锁的客户端只能由获取锁的客户端释放

为什么需要分布式锁

在单机环境下,当多个线程并发操作某个对象时,我们可以通过synchronized来保证同一时刻只能有一个线程获取对象的锁进行处理,其他的线程进行等待,但是随着微服务架构的流行和发展,现在的系统大部分都是基于分布式部署,即一个应用可能会部署在多台服务器上,然而synchronized只能保证当前服务器的线程安全,无法跨服务并发安全控制。所以我们才需要通过分布式锁来进行相关的控制。

Redis如何实现分布式锁

关于Redis分布式锁的实现分为单机环境和集群环境,不同的环境实现的方案不同。

图片.png

Redis单机环境实现

Redis提供了SETNX命令,设置key的值为value,只不过它会先判断key是否已经存在,如果key不存在,那么就设置key的值为value,并返回1;如果key已经存在,则不更新key的值,直接返回0:

加锁

客户端1对stock加锁

127.0.0.1:6379> setnx stock 100
(integer) 1//加锁成功

客户端2在对stock加锁,则加锁失败

127.0.0.1:6379> setnx stock 100
(integer) 0 //加锁失败

说明:此时说明只有客户端1加锁成功。

释放锁

客户端1加锁成功后,需要释放锁,redis提供了DEL来释放Redis锁

127.0.0.1:6379> DEL stock // 释放锁
(integer) 1

基本流程

图片.png

此加锁流程存在两个严重问题:

  • 1.如果程序处理出现异常,则锁无法进行释放。
  • 2.Redis客户端异常没有及时释放锁

这两个问题会导致客户端一致占用锁,其他的客户端无法获取锁,这种现象称之为死锁。

如何避免死锁

我们需要对加锁的对象设置过期时间,例如我们加锁stock对象并设置过期时间为10秒。

127.0.0.1:6379> setnx stock 100 
(integer) 1
127.0.0.1:6379> expire stock 10

但是这两个命令是分开执行,Redis是无法保证原子性的,所以在并发情况下,会出现如下的异常场景。

  • SETNX 执行成功,执行EXPIRE时由于网络问题,执行失败
  • SETNX 执行成功,Redis异常宕机,EXPIRE 没有机会执行
  • SETNX 执行成功,客户端异常崩溃,EXPIRE 也没有机会执行

所以由于这两个命令无法保证原子性操作,所以还是存在风险导致锁过期时间失败的场景,从而发生死锁。那么我们将如何进行处理?

在Reids2.6+的版本之后,Redis对Set命令进行了扩展,提供了当满足了当key不存在则设置 value,同时设置超时时间的命令,并且满足原子性。

SET key value NX PX 10000
  • NX:表示只有key不存在的时候才能SET成功,从而保证只有一个客户端可以获得锁;
  • PX: 表示这个锁有一个10秒自动过期时间。

虽然此命令设置值和过期时间保证了原子性,但是还是存在如下两个严重的问题:

  • 问题1:锁过期,当客户端1执行复杂的业务操作,业务还没执行完,导致锁自动释放了。
  • 问题2:释放其他客户端锁,这是因为客户端在释放锁时并没有进行客户端的唯一性验证,导致释放其他客户端锁。

问题1:锁过期问题

锁过期,可能是我们对于业务复杂度评估的锁的时间不正确导致,虽然我们可以调整锁的时间,例如将锁的时间从10秒调整到20秒等,但是我们无法并覆盖到所有导致耗时变长的场景,无法从根本上解决问题,那么有没有更好的方案呢?我们需要开启一个守护进程,如果锁快过期,但是业务还没执行完成,我们需要对所进行自动续期,重新设置过期时间,Redis为我们提供了Redisson来解决锁自动续期的问题。关于Redisson后续的文章将进行详细讲解。

问题2:释放其他客户端的锁

释放其他客户端锁的解决办法为,客户端在加锁时,设置一个唯一标识,我们可以采用UUID来进行标识,在释放锁时,我们需要进行判断,如果为自己客户端,才进行释放。

 if redis.get("stock") == $uuid:
    redis.del("stock")

注意:这里get和del操作在Redis是通过多个命令执行,所以我们需要考虑原子性,如何保证多个命令进行原子性操作,通过上一章我们知道,可以通过lua脚本来保证多个命令的原子性操作,修改为lua脚本如下:

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

通过相关的优化之后,我们通过Redis+lua脚本才能保证Reids分布式锁的加锁和释放锁的正确性。

小结

单机环境,Redis实现分布式锁,可以通过Set命令进行加锁,lua脚本释放锁.关于集群环境Redis如何分布式锁,将在后续的文章进行讲解。

总结

本文对单机环境下,Redis如何实现分布式锁进行了详细的讲解,如有疑问请随时反馈。

猜你喜欢

转载自juejin.im/post/7126714410562748453