为什么 Redis 的 Pipeline 不是原子的,而 Lua 脚本却是?——从事件循环讲透原子性本质

Pipeline 批量发命令,Lua 脚本也能批量操作,但只有 Lua 是原子的。
为什么?答案藏在 Redis 的单线程心脏里。

在使用 Redis 时,你可能遇到过这样的困惑:

  • 我用 Pipeline 一口气发了 100 个命令,效率很高;
  • 但我听说 Pipeline 不是原子的,中间可能被其他客户端插队;
  • 而写个 Lua 脚本,哪怕包含 10 条 Redis 命令,却能保证完全原子执行

这到底是为什么?难道 Redis 对 Lua 有"特殊照顾"?

今天,我们就从 Redis 内部执行模型 出发,彻底讲清楚:Pipeline 和 Lua 脚本在 Redis 眼中,根本就不是一类东西


一、Pipeline:只是"网络打包",不是"执行合并"

很多人误以为 Pipeline 是一种"批量命令",其实它根本不是 Redis 的功能 ,而是客户端的优化技巧

客户端视角:

dart 复制代码
// 普通方式:3 次网络往返
redis.set("a", "1");
redis.set("b", "2");
redis.set("c", "3");

// Pipeline:1 次网络往返
pipeline.set("a", "1");
pipeline.set("b", "2");
pipeline.set("c", "3");
pipeline.sync();

看起来很高效,对吧?但关键在于:Redis 并不知道你在用 Pipeline

Redis 服务端视角:

当 Redis 收到 TCP 流中的数据:

css 复制代码
SET a 1\r\nSET b 2\r\nSET c 3\r\n

它会逐行解析 ,然后依次执行三个独立的 SET 命令。

也就是说,在 Redis 内部,这三个命令和你分开三次发送没有任何区别 ------它们会被一个一个放进命令队列,再一个一个执行

问题来了:命令之间会发生什么?

Redis 的主循环大致如下(简化版):

scss 复制代码
while (1) {
    // 1. 处理网络 I/O(读取所有客户端数据)
    aeProcessEvents();

    // 2. 执行命令队列中的命令
    while (hasCommand()) {
        executeCommand(dequeueCommand());
        // ← 执行完一条命令后,回到循环顶部!
    }
}

注意:每执行完一条命令,Redis 就会回到事件循环顶部,去处理:

  • 其他客户端的新请求;
  • 定时任务(如 key 过期);
  • 主从同步等。

所以,你的 SET a 1SET b 2 之间,完全可能插入别人发来的 DEL x

Pipeline 只省了网络 RTT,没改变命令的执行语义。


二、Lua 脚本:Redis 的"独占模式"

相比之下,Lua 脚本在 Redis 中享有"VIP 待遇"。

当你执行:

less 复制代码
EVAL "redis.call('SET', KEYS[1], '1'); redis.call('SET', KEYS[2], '2')" 2 a b

Redis 不会把它拆成两个 SET 命令。而是:

  1. 整个脚本被视为一个不可分割的单元
  2. 执行期间,Redis 会暂停处理任何新命令
  3. 脚本内部的所有 redis.call() 都在当前上下文中直接执行,不经过命令队列

技术实现:lua_caller 标志位

在 Redis 源码中,有一个关键变量:

arduino 复制代码
struct redisServer {
    client *lua_caller;  // 当前正在执行 Lua 脚本的客户端
};

在事件循环的关键位置(如 beforeSleepprocessCommand),Redis 会检查:

arduino 复制代码
if (server.lua_caller != NULL) {
    // 正在执行 Lua 脚本,跳过处理新命令!
    return;
}

这意味着:只要 Lua 脚本没跑完,其他客户端的命令一律排队等待

Lua 脚本 = 占用 Redis 主线程的"临界区"


三、一张图看懂本质区别

css 复制代码
普通命令 / Pipeline 命令:
[SET a 1] → 执行 → 回到事件循环 → [其他客户端命令] → [SET b 2] → 执行

Lua 脚本:
[Lua 开始] → 执行 SET a 1 → 执行 SET b 2 → [Lua 结束] → 其他命令才能进来

Pipeline 的命令是"散装"的,Lua 脚本是"封装好的原子包"。


四、常见误区澄清

误区 正解
"Pipeline 是事务" ❌ Pipeline 没有任何事务语义,只是网络优化
"Lua 脚本快是因为本地执行" ❌ Lua 在 Redis 服务端执行,快是因为原子+无网络开销
"Cluster 下 Pipeline 也能跨节点原子" ❌ Cluster 下 Pipeline 必须同 slot,且仍非原子

五、如何选择?

场景 推荐方案
需要原子性(如扣款、限流) ✅ Lua 脚本
只需提升吞吐(如批量导入) ✅ Pipeline
简单批量读写(同 key) MGET / MSET(内置原子命令)

💡 记住:
要速度 → Pipeline;
要一致 → Lua。


结语

Redis 的简洁之美,正体现在它的单线程模型 上。

Pipeline 和 Lua 脚本看似都能"批量操作",但一个只是网络层的巧思 ,另一个则是执行层的保障

相关推荐
古城小栈2 小时前
Rust 的 redis-rs 库
开发语言·redis·rust
Codeking__4 小时前
Redis的value类型介绍——list
数据库·redis·缓存
難釋懷4 小时前
Redis简单介绍
数据库·redis·缓存
ChineHe5 小时前
Redis数据类型篇003_详解Lists列表类型及其命令
数据库·redis·缓存
自燃人~5 小时前
实战都通用的 Watchdog 原理说明
redis·面试
雪域迷影5 小时前
Python中连接Redis数据库并存储数据
redis·python
Codeking__5 小时前
Redis的value类型及编码方式介绍——string
数据库·redis·缓存
win x6 小时前
Redis集群
java·数据库·redis
oMcLin7 小时前
如何在 Debian 10 上通过配置 Redis 集群的持久化选项,提升高可用性缓存系统的容错性与性能?
redis·缓存·debian