Redis 事务和管道
一、Redis 事务
1. 定义
- Redis 事务是一组命令的集合,通过 MULTI 开启事务后,后续命令会进入「事务队列」,直到执行 EXEC 才一次性串行执行队列中所有命令;执行过程中不会插入其他客户端的命令,核心目标是保证命令执行的「串行原子性」(但非结果原子性)。
2. 与传统数据库事务的对比
- 对比

3. 常用操作
(1)MULTI
- 开启事务,后续命令进入事务队列(仅入队,不执行)
- 时间复杂度:O(1)
(2)EXEC
- 执行事务队列中所有命令,返回各命令执行结果;若 WATCH 监控的键被修改,返回 nil
- EXEC 执行后,之前的监控锁会自动取消
(3)DISCARD
- 放弃事务,清空队列,退出事务状态
- 时间复杂度:O(1)
(4)WATCH key1…
- 监控一个/多个键,事务执行前若键被修改,事务取消(乐观锁核心,类似 CAS,check-and-set);断开连接,也会被取消监控
- 时间复杂度:O(1)
- 乐观锁:每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新时会去判断一下别人有没有去更新这个数据
(5)UNWATCH
- 取消所有键的监控,退出乐观锁状态
- 时间复杂度:O(1)
4. 操作示例
# 会话1
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set user:1001 name zhangsan
QUEUED
127.0.0.1:6379(TX)> hset user:1001 age 20
QUEUED
127.0.0.1:6379(TX)> HGET user:1001 age
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR syntax error
2) (integer) 1
3) "20"
127.0.0.1:6379> watch stock:phone
OK
127.0.0.1:6379> get stock:phone
(nil)
127.0.0.1:6379> UNWATCH stock:phone
(error) ERR wrong number of arguments for 'unwatch' command
127.0.0.1:6379> UNWATCH
OK
127.0.0.1:6379> set stock:phone 10
OK
127.0.0.1:6379> WATCH stock:phone
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> DECR stock:phone
QUEUED
# 会话2
127.0.0.1:6379> INCR stock:phone
(integer) 11
# 会话1
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> GETRANGE stock:phone 0 -1
"11"
5. 业务场景
(1)原子性批量操作
- 需保证多个命令串行执行(无其他命令插入),如「扣减用户积分 + 增加订单数」,避免中间状态被其他请求修改
(2)乐观锁控制并发
- 通过 WATCH 监控核心键(如库存、余额),防止并发操作导致数据不一致(如秒杀场景扣减库存)
(3)低一致性要求的批量写
- 如批量更新多个缓存键,无需回滚,即使某命令失败,其他命令执行结果仍保留
6. 注意事项
- 无自动回滚:事务中某命令语法正确但执行失败(如对 String 执行 HSET),后续命令仍会执行,需业务层自行处理失败逻辑
- WATCH 时效性:WATCH 仅在 EXEC 前有效,事务执行后自动失效,若需再次监控需重新执行 WATCH
- 阻塞风险:事务队列中命令过多(如上千条),EXEC 执行时会阻塞主线程,建议拆分小事务
- 不支持嵌套:事务中执行 MULTI 会报错,EXEC/DISCARD/UNWATCH 会直接终止当前事务状态
- 过期键处理:WATCH 监控已过期的键,若事务执行前键被自动删除,视为「键被修改」,事务取消
7. 面试题
(1)Redis 事务为什么不支持回滚?官方设计思路是什么?
- Redis 设计的核心原则是「极简、高性能」
- 性能层面:实现回滚需记录每个命令的逆操作(如 SET 对应 DEL、INCR 对应 DECR),增加内存和执行开销
- 场景层面:Redis 命令执行失败多为「编程错误」(如类型不匹配),应在开发阶段规避,而非运行时回滚
- 结果层面:Redis 认为事务失败多是业务逻辑问题,回滚无法解决本质问题,需业务层自行处理
(2)Redis 事务如何实现乐观锁?适用场景和局限性?
1)实现方式
- 通过 WATCH key 监控核心键 → 开启事务(MULTI)→ 执行命令入队 → EXEC 执行;若期间监控的键被其他客户端修改,EXEC 返回 nil,事务取消
2)适用场景
- 低并发、短事务的并发控制(如秒杀库存扣减、余额转账)
3)局限性
- 高并发下易触发事务失败(需业务层重试)
- 仅支持键级监控,无法监控字段(如 Hash 的某个 field)
- WATCH 会增加内存开销,监控大量键时性能下降
(3)Redis 事务和 Lua 脚本的区别?如何选型?
1)区别

