3. Redis 高级特性:事务、Pipeline、Lua 脚本与发布订阅
在构建高并发、高可用的缓存系统时,仅掌握基础的数据结构操作远远不够。Redis 提供的事务、Pipeline、Lua 脚本和发布订阅四大高级特性,分别解决了原子性、批量性能、复杂逻辑编排和消息通知等核心问题。本章将逐一剖析其原理、适用场景及实战中的“坑”。
3.1 Redis 事务:弱隔离与原子性保障
Redis 事务通过 MULTI、EXEC、DISCARD 和 WATCH 命令实现。与关系型数据库不同,Redis 事务不支持回滚,且不保证严格的隔离性(ACID 中的 I)。
3.1.1 事务执行流程
- 开启事务:客户端发送
MULTI命令,进入事务队列模式。 - 命令入队:后续所有命令不会立即执行,而是被放入一个队列。
- 执行事务:发送
EXEC,Redis 按顺序串行执行队列中的所有命令。 - 取消事务:发送
DISCARD,清空队列并退出事务状态。
EXEC 时一次性、顺序执行,期间不会被其他客户端的命令插入(弱隔离)。但如果在入队阶段发生语法错误,整个事务会被拒绝;如果在执行阶段发生运行时错误(如对 string 类型执行 list 操作),错误命令会失败,但后续命令仍会继续执行,不会回滚。
3.1.2 乐观锁:WATCH 命令
WATCH 用于实现 CAS(Check-And-Set)乐观锁。在事务执行前监视一个或多个 key,如果 EXEC 时这些 key 的值被其他客户端修改过,则事务被中断(返回 nil)。
-- 伪代码:库存扣减
WATCH stock:1001
stock = GET stock:1001
if stock > 0 then
MULTI
DECR stock:1001
EXEC
else
UNWATCH
return "库存不足"
end
| 特性 | Redis 事务 | 关系型数据库事务 |
|---|---|---|
| 原子性 | 部分支持(无回滚) | 完全支持(可回滚) |
| 隔离性 | 弱隔离(串行执行) | 强隔离(MVCC/锁) |
| 持久性 | 依赖 AOF/RDB | 依赖 WAL 日志 |
| 适用场景 | 简单原子操作、乐观锁 | 复杂业务逻辑、强一致性 |
3.2 Pipeline:批量请求的性能利器
Pipeline 允许客户端将多条命令一次性发送到 Redis 服务器,减少网络往返时间(RTT)。它不保证原子性,但能显著提升批量操作的吞吐量。
3.2.1 工作原理
普通模式下,每条命令都需要一次网络请求和一次响应。Pipeline 将多条命令打包发送,Redis 处理完后一次性返回所有结果。
3.2.2 使用注意事项
- 非原子性:Pipeline 中的命令可能部分成功、部分失败,需要业务层处理。
- 内存压力:客户端需要缓存所有响应结果,命令数量过多时可能导致客户端 OOM。
- 建议批次大小:单次 Pipeline 建议控制在 100~1000 条命令,避免阻塞其他客户端。
3.3 Lua 脚本:原子性与复杂逻辑的完美结合
Redis 从 2.6 版本开始支持内嵌 Lua 脚本。脚本在 Redis 服务器端原子性执行,且可以包含分支、循环等复杂逻辑,是替代事务和 Pipeline 的“终极武器”。
3.3.1 核心命令
| 命令 | 说明 | 示例 |
|---|---|---|
EVAL |
执行 Lua 脚本 | EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 name alice |
EVALSHA |
执行已缓存的脚本(通过 SHA1 摘要) | EVALSHA "a1b2c3..." 1 name alice |
SCRIPT LOAD |
将脚本缓存到 Redis,返回 SHA1 | SCRIPT LOAD "return redis.call('GET', KEYS[1])" |
SCRIPT EXISTS |
检查脚本是否已缓存 | SCRIPT EXISTS "a1b2c3..." |
3.3.2 典型应用:原子限流
以下 Lua 脚本实现了一个滑动窗口限流器,在 Redis 中原子性完成计数、过期和阈值判断:
-- 限流脚本:滑动窗口计数器
-- KEYS[1]: 限流 key(如 user:123:rate)
-- ARGV[1]: 窗口大小(毫秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 移除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < max then
-- 添加当前请求
redis.call('ZADD', key, now, now)
-- 设置过期时间
redis.call('PEXPIRE', key, window)
return 1 -- 允许通过
else
return 0 -- 限流
end
KEYS、SMEMBERS 大集合)。建议单个脚本执行时间控制在 10 毫秒以内。
3.4 发布订阅:轻量级消息通知
Redis 发布订阅(Pub/Sub)是一种消息传递模式,发布者将消息发送到频道,订阅者接收该频道的所有消息。它不依赖磁盘存储,消息即发即失,适合实时通知场景。
3.4.1 核心命令
PUBLISH channel message:向指定频道发送消息。SUBSCRIBE channel [channel ...]:订阅一个或多个频道。PSUBSCRIBE pattern:订阅匹配模式的频道(如news.*)。UNSUBSCRIBE [channel]:取消订阅。
3.4.2 架构与局限
3.4.3 与专业消息队列的对比
| 特性 | Redis Pub/Sub | RabbitMQ / Kafka |
|---|---|---|
| 消息持久化 | 不支持 | 支持(磁盘存储) |
| 消息确认 | 无(发后即忘) | 支持 ACK 机制 |
| 消费者组 | 不支持 | 支持(负载均衡) |
| 适用场景 | 实时广播、轻量通知 | 可靠投递、异步任务 |
3.5 四大特性选型指南
| 需求 | 推荐方案 | 原因 |
|---|---|---|
| 简单原子操作(如 INCR、SETNX) | Redis 单命令 | 天然原子,无需额外开销 |
| 批量读取/写入,不要求原子性 | Pipeline | 减少 RTT,吞吐量最高 |
| 多步操作需要原子性(如扣库存+记录日志) | Lua 脚本 | 原子执行,支持复杂逻辑 |
| 乐观锁场景(如秒杀) | WATCH + 事务 | 轻量 CAS,避免锁开销 |
| 实时消息广播(如配置变更通知) | Pub/Sub | 低延迟,一对多推送 |
在实际项目中,Lua 脚本 + Pipeline 是最常用的组合:用 Pipeline 批量获取数据,用 Lua 脚本保证关键业务的原子性。而 Pub/Sub 则作为辅助手段,用于跨实例的缓存失效通知和配置动态更新。