Redis Lua 脚本为什么天然具备原子性?

文章目录

  • 一、原子性到底是什么意思?
  • [二、为什么 Redis 能做到这一点?](#二、为什么 Redis 能做到这一点?)
    • [1. Redis 采用单线程事件循环](#1. Redis 采用单线程事件循环)
  • 三、脚本内部多条命令为何不会"被插队"?
  • 四、脚本执行期间禁止阻塞,原因也是原子性
  • [五、Lua 脚本 vs 事务(MULTI/EXEC)](#五、Lua 脚本 vs 事务(MULTI/EXEC))
  • [六、Lua 脚本应用:库存扣减 + 一人一单](#六、Lua 脚本应用:库存扣减 + 一人一单)
  • [七、Lua 脚本是否"绝对原子"?边界在哪里?](#七、Lua 脚本是否“绝对原子”?边界在哪里?)
    • [1. 脚本不能跨节点(Redis Cluster)](#1. 脚本不能跨节点(Redis Cluster))
    • [2. 脚本执行时间过长会阻塞整个 Redis](#2. 脚本执行时间过长会阻塞整个 Redis)
    • [3. 脚本无法取消](#3. 脚本无法取消)
  • 八、总结

在 Redis 的使用场景中,我们经常听到一句话:"Lua 脚本在 Redis 中具有原子性"。但很多工程师的理解通常停留在表层:Redis 是单线程的,所以天然原子。

这句话没错,但仍然过于粗糙。为什么单线程能保证脚本原子?什么叫原子?脚本内部明明执行了多条 Redis 命令,为什么不会被其他客户端"插队"?Redis 又做了什么保证?


一、原子性到底是什么意思?

先明确一个概念:

在 Redis 中执行一段 Lua 脚本时,整个脚本被视为一条命令,执行过程不可被分割,不会被其他请求打断。

也就是说:

  • 如果脚本里执行了 10 次 redis.call
  • 对 Redis 来说,它们仍然是一次不可分割的操作
  • 中途不会执行其他客户端的命令

这就是 Redis 所说的"原子性"。


二、为什么 Redis 能做到这一点?

原子性的根本原因不是 Lua,而是 Redis 的单线程架构

1. Redis 采用单线程事件循环

Redis 的核心线程负责:

  • 处理客户端请求
  • 执行命令
  • 管理网络 IO
  • 操作内部数据结构

在任何时刻,Redis 都只执行一个请求

无论请求是 GETSET,还是 EVAL(Lua 脚本),都必须排队顺序执行。

所以当 Lua 脚本被调起:

shell 复制代码
EVAL "......" 1 key

Redis 做的事情如下:

  1. 解析脚本
  2. 将脚本加载入 Lua VM
  3. 整段脚本一次性执行完毕
  4. 返回结果
  5. 处理下一个客户端请求

在脚本执行的整个过程中:

Redis 不会执行其他客户端的命令,也不会中途挂起脚本。

因此脚本内的多个操作自然成为原子的整体


三、脚本内部多条命令为何不会"被插队"?

例如脚本中这样写:

lua 复制代码
local stock = redis.call('GET', KEYS[1])
if stock > 0 then
    redis.call('DECR', KEYS[1])
end

表面上执行了两条命令(GET 和 DECR):

  • 读取库存
  • 扣减库存

如果由普通客户端发送两条命令:

复制代码
GET stock
DECR stock

在高并发下中间可能插入:

  • 其他客户端也 GET
  • 其他客户端也 DECR

并发条件下可能出现库存被超卖的问题。

但使用 Lua 之后:

  • GET + 判断 + DECR 变成一条"脚本命令"
  • 不存在被其他命令插队的可能

Redis 的执行模型确保脚本中所有调用都是一个整体。


四、脚本执行期间禁止阻塞,原因也是原子性

Redis 明确禁止 Lua 脚本做任何阻塞操作:

  • 不允许 sleep
  • 不允许 I/O
  • 不允许无限循环
  • 不允许访问网络

如果脚本可以阻塞,就无法保证 Redis 的事件循环继续执行,也就无法保证原子性。

从架构层面:

Redis 牺牲了脚本的复杂度,换取了脚本的确定性与原子性。


五、Lua 脚本 vs 事务(MULTI/EXEC)

很多人误以为 Redis 事务也能做到完全原子,但事实并非如此。

特性 Lua 脚本 MULTI/EXEC
原子性 完整原子 仅保证命令按顺序执行
并发干扰 不会被干扰 可能被其他客户端修改 key
WATCH 支持 不需要 需要使用 WATCH 做乐观锁
性能 高(一次网络往返) 多次网络往返

因此:

Redis 官方推荐复杂业务逻辑全部写 Lua,不要用事务拼命凑。


六、Lua 脚本应用:库存扣减 + 一人一单

这是 Lua 原子操作在电商领域最典型的应用。

一个常见的原子脚本:

lua 复制代码
-- KEYS[1] = stockKey
-- KEYS[2] = userKey
-- ARGV[1] = userId

local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
    return -1            -- 无库存
end

local exists = redis.call('SISMEMBER', KEYS[2], ARGV[1])
if exists == 1 then
    return -2            -- 已下单
end

redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])

return 1

这个脚本具备:

  • 检查库存
  • 检查是否下单
  • 扣库存
  • 写入"一人一单"标记

全部原子完成。

无论多少并发量,都不会发生:

  • 重复下单
  • 超卖
  • 并发状态覆盖

这就是 Lua 原子性在工程实战中的巨大价值。


七、Lua 脚本是否"绝对原子"?边界在哪里?

Lua 的原子性基于 Redis 单线程模型,但仍然有边界:

1. 脚本不能跨节点(Redis Cluster)

Cluster 下存在 key slot:

  • 如果脚本操作的 key 不在同一 slot,会执行失败
  • 因此 Cluster 环境的 Lua 只对"同 slot 的 key"原子

2. 脚本执行时间过长会阻塞整个 Redis

脚本执行时间长会导致:

  • 其他客户端全部堵塞
  • Redis 吞吐量瞬间下降
  • 极端情况下可能卡死

Redis 为此设置了:

复制代码
lua-time-limit

默认 5 秒。

3. 脚本无法取消

脚本开始执行后不能被中断,除非:

  • 人为 SCRIPT KILL(仅限无写操作脚本)
  • 强制杀 Redis(丢数据)

因此脚本逻辑必须精简。


八、总结

Redis Lua 脚本具备原子性,根本原因有三点:

  1. Redis 单线程执行命令,不会并发处理多个请求。
  2. Lua 脚本作为单条命令执行,中途不会被切换。
  3. 脚本内部的所有 redis.call 调用在同一执行上下文中完成。

因此,即使脚本内部包含多条 Redis 命令,整体仍然是不可拆分的原子操作。

在实际工程中,Lua 脚本常用于:

  • 秒杀/抢购库存扣减
  • 一人一单
  • 分布式锁验证
  • 限流计数
  • 复杂原子操作封装

它比 MULTI/EXEC 事务更强、性能更好,也是 Redis 官方推荐的复杂操作方案。

相关推荐
Miss_Chenzr2 小时前
Springboot文化艺术发展有限公司4rl42(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
码界奇点2 小时前
时序数据库界的速度与激情金仓数据库如何以技术创新超越InfluxDB
数据库·时序数据库·ux
Elastic 中国社区官方博客2 小时前
使用 Elasticsearch Agent Builder 构建对话式费用助手,结合 Telegram, n8n 和 AWS Bedrock
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·aws
Alex_81D2 小时前
Spring Data JPA以及JPQL等特性详细使用教程
java·数据库·后端
老马聊技术2 小时前
HBase完全分布式集群搭建详细教程
数据库·分布式·hbase
万邦科技Lafite2 小时前
淘宝开放API批量上架商品操作指南(2025年最新版)
开发语言·数据库·python·开放api·电商开放平台·淘宝开放平台
wtrees_松阳2 小时前
分布式锁实战指南:Redis、ZooKeeper、etcd 三大方案深度对比与避坑指南(附代码)
redis·分布式·zookeeper
Angletank2 小时前
SpringBoot中JPA组件深入查询业务实现
数据库·spring boot·后端·mysql
梦里不知身是客112 小时前
mysql的B+Tree介绍
数据库·mysql