当前位置: 首页 > news >正文

Redis 分布式锁与 Lua 原子性实践

学习整理,如有错误,欢迎指正

分布式锁的原子性

分布式锁的典型实现(SETNX + DEL)在“获取锁、判断锁归属、释放锁”这一系列操作中,并非原子操作
在高并发场景下,多个线程可能发生竞态条件(race condition),从而导致:

  • 误删他人锁
  • 锁提前释放
  • 多线程同时进入临界区

因此,我们选择可以原子操作的LUA脚本

Redis 官方规定:

Lua 脚本在 Redis 中是单线程、原子执行的。

这是因为Redis 是单线程执行模型,Lua 脚本在执行期间不会被打断,也不会插入其他命令。

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

意味着:

  1. Redis 收到 Lua 脚本后
  2. 立刻独占执行,不插入任何其他命令
  3. 执行完才会处理其他请求lua操作redis

文档

lua文档:

Lua 教程 | 菜鸟教程

3.4.1 - 算术操作 | Lua 5.4 中文参考手册

lua操纵redis

文档

使用 Lua 脚本 | 文档 - Redis 文档

从脚本与redis交互

redis.call('命令名称','key','其它参数',...)

例:

存储

redis.call('set','username','jack')

获取并返回,使用local定义局部变量

localusername=redis.call('get','username')returnusername

脚本参数化

用来动态获取参数,具备拓展性

redis>EVAL"return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }"2key1 key2 arg1 arg2 arg31)"key1"2)"key2"3)"arg1"4)"arg2"5)"arg3"

函数搭配参数化

>EVAL"return redis.call('SET', KEYS[1], ARGV[1])"1foo bar OK

业务实现

用lua脚本实现分布式锁释放时

​ 1、获取锁中的线程标示

​ 2、判断是否与指定的标示(当前线程标示)一致

​ 3、如果一致则释放锁(删除)

​ 4、如果不一致则什么都不做

实现:

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示-- 获取锁中的标示,判断是否与当前线程标示一致if(redis.call('GET',KEYS[1])==ARGV[1])then-- 一致,则删除锁returnredis.call('DEL',KEYS[1])end-- 不一致,则直接返回return0

注:

这里 return redis.call(‘DEL’, KEYS[1])

返回值:

  • 1:删除成功(说明锁属于当前线程)
  • 0:删除失败(锁不存在或不属于当前线程)

java执行lua脚本

RedisTemplate中,可以利用execute方法去执行lua脚本

publicSimpleRedisLock(Stringname,StringRedisTemplatestringredisTemplate){this.stringredisTemplate=stringredisTemplate;this.name=name;}publicvoidunLock(){stringredisTemplate.execute(UNLOCK_Script,Collections.singletonList(KEY_PREFIX+name),ID_PREFIX+Thread.currentThread().getId());}

释放锁时执行lua脚本,实现了原子性

注意:

1、KEYS 必须是集合(List):因为 Lua 规范要求 KEYS 是数组,Redis 规定所有 Redis 的 key 必须放进 KEYS 数组,所以必须用 Collections.singletonList 包一层。

ARGV 不用是集合:因为 Spring 做了自动封装,你传单个值,它会自动帮你转成数组

另外,KEYS 不能自动封装!必须你自己传 List

KEYS 是 Redis 规定的 “Redis 键集合”,不是普通参数。

Spring 不敢帮你自动封装 KEYS,怕你传错、传乱,导致 Redis 集群直接报错

总结:

Redis Lua 脚本参数分为两类:

  • KEYS:用于传递 Redis Key(必须是 List)
  • ARGV:用于传递普通参数

在 Spring Data Redis 中:

  • KEYS必须手动封装为 List
  • ARGV可以自动封装

2、Collections.singletonList(xxx)把 1 个值,变成一个 “只有 1 个元素的数组 / List”

问题

1、不可重入

问题本质:同一个线程,拿了锁之后,不能再拿同一把锁

  • 比如:线程 A 获取了锁lock:order,执行业务逻辑时,又调用了一个同样需要获取lock:order的方法。
  • 普通SETNX实现的锁,因为 key 已经存在,会直接返回获取失败。
  • 结果:线程 A 自己把自己 “卡死” 了,业务无法继续执行。

为什么这是个问题?

很多业务场景天然需要可重入锁

  • 递归调用
  • 嵌套方法调用(外层拿了锁,内层方法也要拿同一把锁)
  • 框架内部封装导致的重复加锁(比如事务 + 锁)

普通SETNX锁不支持这种场景,会导致线程死锁。


2、不可重试

问题本质:抢锁失败一次,就直接放弃,没有重试机制

  • 当锁被其他线程占有时,当前线程调用SETNX会直接返回false
  • 实现里如果不加任何重试逻辑,线程就直接放弃,业务流程直接失败。

为什么这是个问题?

  • 分布式环境下,锁竞争很常见,很多时候只是暂时抢不到。

  • 不重试会导致:业务成功率低,系统可用性差

  • 正确的做法通常是:抢锁失败后,等待一段时间再重试(自旋 / 轮询),直到超时。


3、 超时释放(锁过期问题)

问题本质:锁超时释放虽然能防死锁,但会带来更危险的问题

