Redis 分布式锁

目录

什么是分布式锁

分布式锁实现

锁的基础实现

过期时间

校验ID

[Lua 脚本](#Lua 脚本)

[watch dog(看门狗)](#watch dog(看门狗))

[Redlock 算法](#Redlock 算法)


什么是分布式锁

在之前的文章 线程安全问题_线程安全示例-CSDN博客 中,我们学习了如何使用 synchronized 来保证 多个线程访问共享资源 时,同一时刻只有一个线程能够执行某段代码的控制逻辑 ,避免出现线程安全问题,但是,synchronized 只能解决同一个 JVM 进程内多线程问题

而在分布式系统中,常常会涉及到多个 节点/进程 访问同一个公共资源的情况

那么,此时该如何保证同一时刻只有一个 节点/进程 能够访问共享资源呢?

此时就需要使用到分布式锁 了,而分布式锁的本质是利用外部存储 (如 Redis、zookeeper(zookeeper 实现分布式锁_zk实现分布式锁-CSDN博客)),来记录当前的加锁状态(谁持有资源) ,从而让成功加锁的 节点/进程 进行访问操作,其他 节点/进程 不能进行操作

接下来,我们就来看 Redis 如何实现分布式锁

分布式锁实现

锁的基础实现

当使用 Redis 来实现分布式锁时,我们可以通过一个键值对来标识锁的状态

例如,在购买某件商品时,服务器需要先查询商品的库存,若库存 > 0,则库存 -=1

而在上述场景中,存在多个服务器节点,它们都需要操作数据库中的商品库存,而此时若不进行加锁,就很可能会出现超卖问题

上述库存只减扣了一次,但却有两个服务器都返回了购买成功,此时就导致实际卖出的商品多于库存数量

那么,我们如何进行加锁操作呢?

我们在上述场景中引入Redis 作为分布式锁的管理器

此时,当服务器尝试减扣库存时,需要先访问 Redis,在 Redis 上设置一个商品键值对:

若设置失败,则加锁失败,当前不能对数据库进行读写操作,需要等待或暂时放弃

若设置成功,则加锁成功,服务器能够对数据库进行读写操作,操作完成后再删除 Redis 上的键值对即可解锁成功

而 Redis 中提供了原子性写入的 setnx 操作:SETNX key value,若 key 不存在就设置,返回1;存在则设置失败,返回 0,这样我们就可以根据返回值判断加锁是否成功

过期时间

上述我们通过 setnx操作来实现分布式锁的加锁和解锁操作,但此时存在问题:

当 服务器1 加锁成功后,开始进行库存减扣逻辑处理,然而,若 服务器1 在处理过程中意外宕机了,就会导致解锁操作(删除 key)不能执行,也就导致其他服务器始终无法获取到锁

那么这个问题该如何解决呢?

我们可以在设置 key 的同时设置过期时间 ,通过set key ex nx的方式,明确这个锁最多持有多少时间,就应该被释放

注意:在设置 key 的过期时间时,需使用set key ex nx 命令方式设置,若分开进行操作,先 setnx 再单独设置 expire,由于Redis 多个指令之间并不存在关联性 ,且即使使用Redis 事务也不能保证这两个操作一定都成功,因此,就很可能会出现 setnx 操作成功,但 expire 操作失败的情况,从而导致无法正确释放锁

校验ID

然而,此时还存在问题:Redis 中写入的加锁键值对,其他节点也是可以删除的

例如:服务器1 写入键值对:key: t_shirt,value:1,服务器2是可以将这个键值对进行删除的

为了解决这个问题,我们可以将键值对的值设置为服务器编号,如 key: t_shirt,value:001,此时,在进行解锁操作时,服务器会先检查 value 是否为自己的服务器编号,若是,则进行删除;若不是,则不能进行删除

java 复制代码
String key = "t_shirt";
String serverId = "001";
// 加锁,并设置过期时间为 10s

redis.set(key, serverId, "NX", "EX", "10s");
// 执行业务逻辑
...

// 解锁
if(redis.get(key) == serverId){
    redis.del(key)
}

但此时出现了新的问题:解锁逻辑的 get 和 del 操作并不是原子的

Lua 脚本

为了解决解锁操作的原子性问题,我们可以使用 Redis 的 Lua 脚本功能

Lua 的语法类似于 JS,是一个动态弱类型语言 ,Lua 的语法简单精炼,执行速度快,解释器也比较轻量,而 Redis 内嵌 Lua 脚本,通过脚本将多条 Redis 命令封装为一段代码,并在 Redis 中以单线程原子方式执行脚本

我们使用 Lua 脚本来进行原子性解锁

Lua 复制代码
-- unlock.lua
-- KEYS[1]: 锁的 Key
-- ARGV[1]: 加锁时传入的唯一标识(如 serverId + UUID + ThreadId)

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

watch dog(看门狗)

在之前,为了防止加锁后无法解锁问题,我们引入了过期时间,但此时存在一个新的问题:设置过期时间后,任务还未完成,key 就先过期了,从而导致锁提前失效

由于业务逻辑执行时间的不确定性,我们也就无法保证任务一定能在过期时间前执行完成;而若我们设置更大的过期时间,就会导致若获取锁的服务器宕机,其他服务器不能及时地获取到锁

那么,该如何解决上述问题呢?

我们可以使用watch dog(看门狗) 来对锁的过期时间进行动态"续约", watch dog 是一个后台守护线程,在持有锁期间自动为锁"续期",防止业务未执行完锁就因过期而被释放:

设置初始情况下过期时间(TTL = 10s),并设置 watch dog 每隔 3s 检测一次:

若检测到任务未完成,执行续期操作,重置 TTL = 10s

若任务已完成,释放锁

此时,就不会出现锁提前失效的问题了,且若持有锁的服务器挂了,watch dog 线程也随之挂了,此时没有线程执行续期操作,key 也可以快速过期,让其他服务器获取到锁

Redlock 算法

在实际情况中,Redis 通常是以集群的方式部署的,此时就可能会出现以下极端情况:

服务器1向 master 节点进行加锁操作,刚刚写入 key,还未将其同步给 slave,master 就挂了;此时挑选出的 slave 节点升级为 新master,之前写入的 key 就丢失了,导致其他服务器仍然可以进行加锁操作

为了解决这个问题,Redis 提供了 Redlock分布式锁算法,通过向多个独立的 Redis 节点依次申请锁,只有获得 多数派节点的锁且总耗时小于锁有效期,才认为加锁成功。

通过一组 Redis 节点(每组 Redis 节点都包含一个 master 和若干 slave),每组节点存储的数据都是一致的,相互为备份关系

在进行加锁时,按照一定的顺序,向多个 master 节点执行写操作 ,并设置写操作的超时时间,如 set 操作必须在 50ms 内完成;若超过 50ms 还未完成,则加锁失败:

若某个节点加锁失败,立即让下一个节点尝试加锁

加锁成功的节点数超过总节点数的一半时,加锁成功

此时,就算这组节点中的某些节点挂了,其他节点中也存储了正确的锁信息

在释放锁时,也需要将所有节点都进行解锁操作(即使是之前超时的节点,也要尝试解锁)

相关推荐
尚雷558010 小时前
Oracle 18C 物理 DataGuard 搭建部署完整文档(适合开发测试)
数据库·oracle·dataguard
金仓数据库10 小时前
性能提升超十倍!金仓时序数据库首入北京轨交TCC
数据库·时序数据库
java1234_小锋11 小时前
Redis 如何实现持久化?RDB 和 AOF 的区别是什么?如何选择合适的持久化方式?
数据库·redis·bootstrap
倔强的石头10611 小时前
深度解析:数据库内核如何通过逻辑推理与常值推导突破去重性能瓶颈
数据库·oracle
为什么不问问神奇的海螺呢丶11 小时前
Oracle database SYSAUX 表空间占用率过高处理方案
数据库·oracle
fengxin_rou11 小时前
【MySQL SQL 执行全链路剖析】:执行计划、慢查询与经典场景优化指南
数据库·sql·mysql
betazhou11 小时前
LOG_ARCHIVE_DEST_2 ORA-01033: ORACLE initialization or shut
数据库·oracle·oracle19c adg
思诺学长11 小时前
MySQL——数据库并发控制策略: 乐观锁与悲观锁
数据库
fengxin_rou11 小时前
【Spring AI 集成 DeepSeek 实现 AI 摘要与 RAG 问答】:从原理到落地实践
数据库·mysql·rag·deepseek