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

相关推荐
仍然.24 分钟前
MYSQL--约束
数据库·mysql
乡野码圣1 小时前
【RK3588 Android12】RCU机制
java·jvm·数据库
亓才孓1 小时前
[数据库]应该注意的细节
数据库·sql
m0_561359672 小时前
掌握Python魔法方法(Magic Methods)
jvm·数据库·python
xxxmine2 小时前
redis学习
数据库·redis·学习
qq_5470261793 小时前
Redis 常见问题
数据库·redis·mybatis
APIshop3 小时前
Java 实战:调用 item_search_tmall 按关键词搜索天猫商品
java·开发语言·数据库
小陈phd3 小时前
混合知识库搭建:本地Docker部署Neo4j图数据库与Milvus向量库
数据库·docker·neo4j
2401_838472513 小时前
使用Python进行图像识别:CNN卷积神经网络实战
jvm·数据库·python
知识即是力量ol3 小时前
基于 Redis 实现白名单,黑名单机制详解及应用场景
数据库·redis·缓存