3. Redis 高级特性:事务、Pipeline、Lua脚本与发布订阅

在业务削峰与缓存一致性场景中,仅仅掌握 Redis 的基础数据结构远远不够。Redis 提供的事务、Pipeline、Lua 脚本和发布订阅模式,是构建高性能、高一致性缓存系统的四大核心武器。本章将深入剖析这四种特性的原理、适用场景及实战注意事项。

3.1 Redis 事务:ACID 的折中与实战

Redis 事务通过 MULTI、EXEC、DISCARD、WATCH 命令实现。它提供了一种“将多个命令打包,然后按顺序执行”的机制,但与传统关系型数据库的事务有显著差异。

3.1.1 事务执行流程

  1. 开启事务:客户端发送 MULTI 命令,服务器将客户端状态标记为“事务中”。
  2. 命令入队:后续命令不会立即执行,而是被放入一个队列。
  3. 执行事务:发送 EXEC 命令,服务器原子性地按顺序执行队列中的所有命令。
  4. 取消事务:发送 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 工作原理

Pipeline 管道 vs 普通请求 Client Redis Server 请求1 回复1 请求2 回复2 Client Redis Server 批量发送 批量回复 对比总结 普通请求:N 次请求 = N 次 RTT Pipeline:N 次请求 = 1 次 RTT 适用场景:批量写入、批量读取 注意事项: 1. 非原子性,命令间无事务保证 2. 避免一次性发送过多命令

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 发布订阅架构

Redis 发布订阅模式 Publisher Channel Subscriber 1 Subscriber 2 Subscriber N PUBLISH SUBSCRIBE

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% 以上的业务削峰与缓存一致性挑战。