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 官方推荐的复杂操作方案。

相关推荐
QQ_4376643148 分钟前
Redis协议与异步方式
数据库·redis·bootstrap
纪莫16 分钟前
技术面:MySQL篇(InnoDB事务执行过程、事务隔离级别、事务并发异常)
数据库·java面试⑧股
Nerd Nirvana26 分钟前
数据库模型全景:从原理到实践的系统性指南
数据库·oracle·电力行业
SelectDB29 分钟前
从 Greenplum 到 Doris:集群缩减 2/3、年省数百万,度小满构建超大规模数据分析平台经验
数据库·数据分析·apache
alonewolf_9936 分钟前
MySQL索引优化实战二:分页、关联查询与Count优化深度解析
数据库·mysql
oMcLin1 小时前
如何在 Debian 10 上配置并优化 Redis 集群,确保低延迟高并发的实时数据缓存与查询
redis·缓存·debian
TDengine (老段)1 小时前
TDengine Python 连接器进阶指南
大数据·数据库·python·物联网·时序数据库·tdengine·涛思数据
赵渝强老师1 小时前
【赵渝强老师】OceanBase的配置文件与配置项
数据库·oceanbase
玖日大大2 小时前
OceanBase SeekDB:AI 原生数据库的技术革命与实践指南
数据库·人工智能·oceanbase
高溪流3 小时前
3.数据库表的基本操作
数据库·mysql