3. Redis 高级特性:事务、Pipeline、Lua脚本与发布订阅
在业务削峰与缓存一致性场景中,仅仅掌握 Redis 的基础数据结构远远不够。Redis 提供的事务、Pipeline、Lua 脚本和发布订阅模式,是构建高性能、高一致性缓存系统的四大核心武器。本章将深入剖析这四种特性的原理、适用场景及实战注意事项。
3.1 Redis 事务:ACID 的折中与实战
Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 命令实现。它提供了一种“将多个命令打包,然后按顺序执行”的机制,但与传统关系型数据库的事务有显著差异。
3.1.1 事务执行流程
- 开启事务:客户端发送
MULTI命令,服务器将客户端状态标记为“事务中”。 - 命令入队:后续命令不会立即执行,而是被放入一个队列。
- 执行事务:发送
EXEC命令,服务器原子性地按顺序执行队列中的所有命令。 - 取消事务:发送
DISCARD命令,清空事务队列并退出事务状态。
3.1.2 事务的 ACID 特性分析
| 特性 | Redis 支持情况 | 说明 |
|---|---|---|
| 原子性 (Atomicity) | 部分支持 | 事务中的所有命令要么全部执行,要么全部不执行(如执行前发生语法错误)。但运行时错误(如对 string 类型执行 list 操作)不会回滚,已成功的命令不会撤销。 |
| 一致性 (Consistency) | 支持 | 事务执行前后,数据库的完整性约束没有被破坏。 |
| 隔离性 (Isolation) | 支持 | Redis 是单线程模型,事务执行过程中不会被其他客户端的命令打断,天然具有隔离性。 |
| 持久性 (Durability) | 取决于持久化策略 | 如果未开启 AOF 或 AOF 未配置为 always,事务执行结果可能因宕机而丢失。 |
重要提醒: Redis 事务不支持回滚。如果业务逻辑要求“要么全部成功,要么全部失败”,请使用 Lua 脚本替代。
3.1.3 乐观锁:WATCH 命令
WATCH 命令用于实现乐观锁。在事务执行前,监控一个或多个 key。如果在事务执行前(即 EXEC 之前),被监控的 key 被其他客户端修改,则当前事务会被中断(EXEC 返回 nil)。
WATCH stock:1001
stock = GET stock:1001
if stock > 0 then
MULTI
DECR stock:1001
EXEC
else
UNWATCH
end
实战建议: 在秒杀、库存扣减等场景中,使用
WATCH + 事务可以避免超卖,但高并发下重试次数会增多。更推荐使用 Lua 脚本实现原子扣减。
3.2 Pipeline 管道:批量操作与网络优化
Pipeline 允许客户端将多个命令一次性发送到服务器,而不必等待每个命令的回复。服务器处理完所有命令后,再将所有结果一次性返回。这能显著减少网络往返时间(RTT)。
3.2.1 Pipeline 工作原理
3.2.2 Pipeline 使用示例
// Java 示例:使用 Jedis Pipeline 批量写入
Jedis jedis = new Jedis("localhost", 6379);
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipeline.set("key:" + i, "value:" + i);
}
List<Object> results = pipeline.syncAndReturnAll();
3.2.3 Pipeline 与事务的对比
| 特性 | Pipeline | 事务 (MULTI/EXEC) |
|---|---|---|
| 原子性 | 否 | 是(但不支持回滚) |
| 网络优化 | 是,减少 RTT | 是,减少 RTT |
| 隔离性 | 无 | 有,单线程执行 |
| 命令数量 | 建议控制在 5000 以内 | 无严格限制 |
| 典型场景 | 批量数据导入、缓存预热 | 需要原子性的多步操作 |
最佳实践: Pipeline + 事务可以结合使用:
MULTI 开启事务后,通过 Pipeline 批量发送命令,最后 EXEC 执行。这样既获得了网络优化,又保证了原子性。
3.3 Lua 脚本:原子性与复杂逻辑的终极方案
Redis 从 2.6 版本开始支持 Lua 脚本。通过 EVAL 或 EVALSHA 命令,可以将一段 Lua 脚本发送到 Redis 服务器执行。脚本在 Redis 中是原子执行的,执行期间不会被其他命令打断。
3.3.1 Lua 脚本的核心优势
- 原子性:脚本中的所有操作要么全部成功,要么全部失败,不存在部分执行的问题。
- 减少网络开销:将多个命令封装在一个脚本中,只需一次网络往返。
- 复用性:脚本可以被缓存(
SCRIPT LOAD),后续通过 SHA1 值调用,减少带宽消耗。 - 逻辑封装:可以在 Redis 端完成条件判断、循环等复杂逻辑。
3.3.2 秒杀扣库存 Lua 脚本实战
-- 扣减库存脚本
-- KEYS[1]: 库存 key
-- ARGV[1]: 扣减数量
-- 返回值: 1 成功, 0 库存不足
local stock = redis.call('GET', KEYS[1])
if not stock then
return 0
end
if tonumber(stock) < tonumber(ARGV[1]) then
return 0
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
// Java 调用示例
String script = "local stock = redis.call('GET', KEYS[1]) ..."; // 上述脚本
String sha = jedis.scriptLoad(script);
Object result = jedis.evalsha(sha, 1, "stock:1001", "1");
3.3.3 Lua 脚本的注意事项
- 不要使用随机函数:如
time()、random(),会导致主从复制不一致。 - 控制执行时间:Lua 脚本默认有 5 秒的执行超时(可通过
lua-time-limit配置),超时后 Redis 会记录日志但不会主动终止脚本。 - 避免全局变量:脚本中应使用局部变量,避免污染环境。
- 优先使用 EVALSHA:减少网络传输量。
3.4 发布订阅模式:消息实时分发
Redis 的发布订阅(Pub/Sub)模式是一种消息通信模式:发送者(Publisher)发送消息到频道(Channel),订阅者(Subscriber)接收消息。这种模式在缓存一致性场景中,常用于通知其他节点更新本地缓存。
3.4.1 发布订阅架构
3.4.2 核心命令
| 命令 | 功能 | 示例 |
|---|---|---|
PUBLISH channel message |
向指定频道发送消息 | PUBLISH cache:update "user:1001" |
SUBSCRIBE channel [channel ...] |
订阅一个或多个频道 | SUBSCRIBE cache:update |
PSUBSCRIBE pattern [pattern ...] |
订阅匹配模式的频道(支持通配符) | PSUBSCRIBE cache:* |
UNSUBSCRIBE [channel ...] |
取消订阅 | UNSUBSCRIBE cache:update |
3.4.3 缓存一致性场景应用
当某个服务更新了数据库中的数据后,通过发布订阅通知所有相关服务节点,使其主动失效或更新本地缓存。
// 发布者:数据更新后通知
String message = "user:1001";
jedis.publish("cache:update", message);
// 订阅者:监听更新通知,失效本地缓存
JedisPubSub listener = new JedisPubSub() {
public void onMessage(String channel, String message) {
// 失效本地缓存
localCache.invalidate(message);
}
};
jedis.subscribe(listener, "cache:update");
注意事项: Redis 发布订阅模式是“即发即弃”的。如果订阅者不在线,消息会丢失。对于需要可靠投递的场景,请使用 Redis Stream 或专业的消息队列(如 RabbitMQ、Kafka)。
3.5 四种高级特性对比总结
| 特性 | 原子性 | 网络优化 | 逻辑复杂度 | 消息持久化 | 推荐场景 |
|---|---|---|---|---|---|
| 事务 | 部分支持 | 是 | 低 | 否 | 简单多步操作 |
| Pipeline | 否 | 是 | 低 | 否 | 批量数据操作 |
| Lua 脚本 | 是 | 是 | 高 | 否 | 复杂原子逻辑、秒杀 |
| 发布订阅 | 否 | 否 | 低 | 否 | 实时通知、缓存失效 |
学习路径建议: 先掌握 Lua 脚本(解决原子性问题),再结合 Pipeline(解决性能问题),最后用发布订阅(解决分布式通知问题)。这三者组合使用,可以应对 90% 以上的业务削峰与缓存一致性挑战。