简单地说,事务表示一组动作,要么全部执行,要么全部不执行。Redis事务是一组命令的集合,通过MULTI
、EXEC
等命令实现批量操作的原子性执行。
Redis事务的特点
其核心特点包括:
- 顺序性:命令按入队顺序执行
- 隔离性:单线程模型保证事务执行不被打断
- 弱原子性:不支持回滚机制(运行时错误不会中断后续命令)
- 乐观锁支持 :通过
WATCH
命令监控键值变化
注意:Redis事务不同于传统数据库事务,它更类似于命令批处理机制。
Redis事务的本质
命令队列机制
通过三个阶段实现事务:
- 开启事务 :
MULTI
命令创建命令队列 - 命令入队:将操作指令存入内存队列(不执行语法检查)
- 执行事务 :
EXEC
触发队列命令的FIFO顺序执行
单线程保障
Redis主线程依次执行事务队列中的命令,保证:
- 执行期间不会被其他客户端请求打断
- 天然实现隔离性(串行化执行)
Redis事务命令使用
有关事务类型的命令可以通过help @transactions
命令来查看。有关命令的使用可以通过help 命令
来查看,例如help exec
。
shell
127.0.0.1:6379> help @transactions
DISCARD -
summary: Discard all commands issued after MULTI
since: 2.0.0
EXEC -
summary: Execute all commands issued after MULTI
since: 1.2.0
MULTI -
summary: Mark the start of a transaction block
since: 1.2.0
UNWATCH -
summary: Forget about all watched keys
since: 2.2.0
WATCH key [key ...]
summary: Watch the given keys to determine execution of the MULTI/EXEC block
since: 2.2.0
基础命令演示
事务提交成功的演示:
shell
# 开启事务
127.0.0.1:6379> multi
OK
# 命令进入队列,并没有直接执行
127.0.0.1:6379(TX)> set xxoo ooxx
QUEUED
# 命令进入队列,并没有直接执行
127.0.0.1:6379(TX)> incr count
QUEUED
# 执行事务
127.0.0.1:6379(TX)> exec
1) OK
2) (integer) 1
事务取消的演示:
shell
# 开启事务
127.0.0.1:6379> multi
OK
# 命令进入队列,并没有直接执行
127.0.0.1:6379(TX)> set ooxx xxoo
QUEUED
# 命令进入队列,并没有直接执行
127.0.0.1:6379(TX)> incr count
QUEUED
# 取消事务
127.0.0.1:6379(TX)> discard
OK
高级用法:乐观锁
乐观锁获取锁成功的演示:
shell
127.0.0.1:6379> set balance:Morris 1000
OK
# 监视账户余额,类似cas
127.0.0.1:6379> watch balance:Morris
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby balance:Morris 100
QUEUED
127.0.0.1:6379(TX)> incrby balance:Bob 100
QUEUED
127.0.0.1:6379(TX)> exec
1) (integer) 900
2) (integer) 100
乐观锁获取锁失败的演示:
bash
127.0.0.1:6379> set balance:Morris 1000
OK
127.0.0.1:6379> watch balance:Morris
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> decrby balance:Morris 100
QUEUED
127.0.0.1:6379(TX)> incrby balance:Bob 100
QUEUED
# 此时balance:Morris被其他客户端修改,事务失败返回(nil)
127.0.0.1:6379(TX)> exec
(nil)
原子性缺陷演示
Redis在事务执行时,如果在命令入队时就有语法错误,比如命令不存在或者参数错误,那么整个事务会被拒绝执行,这时候是原子性的。但如果在执行过程中出现运行时错误,比如对字符串进行INCR操作,这时候错误命令之后的命令还会继续执行,这样原子性就被破坏了。
语法错误导致全体失败
shell
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
# 命令不存在,语法错误
127.0.0.1:6379(TX)> xxx k1
(error) ERR unknown command `xxx`, with args beginning with: `k1`,
127.0.0.1:6379(TX)> exec
(error) EXECABORT Transaction discarded because of previous errors.
原子性表现:若存在入队时即可检测的语法错误(如命令不存在、参数错误),整个事务会被拒绝执行,此时具有原子性。
运行时错误部分成功
shell
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
# 类型错误(对字符串执行SADD)
127.0.0.1:6379(TX)> sadd k1 oo xx
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
原子性缺陷:事务中运行时错误(如数据类型不匹配)不会影响后续命令执行,结果中前两条命令生效,第三条失败,导致部分成功
与传统数据库事务对比
特性 | Redis事务 | MySQL事务 |
---|---|---|
原子性 | 部分支持(无回滚机制) | 完全支持(UNDO日志回滚) |
隔离性 | 天然隔离(单线程模型) | 通过锁机制实现多隔离级别 |
持久性 | 依赖AOF配置 | 默认保证(redo日志) |
一致性 | 需配合WATCH实现 | 外键/约束自动保障 |
执行时机 | 命令延迟执行 | 实时执行 |
Pipeline和事务的区别
服务端行为VS客户端行为
-
事务 :是Redis服务端原生支持的原子操作机制,通过
MULTI
开启事务模式后,服务端会将后续命令存入队列缓存,直到执行EXEC
时才一次性按顺序执行所有命令。 -
Pipeline:本质是客户端批量发送命令的优化策略,服务端无法区分普通命令与Pipeline命令,仅作为连续的命令请求处理。
原子性保障
-
事务 :提供弱原子性 保证,事务队列中的命令在
EXEC
执行时会作为整体串行执行,但运行时错误(如类型操作错误)不会中断后续命令 -
Pipeline:完全不具备原子性,命令可能被其他客户端的操作打断,仅在命令量少(能被内核缓冲区容纳)时可能保持原子性
执行流程对比
特性 | 事务 | Pipeline |
---|---|---|
命令执行时机 | EXEC触发批量执行 | 立即发送到服务端 |
网络交互次数 | MULTI+命令+EXEC共3次往返 | 单次批量发送所有命令 |
错误处理 | 语法错误导致全体失败,运行时错误部分执行 | 无特殊处理,按普通命令逐条响应 |
阻塞风险 | 长事务可能阻塞其他请求 | 无服务端阻塞风险 |
应用场景与优化策略
适用场景
-
事务 :需要简单原子性操作的场景(如库存扣减、转账),配合
WATCH
实现乐观锁控制 -
Pipeline:批量非原子性操作(如日志写入、数据预热),减少高并发场景下的网络开销
组合优化方案
可将事务与Pipeline结合,减少事务命令的网络传输时间:
python
# 将MULTI/EXEC封装在Pipeline中
pipe = redis.pipeline(transaction=True)
pipe.multi()
pipe.set('a', 1)
pipe.incr('b')
pipe.execute() # 单次网络请求完成事务
特殊注意事项
事务的局限性:
- 不支持回滚机制,需通过
WATCH
+重试策略实现数据一致性 - 事务内不能执行阻塞命令(如
BLPOP
),否则会退化为普通命令
Pipeline的风险点:
- 大流量Pipeline可能触发服务端缓冲区溢出(需配置
client-output-buffer-limit
) - Cluster模式下需确保所有命令指向同一节点
选型建议
指标 | 选择事务 | 选择Pipeline |
---|---|---|
需要原子性 | ✔️ | ❌ |
高吞吐量需求 | ❌(事务阻塞风险) | ✔️ |
涉及跨键操作 | ✔️(配合WATCH) | ❌ |
命令间存在逻辑依赖 | ✔️(Lua脚本更优) | ❌ |
扩展方案:对于需要强原子性的复杂操作,推荐使用Lua脚本替代事务,可实现真正的原子性执行。
lua
-- Lua脚本原子扣减库存
if redis.call('GET', KEYS[1]) >= ARGV[1] then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
Java中使用Redis事务
java
package com.morris.redis.demo.transaction;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import java.util.List;
/**
* jedis中事务的使用
*/
public class JedisTransactionDemo {
public static void main(String[] args) {
try (Jedis jedis = new Jedis("localhost")) {
// 监控关键键
jedis.watch("balance:Morris");
Transaction tx = jedis.multi();
try {
tx.decrBy("balance:Morris", 100);
tx.incrBy("balance:Bob", 100);
List<Object> results = tx.exec(); // 提交事务
System.out.println("转账成功:" + results);
} catch (Exception e) {
tx.discard(); // 放弃事务
System.out.println("数据被修改,事务回滚");
} finally {
jedis.unwatch(); // 解除监控
}
}
}
}