2)选型原则
- 简单批量串行操作(如批量 SET/GET):用事务,成本低
- 复杂逻辑/强原子性要求(如分布式锁解锁、多条件判断):用 Lua 脚本(8.4 推荐)
- 高并发场景:优先 Lua 脚本,避免 WATCH 导致的事务失败
(4)Redis 事务执行过程中,哪些情况会导致部分命令执行失败?如何处理?
1)入队时失败(MULTI 后、EXEC 前)
# 部分语法错误并没有导致入队失败
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> exec
1) (error) ERR syntax error
2) OK
127.0.0.1:6379> keys *
1) "k1"
# 仅 命令不存在 等基础错误会入队失败
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> seterror k5 v5
(error) ERR unknown command 'seterror', with args beginning with: 'k5' 'v5'
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
- 如对 String 执行 HSET,命令无法入队,EXEC 直接放弃所有命令执行(8.4 优化了入队失败的错误提示)
- 处理:开发阶段校验命令语法和键类型,避免入队失败
- 扩展:并非所有语法错误都会在入队阶段拦截,仅「命令不存在」等基础错误会入队失败,「参数非法」类语法错误需到 EXEC 执行阶段才会暴露
2)执行时失败(EXEC 执行时)
- 如对非数字 String 执行 INCR,该命令失败,后续命令仍执行
- 处理:业务层遍历 EXEC 返回结果,检测失败命令,手动执行补偿逻辑(如回滚已执行的命令)
(5)Redis 事务和主从复制的兼容性问题?
1)问题
- 事务执行时,主节点会将 MULTI/EXEC 及队列中的命令作为整体同步到从节点
- 若主节点执行事务过程中宕机,可能出现「主从数据不一致」:主节点仅执行了部分命令,从节点未同步完整事务
- 8.4 优化:主节点执行 EXEC 时,先将事务命令写入 AOF,再执行,从节点通过 AOF 同步,减少不一致概率
2)解决方案
- 开启 Redis 持久化(混合 AOF),主从同步开启 repl-diskless-sync no,保证事务命令完整同步
(6)为什么 Redis 事务不支持隔离级别?
- Redis 是单线程执行命令,事务执行期间,其他客户端的命令会被放入队列等待,无法插入到事务执行过程中,天然满足「串行化隔离级别」,因此无需设计多级别隔离
- 而传统数据库是多线程执行,需通过隔离级别解决脏读、幻读等问题,与 Redis 单线程模型本质不同
二、Redis 管道
1. 定义
- Redis 管道(Pipeline)是批量命令执行机制:允许客户端将多个命令一次性打包发送到 Redis 服务器,服务器按顺序执行所有命令后,一次性返回所有结果;核心价值是减少客户端与服务器之间的网络往返次数(RTT),解决高频小命令因网络延迟导致的性能瓶颈。
2. 常用操作(核心价值)
- 网络开销:从「N 条命令 N 次 RTT」降为「N 条命令 1 次 RTT」,是性能提升的核心
3. 操作示例
# 开启管道交互模式(--pipe)
cat > cmd.txt << EOF
SET k1 v1
GET k1
HSET user:1001 name zhangsan age 20
HGETALL user:1001
EOF
# 发送管道命令
cat cmd.txt | redis-cli --pipe
# 输出结果(批量返回各命令执行结果)
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 4
4. 操作方式
(1)sync()
(2)async()
(3)syncAndReturnAll()
5. 业务场景
(1)批量数据写入/读取
- 如批量导入商品缓存(1000 个商品 SET 命令)、批量查询用户信息(MGET 替代方案,或混合 Hash 操作),减少网络往返耗时
(2)缓存预热
- 系统启动时,通过管道批量加载热点数据到 Redis,提升预热效率
(3)数据迁移/同步
- 从其他存储(如 MySQL)同步数据到 Redis 时,批量封装命令通过管道发送,降低同步耗时
(4)高频小命令批量执行
- 如秒杀场景中批量扣减库存、批量更新用户积分,避免单命令高频发送导致的网络瓶颈
(5)日志/埋点数据上报
- 批量上报用户行为埋点(如 PV/UV 统计),无需实时获取结果,用异步管道提升吞吐量
6. 注意事项
- 原子性问题:管道仅保证「命令按顺序执行」,但不保证原子性 —— 若执行中某命令失败(如类型不匹配),后续命令仍会执行(与事务类似);需原子性则结合 MULTI/EXEC(管道 + 事务),或用 Lua 脚本
- 命令数量限制:单次管道命令数不宜过多(建议≤1000),8.4 虽优化了内存,但过多命令会导致:① 客户端内存占用过高(缓存大量命令);② 服务器主线程阻塞(批量执行耗时久);③ 网络包过大导致分片传输,反而降低效率
- 与批量命令的区别:MSET/MGET 等原生批量命令是「单命令处理多键」,管道是「多命令批量发送」;原生批量命令原子性更强,管道更灵活(支持不同类型命令混合)
- 异步执行风险:async() 异步执行时,Redis 服务器若宕机,客户端无法感知命令是否执行成功,需结合持久化或重试机制
- 8.4 特殊优化适配:8.4 对管道命令的解析做了分段处理,大管道可拆分为小批次解析,减少主线程阻塞,但仍需控制单次命令数
- 不支持事务级 WATCH:管道中若加 WATCH 监控键,需将 WATCH/MULTI/EXEC 都放入管道,单独 WATCH 无意义(网络往返会导致监控失效)
7. 面试题
(1)Redis 管道提升性能的核心原理?与单命令执行的性能差异?
1)核心原理
- 减少客户端与服务器的网络往返次数(RTT)—— 单命令执行时,每条命令需经历「客户端发送→服务器接收→执行→返回结果」的完整网络往返,N 条命令需 N 次 RTT;管道将 N 条命令打包为 1 次发送/1 次返回,仅 1 次 RTT,大幅降低网络延迟占比(尤其跨机房/远距离部署场景)
2)性能差异
- 网络延迟越高(如跨地域部署,RTT=100ms),管道性能提升越明显(1000 条命令从 100*1000=100s 降为 0.1s + 命令执行时间);本地部署(RTT≈0)提升有限,主要节省网络交互开销
(2)管道和事务(MULTI/EXEC)的区别?管道 + 事务如何结合使用?
1)区别

