3. Redis 高级特性:事务、Pipeline、Lua脚本、发布订阅与Stream
在削峰与缓存一致性场景中,仅靠基础读写操作无法应对高并发下的原子性、批量吞吐与消息解耦需求。Redis 提供的事务、Pipeline、Lua 脚本、发布订阅及 Stream 五大高级特性,分别从原子执行、批量加速、逻辑下沉、实时通知与可靠消息五个维度补全了缓存系统的能力。本章逐一剖析其原理、适用边界与实战陷阱。
3.1 Redis 事务:乐观锁与原子队列
Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 四个命令实现。与传统关系型数据库不同,Redis 事务不支持回滚,但保证队列中的命令连续、隔离执行。
| 命令 | 作用 | 说明 |
|---|---|---|
| MULTI | 开启事务 | 后续命令进入队列,不立即执行 |
| EXEC | 执行事务 | 原子化执行队列中所有命令 |
| DISCARD | 取消事务 | 清空队列,退出事务状态 |
| WATCH | 乐观锁监视 | 监视 key 变化,若被修改则事务拒绝执行 |
实战要点:WATCH 实现 CAS(Compare And Set)机制。在缓存一致性中,常用于“先比较版本号再更新”的防并发覆盖场景。若 WATCH 的 key 在 EXEC 前被其他客户端修改,EXEC 返回 nil,需重试。
// 使用 WATCH 实现乐观锁扣减库存
WATCH stock:1001
stock = GET stock:1001
if stock > 0
MULTI
DECR stock:1001
EXEC
else
DISCARD
避坑:Redis 事务在命令入队时不会检查语法错误以外的逻辑错误(如对 string 类型执行 LPUSH),错误命令会在 EXEC 时抛出异常,但已成功的命令不会回滚。因此事务内命令应保证类型安全。
3.2 Pipeline 管道:批量网络优化
Pipeline 将多条命令打包一次性发送给 Redis 服务端,再一次性读取所有响应。在削峰场景中,Pipeline 能显著降低 RTT(往返时延)对吞吐的影响。
- 适用场景:批量写入缓存、批量删除缓存键、批量读取不依赖中间结果的数据。
- 不适用场景:命令之间存在前后依赖(如先 SET 再 GET 该值),此时必须使用 Lua 脚本保证原子性。
- 注意事项:Pipeline 打包的命令数不宜过大(建议 50~200 条/批),否则会阻塞客户端或服务端缓冲区。
// Java Jedis Pipeline 示例
Pipeline pipeline = jedis.pipelined();
for (String key : keysToDelete) {
pipeline.del(key);
}
pipeline.sync(); // 一次性发送并接收结果
性能对比:在 1ms 网络延迟下,1000 次独立 SET 耗时约 1000ms;使用 Pipeline 打包 100 条/批,总耗时约 10~15ms,吞吐提升 60~100 倍。
3.3 Lua 脚本:原子逻辑下沉
Redis 内嵌 Lua 5.1 解释器,允许将多个命令封装为一个脚本原子执行。脚本执行期间不会插入其他命令,天然具备事务性,且支持复杂逻辑判断。
| 对比维度 | Redis 事务 | Lua 脚本 |
|---|---|---|
| 原子性 | 是 | 是 |
| 条件分支 | 不支持 | 支持 if/else |
| 中间结果复用 | 不支持 | 支持变量暂存 |
| 回滚 | 不支持 | 不支持 |
| 网络往返 | 多次(入队+执行) | 一次(EVAL) |
-- Lua 脚本:扣减库存并记录扣减日志
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
return 0 -- 库存不足
end
redis.call('DECRBY', KEYS[1], ARGV[1])
redis.call('LPUSH', KEYS[2], ARGV[2]) -- 记录日志
return 1
削峰实战:将“库存扣减 + 限流计数 + 缓存一致性标记”合并为一个 Lua 脚本,一次网络调用完成所有操作,避免多次 RTT 和并发竞态。
3.4 发布订阅(Pub/Sub):实时消息通知
Redis 发布订阅模式基于频道实现:发布者向频道发送消息,所有订阅该频道的消费者实时接收。在缓存一致性中,常用于缓存失效通知和配置动态刷新。
- 核心命令:
PUBLISH channel message、SUBSCRIBE channel、UNSUBSCRIBE channel。 - 特点:消息即发即失,不持久化;消费者离线则丢失消息。
- 适用场景:实时性要求高、允许少量丢失的广播通知(如缓存预热信号、限流阈值变更)。
- 不适用场景:需要可靠投递、消息回溯的场景。
生产避坑:Pub/Sub 的消费者断开重连后,会丢失断开期间的所有消息。在削峰场景中,若依赖 Pub/Sub 做缓存一致性同步,必须配合主动查询兜底,或改用 Redis Stream。
3.5 Redis Stream:持久化消息队列
Redis 5.0 引入的 Stream 数据类型,提供了持久化、消费者组、消息确认(ACK)等完整消息队列能力,是 Pub/Sub 的升级替代方案。
| 特性 | Pub/Sub | Stream |
|---|---|---|
| 消息持久化 | 否 | 是(RDB/AOF) |
| 消费者组 | 不支持 | 支持(XGROUP) |
| 消息确认 | 不支持 | 支持(XACK) |
| 消息回溯 | 不支持 | 支持(XRANGE) |
| 阻塞读取 | 支持 | 支持(XREAD BLOCK) |
# 创建消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM
# 消费者读取未确认消息
XREADGROUP GROUP mygroup consumer1 BLOCK 5000 COUNT 10 STREAMS mystream >
# 处理完成后确认消息
XACK mystream mygroup 1634829200000-0
削峰实战:将秒杀请求先写入 Stream,后端 worker 以消费者组模式异步消费,实现请求削峰 + 可靠处理。配合 PEL(Pending Entries List)实现失败重试,确保缓存一致性操作不丢失。
3.6 特性选型对比
| 特性 | 原子性 | 持久化 | 批量加速 | 消息可靠 | 推荐场景 |
|---|---|---|---|---|---|
| 事务 | ✓ | — | — | — | 乐观锁防并发覆盖 |
| Pipeline | — | — | ✓ | — | 批量读写、缓存预热 |
| Lua 脚本 | ✓ | — | ✓ | — | 复杂原子操作、限流扣减 |
| Pub/Sub | — | — | — | 低 | 实时广播、配置通知 |
| Stream | — | ✓ | — | 高 | 削峰队列、缓存同步 |
架构建议:在缓存一致性实战中,优先组合使用 Lua 脚本(保证原子性)+ Stream(保证消息可靠投递),替代传统的事务 + Pub/Sub 组合,以获得更强的数据一致性保障。