【redis】事务

简单地说,事务表示一组动作,要么全部执行,要么全部不执行。Redis事务是一组命令的集合,通过MULTIEXEC等命令实现批量操作的原子性执行。

Redis事务的特点

其核心特点包括:

  • 顺序性:命令按入队顺序执行
  • 隔离性:单线程模型保证事务执行不被打断
  • 弱原子性:不支持回滚机制(运行时错误不会中断后续命令)
  • 乐观锁支持 :通过WATCH命令监控键值变化

注意:Redis事务不同于传统数据库事务,它更类似于命令批处理机制。

Redis事务的本质

命令队列机制

通过三个阶段实现事务:

  1. 开启事务MULTI命令创建命令队列
  2. 命令入队:将操作指令存入内存队列(不执行语法检查)
  3. 执行事务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();  // 解除监控
            }
        }
    }

}
相关推荐
Yasen^o1 小时前
Redis高可用
数据库·redis·缓存
小黑蛋学java4 小时前
redis 集群节点切换角色
redis
.生产的驴4 小时前
SpringBoot 接口限流Lua脚本接合Redis 服务熔断 自定义注解 接口保护
java·大数据·数据库·spring boot·redis·后端·lua
施嘉伟5 小时前
Oracle 表空间高水位收缩全攻略
数据库·oracle
apcipot_rain5 小时前
【数据库原理及安全实验】实验二 数据库的语句操作
数据库·安全·oracle
Dav_20996 小时前
dav_1_MySQL数据库排查cpu消耗高的sql
数据库·sql·mysql
爱吃鱼饼的猫7 小时前
【SpringBoot篇】如何使用CommandLineRunner实现缓存预热
spring boot·spring·缓存
小诸葛的博客8 小时前
Go 语言中的select是做什么的
数据库·sql·golang
独泪了无痕8 小时前
数据库开发必备:理解DDL、DML、DQL和DCL
数据库·后端
Gauss松鼠会9 小时前
GaussDB Plan Hint调优实战:从执行计划控制到性能优化
数据库·sql·性能优化·database·gaussdb