2)管道 + 事务结合使用
- 将 MULTI/EXEC 放入管道中,既减少网络 RTT,又保证事务的串行执行(原子性增强)
(3)管道和 Lua 脚本的区别?如何选型?
1)区别

2)选型原则
- 简单批量操作(如多 SET/GET):用管道(成本低、易编写)
- 复杂逻辑/强原子性要求(如扣库存 + 校验 + 记录日志):用 Lua 脚本
- 8.4 场景:大批次简单命令用管道,小批次复杂逻辑用 Lua 脚本
(4)为什么管道命令数不宜过多?8.4 做了哪些优化?
1)过多命令的问题
- 客户端:缓存大量命令占用内存,易 OOM
- 服务器:一次性接收大量命令需占用内存解析,主线程批量执行耗时久,阻塞其他请求
- 网络:超大数据包会被 TCP 分片,导致传输效率下降、丢包风险升高
2)8.4 优化
- 管道命令分段解析:将大管道拆分为小批次解析,避免单次占用过多内存
- 优化命令解析算法:提升批量命令的解析速度,减少主线程阻塞时间
- 内存复用:减少管道执行过程中的临时内存分配,降低内存碎片
(5)管道和原生批量命令(如 MSET/MGET)哪个性能更好?
1)原生批量命令(MSET/MGET)性能略优
- 服务器端:原生批量命令是「单命令处理多键」,解析开销更低;管道是「多命令批量解析」,需逐个解析命令
- 原子性:原生批量命令天然原子性,管道无
2)管道的优势
- 灵活性:支持不同类型命令混合(如 SET+HSET+ZADD),原生批量命令仅支持同类型
- 适用范围:原生批量命令仅覆盖部分场景(如 String 的 MSET/MGET),管道支持所有命令
(6)异步管道(async())的使用风险?如何规避?
1)风险
- 无返回结果,无法感知命令是否执行成功(如服务器宕机、命令语法错误)
- 批量发送过快可能导致 Redis 输入缓冲区溢出,触发客户端连接关闭
2)规避方案
- 控制异步管道的发送速率(如每秒≤1w 命令),避免缓冲区溢出
- 结合 Redis 持久化(AOF),保证命令落盘
- 定期校验数据(如抽样查询),发现缺失则重试
- 8.4 可开启 client-output-buffer-limit 监控,避免缓冲区异常