锁加过期时间,初衷是为了防止死锁:

  • 线程 A 拿了锁,业务执行中宕机,没释放锁 → 锁会自动过期释放,不会永久占住。

但它有致命缺陷:

  1. 业务执行时间 > 锁过期时间

    • 线程 A 拿到锁,锁过期时间设为 30 秒。
    • 但业务逻辑执行需要 40 秒。
    • 30 秒时锁自动释放,线程 B 拿到了锁,开始执行业务。
    • 第 40 秒,线程 A 业务执行完,释放锁(此时释放的是线程 B 的锁)。
    • 线程 C 又拿到锁,同一时间多个线程执行业务,锁完全失效
  2. 释放了别人的锁

    • 线程 A 的锁超时释放后,线程 B 拿到锁。
    • 线程 A 还在执行,执行完后调用DEL释放锁,结果把线程 B 的锁删了。
    • 后续线程又能拿到锁,导致并发问题。

核心矛盾:

锁的过期时间,无法和业务执行时间强绑定

你设短了,业务没执行完锁就没了;设长了,宕机后锁要很久才释放,影响性能。


4、主从一致性问题

问题本质:Redis 主从集群下,锁数据同步延迟会导致锁失效

Redis 主从集群的工作方式:

  • 主节点写数据(SETNX),然后异步同步给从节点。
  • 客户端读请求可以走从节点,提高性能。

但锁场景下,会出大问题:

  1. 线程 A 向主节点请求加锁,主节点写入成功,但数据还没同步到从节点。
  2. 主节点突然宕机,哨兵 / 集群自动将从节点提升为新主节点。
  3. 新主节点里,还没有线程 A 的锁数据。
  4. 线程 B 向新主节点请求加锁,成功拿到了锁。
  5. 结果:线程 A 和线程 B 同时持有锁,锁完全失效

为什么这是个严重问题?

  • Redis 主从同步是异步的,存在时间窗口。

  • 锁的核心要求是强一致性,而 Redis 主从集群在故障切换时,无法保证锁数据不丢失。

  • 基于主从架构的普通分布式锁,无法保证极端故障下的锁安全

总结

总结对比下,lua脚本仍存在问题

为了解决:

  • 可重入问题
  • 自动续期问题
  • 主从一致性问题

工程中通常直接使用成熟方案,例如:Redisson

Redisson 提供:

  • 可重入锁(RLock)
  • 自动续期(WatchDog)
  • 公平锁 / 读写锁
  • 基于 Lua 的原子操作封装
实现方式优点缺点
SETNX + DEL简单非原子、误删锁
SET NX EX支持过期时间仍可能误删
Lua脚本原子释放锁不支持重入
Redisson功能完善依赖重

具体可参考我的博客
Redisson 分布式锁原理简析(可重入、续期与高可用)

http://www.cnnetsun.cn/news/2091665.html

相关文章:

  • 【GEO】为什么很多本地生活商家接不住 AI 流量?问题不在曝光,而在“临门一脚”
  • 从‘虹猫蓝兔’到终身学习:聊聊Continual Learning如何让AI模型像人一样成长
  • 汉字转拼音工具,即输即转可多格式导出
  • 英雄联盟国服终极换肤神器:R3nzSkin完整使用指南
  • 5分钟掌握WebSite-Downloader:Python网站离线下载终极指南
  • Sun-Panel:打造你的专属NAS门户,从零开始构建高效导航首页
  • DeepSeek V4 × 华为昇腾:国产AI算力推理适配的实质性进展
  • MCP 2026沙箱隔离配置错误率高达67%?3分钟自检清单+5行PowerShell诊断脚本(限首批200名读者获取)
  • LSTM时间序列预测实战:从原理到销售预测应用
  • golang如何集成Qdrant向量数据库_golang Qdrant向量数据库集成策略
  • 别再手动拼接URL了!手把手教你用web-view实现小程序与uni-app H5的优雅双向传参
  • 【工业级MCP网关开发白皮书】:基于C++20/Boost.Asio/FlatBuffers构建延迟<50μs的金融级网关
  • 猫抓cat-catch:浏览器资源嗅探神器,让网页资源下载变得如此简单
  • Agent 在二手车行业的落地实践
  • 终极指南:如何使用ncmdump快速免费解密网易云音乐NCM文件
  • TiDB 实战项目:从需求分析到生产级代码完整记录
  • 考研数学二图鉴——行列式
  • Visual Syslog Server:Windows上最直观的日志监控解决方案
  • SCPI指令获取不求人:以RS FSW为例,手把手教你用SCPI Recorder抓取‘隐藏’命令
  • 如何快速移除Unity游戏马赛克:5分钟完成配置的终极指南
  • AI Coding 选哪一家?2026 全面对比指南
  • 10个实用技巧:用AnimateDiff插件轻松制作AI动画视频
  • DamaiHelper:如何用智能自动化告别演唱会抢票焦虑?
  • 2026 年最新:Anthropic 注册政策变化及应对策略
  • 知识竞赛奖品清单推荐
  • RS485网络拓扑结构
  • PostgreSQL 在AWS的 T 系列实例上的性能陷阱
  • Mythsky维生素D3含量成分是什么?全面解析其营养构成
  • Superturtle:模块化命令行工具集的设计哲学与自动化实践
  • 工业机器人仿真与方形路径示教作业报告