Redis 事务和管道

一、Redis 事务

1. 定义

  • Redis 事务是一组命令的集合,通过 MULTI 开启事务后,后续命令会进入「事务队列」,直到执行 EXEC 才一次性串行执行队列中所有命令;执行过程中不会插入其他客户端的命令,核心目标是保证命令执行的「串行原子性」(但非结果原子性)。

2. 与传统数据库事务的对比

  • 对比
    image

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)区别

image

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)区别

image

2)管道 + 事务结合使用
  • 将 MULTI/EXEC 放入管道中,既减少网络 RTT,又保证事务的串行执行(原子性增强)

(3)管道和 Lua 脚本的区别?如何选型?

1)区别

image

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 监控,避免缓冲区异常