1. 管道 (Pipelining)
-
网络延迟 (Round-Trip Time - RTT) 瓶颈。
-
在传统模式下,客户端发送一个命令 -> 等待 Redis 服务器处理并返回结果 -> 再发送下一个命令。如果客户端需要执行大量命令(例如设置或获取多个键),每个命令之间的网络往返时间(RTT)会成为主要的性能瓶颈,即使每个命令本身执行很快。
-
-
原理:
-
客户端一次性打包多个命令发送给 Redis 服务器。
-
Redis 服务器按顺序连续执行收到的所有命令。
-
服务器执行完所有命令后,一次性将所有结果打包返回给客户端。
-
-
关键特性:
-
性能优化: 核心目标是极大减少网络 RTT 次数。将 N 次 RTT 减少为 1 次 RTT(发送请求包 + 接收响应包),显著提升吞吐量,尤其适合批量写入或读取。
-
非原子性: 管道中的命令虽然是一起发送和返回的,但在执行过程中,命令之间并不是原子的。Redis 会按顺序依次执行每个命令。其他客户端的命令可能穿插在管道中某个命令执行的前后。
-
无回滚: 如果管道中某个命令执行出错(例如语法错误、对错误类型操作),它不会影响其他命令的执行,也不会导致整个管道回滚。客户端在收到所有结果后需要自行检查每个命令的返回结果。
-
-
使用方式:
-
客户端库通常提供管道支持(如 Python 的
redis-py
使用pipeline()
方法)。 -
在 Redis CLI 中,可以手动将多个命令写在一行(用分号
;
分隔)或使用echo
和nc
组合。
-
-
适用场景:
-
需要执行大量独立、无依赖关系的命令,且对原子性没有要求。
-
批量设置(
MSET
)、批量获取(MGET
虽然原生支持,但复杂键名时仍需管道)、批量删除(DEL key1 key2 ...
)。 -
数据导入/导出。
-
需要最大化吞吐量的场景(如实时计数器批量更新)。
-
-
优点:
-
大幅提升吞吐量,减少网络延迟影响。
-
使用相对简单。
-
-
缺点:
-
不保证原子性: 命令可能被其他客户端插入。
-
不提供隔离性: 管道执行过程中,其他客户端可以修改数据。
-
错误处理: 需要客户端在收到响应后逐一检查每个命令的结果。
-
2. 事务 (Transactions)
-
简单命令序列的原子性执行需求。
-
确保一组命令在
EXEC
时作为一个整体连续执行(执行原子性),但运行时错误不会回滚已执行的命令。
-
-
核心命令:
-
MULTI
: 标记事务开始。之后的命令不会立即执行,而是被放入一个队列。 -
EXEC
: 执行事务队列中的所有命令。Redis 会按顺序、连续地、原子地执行队列中的所有命令。在执行EXEC
期间,服务器不会被其他客户端的命令打断。 -
DISCARD
: 放弃事务,清空队列。 -
WATCH key [key ...]
: 关键机制! 在MULTI
之前执行。监视一个或多个键。如果在WATCH
之后、EXEC
之前,有任何被监视的键被其他客户端修改,那么当该客户端执行EXEC
时,整个事务将被取消(返回nil
)。这是 Redis 实现 CAS(Compare-and-Set)乐观锁的基础。
-
-
关键特性:
-
原子性:
EXEC
命令触发时,队列中的所有命令作为一个整体执行,不会被其他命令打断。 -
隔离性: 通过
WATCH
实现乐观锁,可检测并发修改,但事务本身无隔离性保证。 -
无回滚: 这是 Redis 事务的一个重要特点! 如果在事务执行过程中(
EXEC
之后)某个命令运行时出错(例如对字符串执行HINCRBY
),只有出错的命令不会生效,而队列中其他命令依然会被执行! Redis 不会回滚已经执行成功的命令。事务的错误通常发生在入队时(命令语法错误,Redis 会拒绝入队)或运行时(数据类型错误)。
-
-
执行流程:
-
WATCH key
(可选,用于乐观锁) -
MULTI
-
发送要执行的命令 (这些命令被放入队列,返回
QUEUED
) -
EXEC
或DISCARD
-
如果执行
EXEC
:-
检查
WATCH
的键是否被修改过?是 -> 放弃执行,返回(nil)
。 -
否 -> 按顺序原子执行所有队列命令,返回所有命令的结果数组。
-
-
-
-
适用场景:
-
需要保证一组命令原子执行的简单场景(例如:转账
A-100; B+100
)。 -
结合
WATCH
实现乐观锁,处理简单的并发竞争(例如:库存扣减、抢票)。
-
-
优点:
-
提供命令序列的原子性保证。
-
WATCH
提供了基本的并发控制手段。
-
-
缺点:
-
运行时错误不回滚: 事务执行中部分命令失败,其他命令仍然生效。需要客户端精心设计命令或依赖
WATCH
重试。 -
无法获取中间结果: 在事务中(
MULTI
之后),无法直接获取之前命令的执行结果来决定后续操作(所有命令在EXEC
时一次性执行)。命令是静态入队的。 -
性能: 虽然
MULTI/EXEC
本身开销不大,但WATCH
失败重试可能导致性能下降。EXEC
执行期间会阻塞其他命令。 -
复杂性: 需要理解
WATCH
的机制和错误处理逻辑。
-
3. Lua 脚本 (Lua Scripting)
-
复杂原子操作、需要中间逻辑判断、事务的局限性。
-
事务无法在命令执行过程中根据中间结果做动态决策。
-
事务的错误回滚行为不符合某些预期。
-
需要执行更复杂的逻辑,这些逻辑无法简单地拆分成一组 Redis 命令序列。
-
-
原理:
-
Redis 内嵌了 Lua 5.1 解释器。
-
客户端将一段 Lua 脚本发送给 Redis 服务器(使用
EVAL
或EVALSHA
)。 -
Redis 服务器在单个线程中原子性地执行整个 Lua 脚本。
-
脚本执行期间,服务器不会执行任何其他命令或脚本(完全隔离)。
-
脚本可以访问和操作 Redis 数据,可以包含复杂的逻辑(条件判断、循环、计算等)。
-
脚本最后返回一个结果给客户端。
-
-
关键特性:
-
原子性: 核心优势! 整个脚本的执行是原子的、隔离的。脚本执行过程中不会有其他命令或脚本执行,脚本看到的数据视图在执行开始时是确定的。
-
灵活性: 可以使用 Lua 语言的全部特性(变量、条件、循环、函数、表等)实现复杂的业务逻辑。
-
可获取中间结果: 脚本内部可以执行多个 Redis 命令(通过
redis.call()
或redis.pcall()
),并能立即获取这些命令的返回值,用于后续的逻辑判断和计算。这是超越事务的关键点。 -
redis.call()
: 执行 Redis 命令,如果命令出错会抛出 Lua 错误,导致整个脚本停止执行(类似事务中的运行时错误)。 -
redis.pcall()
: 执行 Redis 命令,如果命令出错会捕获错误并以 Lua 表的形式返回错误信息,不会中断脚本执行,脚本可以处理这个错误。 -
复用性: 脚本可以被缓存(使用
SCRIPT LOAD
返回 SHA1 摘要),后续通过EVALSHA
用摘要执行,减少网络传输。
-
-
使用方式:
-
EVAL "lua_script" numkeys key [key ...] arg [arg ...]
-
lua_script
: Lua 脚本字符串。 -
numkeys
: 后面跟着的键名的个数。 -
key [key ...]
: 脚本中用到的 Redis 键名,通过KEYS[1]
,KEYS[2]
访问。 -
arg [arg ...]
: 传递给脚本的附加参数,通过ARGV[1]
,ARGV[2]
访问。
-
-
SCRIPT LOAD "lua_script"
: 加载脚本并返回 SHA1 摘要。 -
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
: 使用 SHA1 摘要执行缓存的脚本。 -
SCRIPT EXISTS sha1 [sha1 ...]
: 检查脚本是否已缓存。 -
SCRIPT FLUSH
: 清空脚本缓存。 -
SCRIPT KILL
: 终止当前正在执行的脚本(仅当脚本未执行任何写操作时有效)。
-
-
适用场景:
-
需要真正原子性的复杂操作: 例如:检查库存、扣减库存、记录订单;实现分布式锁(Redlock 或更复杂的锁);实现自定义的原子计数器/限流器。
-
需要基于中间结果做决策: 例如:如果
HGET
的值大于 X,则执行ZADD
和PUBLISH
。 -
封装复杂操作: 将多个 Redis 命令和业务逻辑封装成一个原子操作,简化客户端代码。
-
保证计算与操作原子性: 在脚本中进行计算并基于计算结果更新 Redis。
-
-
优点:
-
真正的原子性和隔离性: 脚本是执行 Redis 命令的最小单元。
-
强大的灵活性: 几乎可以实现任何复杂的原子逻辑。
-
可获取中间结果: 脚本内部可基于之前命令的返回值进行决策。
-
复用性:
EVALSHA
减少网络开销。 -
性能: 脚本在服务器端执行,避免了多次网络 RTT(虽然单次
EVAL
请求可能比单个命令大,但比多次 RTT 好)。脚本执行很快(Redis 单线程高效)。
-
-
缺点/注意事项:
-
脚本编写: 需要学习 Lua 语法和 Redis Lua API (
redis.call
,redis.pcall
)。 -
调试: Redis Lua 脚本调试相对困难。
-
阻塞风险: 非常重要! 一个运行缓慢的 Lua 脚本会阻塞整个 Redis 服务器,导致所有其他客户端请求超时。必须确保脚本是高效的、执行时间可预测的。避免长循环、避免执行大量耗时的命令、避免使用
KEYS
命令。 -
可复制性: Lua 脚本在复制和持久化时有一些特殊规则(通常脚本本身和效果都会被复制)。
-
资源管理: 脚本缓存需要管理(虽然 Redis 会管理,但
SCRIPT FLUSH
需谨慎)。 -
错误处理: 需要理解
redis.call
和redis.pcall
的错误处理差异。脚本中的逻辑错误可能导致整个操作失败(原子性保证)。
-
总结对比表
特性 | 管道 (Pipeline) | 事务 (Transaction) | Lua 脚本 (Lua Scripting) |
---|---|---|---|
核心目标 | 优化性能 (减少 RTT) | 保证隔离性 (命令连续执行) | 原子性执行复杂逻辑 |
发送方式 | ✅ 批量发送 | ❌ 逐条发送(MULTI +命令) | ✅ 一次性发送(EVAL /EVALSHA ) |
原子性 | ❌ 不保证。命令逐个执行,可能被穿插。 | ⚠️ 隔离性而非严格原子性。 EXEC 期间连续执行,但运行时错误不回滚。 | ✅ 强原子性。 整个脚本执行不可中断,脚本内错误会中止执行(已执行效果保留)。 |
性能优化 | ✅ 显著减少 RTT (主要优势) | ⚠️ 批量执行减少部分 RTT,但 WATCH 失败重试会降低效率。 | ✅ 减少复杂逻辑的 RTT,但脚本执行本身可能阻塞服务器。 |
复杂逻辑 | ❌ 只能执行简单命令序列。 | ❌ 只能执行简单命令序列。 | ✅ 支持条件、循环、变量、函数等复杂 Lua 逻辑。 |
错误处理 | 每个命令独立成功/失败。 | ⚠️ 入队错误导致事务放弃;运行时错误仅失败命令,其他执行。 | ⚠️ 脚本或命令错误导致脚本中止,已执行命令效果保留。 |
阻塞性 | 低。服务器按序执行,但命令间可穿插其他客户端命令。 | EXEC 执行期间阻塞其他命令。 | 整个脚本执行期间完全阻塞服务器! |
关键命令 | 客户端实现(一次性发送/接收)。 | MULTI , EXEC , DISCARD , WATCH | EVAL , EVALSHA , SCRIPT LOAD , SCRIPT FLUSH |
典型场景 | 大批量独立命令操作(无原子性要求)。 | 需要连续执行且不被干扰的命令组 + 乐观锁 (WATCH )。 | 需要原子性执行的复杂业务逻辑(如分布式锁、限流)。 |
如何选择?
-
需要高性能批量读写,且命令独立、无原子性要求? -> 管道。
-
需要保证几个简单命令要么都成功要么都不成功,并且并发竞争不激烈或可以通过
WATCH
重试解决? -> 事务。 -
需要实现复杂的业务逻辑、需要基于中间结果做决策、需要真正的原子性保证(即使包含逻辑判断和循环)、或者事务的运行时错误不回滚特性不符合需求? -> Lua 脚本 (务必保证脚本高效!)。
-
需要结合使用? 很常见!例如,使用管道发送多个
EVAL
或EVALSHA
命令来批量执行多个(独立的)原子操作。
重要补充:Redis 7 函数 (Functions)
Redis 7 引入了 Redis Functions,这是对 Lua 脚本的进一步封装和增强。它允许你将 Lua 脚本注册为命名的函数(FUNCTION LOAD
),然后像调用内置命令一样调用它们(FCALL
, FCALL_RO
)。这提供了更好的代码组织、复用性、访问控制和调试支持(通过 FUNCTION
命令族管理)。函数在底层仍然是 Lua 脚本执行,继承了其原子性、隔离性、阻塞风险等核心特性,但提供了更优雅和安全的使用方式。对于新项目,特别是需要复用复杂逻辑的场景,推荐优先考虑 Redis Functions。