Redis事务全面解析

一、Redis事务概述

1.1 什么是Redis事务?

Redis事务是一组命令的集合,这些命令会被顺序化、串行化地执行,确保在执行过程中不会被其他客户端的命令打断。

核心特性

  • 命令打包:多个命令打包成一个单元执行
  • 顺序保证:执行期间不被其他客户端插队
  • 简单易用:相比MySQL事务实现更轻量

1.2 Redis事务与MySQL事务对比

特性 Redis事务 MySQL事务 Redis实现情况
原子性 ✅ 弱化原子性 ✅ 强原子性 部分失败不自动回滚
一致性 ❌ 不支持 ✅ 支持 无一致性约束
隔离性 ❌ 不涉及 ✅ 支持 单线程天然串行
持久性 ❌ 不支持 ✅ 支持 与持久化机制解耦
设计目标 防止命令插队 保证ACID特性 简单高效

1、原子性 (redis具有弱化的原子性)

redis的原子性存在争议。

原子性是说,多个操作打包成一个操作,要么全部执行,要么全都不执行。

redis确实做到了,但是,MySQL中的原子性更高级,多个操作打包成一个操作,要么全部执行成功,要么全都不执行,有一个操作失败了,就全部回滚。

而redis的原子操作中的部分操作执行失败了,不会回滚,而是接着执行后面的操作。

所以,redis的原子性比MySQL的原子性更弱一些。
2.一致性

redis的事务没有一致性。

redis没有约束,也没有回滚机制,事务执行过程中如果部分操作失败了,

就会导致事务执行前和执行后的状态不一致。
3、持久性

redis的事务没有持久性。
redis本身是一个内存级的数据库,数据存储在内存中,

虽然redis也有持久化机制,不过持久化机制是为了redis重新启动恢复历史数据的。

MySQL事务提交后,就会有一个异步工作线程立即刷新到磁盘上,

而redis事务提交后,数据依旧在内存中,只有手动bgsave或者bgrewriteaof,以及达到自动触发持久化的条件,才会去持久化到磁盘上。

和事务是否提交没有关系。
4、隔离性

redis的事务不涉及隔离性。
redis是单线程模型,处理所有的命令请求是串行化的,根本不会涉及到并发操作,

而隔离性是指多个事务并发执行时,互相不影响,

redis根本就不会出现多个事务并发执行的情况,也就不涉及隔离性。

1.3 Redis事务的意义

redis的事务效果这么弱,原子性是弱原子性,不存在一致性、隔离性、持久性,

和MySQL的事务比起来差远了,那么redis中的事务有啥意义呢?

redis事务的核心意义在于: 让一个客户端的一串命令连续执行,不会被其他的客户端的命令插队。


二、Redis事务核心原理

2.0 事务的原理

当收到开启事务的命令的时候,redis就会给该客户端创建一个事务队列(每个客户端的事务队列是独立的),

之后,该客户端发送过来的所有命令都放入到事务队列中,不急着执行,

等收到该客户端发送过来的EXEC命令的时候,

redis就会把该客户端的等待队列中的所有命令全部连续执行,在这些该客户端事务的时候,不允许其他客户端插队,直到完成了该客户端的事务中的所有命令。

2.1 事务执行模型

事务队列管理 命令1: set key1 value1 命令2: incr counter 命令3: hset hash field value 客户端A MULTI命令 Redis创建事务队列 客户端A的命令入队 客户端A发送EXEC Redis串行执行队列命令 返回所有命令结果 客户端B 其他命令 等待事务队列执行完毕

2.2 事务队列工作机制

执行流程

复制代码
客户端发送MULTI → Redis创建事务队列 → 后续命令入队 → 发送EXEC执行 → 返回结果

示例

redis 复制代码
# 1. 开启事务
127.0.0.1:6379> MULTI
OK

# 2. 命令入队(不立即执行)
127.0.0.1:6379> SET user:name "张三"
QUEUED
127.0.0.1:6379> INCR user:count
QUEUED
127.0.0.1:6379> HSET user:info age 25
QUEUED

# 3. 执行事务
127.0.0.1:6379> EXEC
1) OK        # SET命令结果
2) (integer) 1  # INCR命令结果
3) (integer) 1  # HSET命令结果

2.3 Redis弱原子性分析

事务开始 MULTI 命令1: SET key1 value1 命令2: INCR non_existent_key 命令3: SET key2 value2 执行事务 EXEC 命令执行结果 SET key1 value1: 成功 INCR non_existent_key: 失败
返回错误 SET key2 value2: 成功 事务执行完成 最终状态
key1=value1, key2=value2
部分命令失败但未回滚

Redis原子性问题

  • 不支持回滚:部分命令失败时,已成功的命令不会回滚
  • 错误继续执行:遇到错误命令后,后续命令继续执行

三、Redis事务相关命令

3.1 基础事务命令

命令 作用 返回值 使用场景
MULTI 开启事务 OK 事务开始标识
EXEC 执行事务 命令结果数组 触发队列命令执行
DISCARD 取消事务 OK 放弃当前事务
WATCH 监控键值变化 OK 实现乐观锁
UNWATCH 取消监控 OK 取消所有WATCH

3.2 事务命令使用示例

redis 复制代码
-- 场景1:正常事务执行
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET order:1001 "pending"
QUEUED
127.0.0.1:6379> INCR total_orders
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 42

-- 场景2:取消事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET temp "value"
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> GET temp
(nil)  -- 命令未执行

-- 场景3:命令执行错误
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET a 1
QUEUED
127.0.0.1:6379> INCR b  -- b不存在,会出错
QUEUED
127.0.0.1:6379> SET c 3
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR wrong number of arguments for 'incr' command
3) OK  -- 注意:错误后继续执行!

3.3 WATCH命令

watch命令用来监控某个key在事务执行之前,是否发生了改变。

我们来看这么一个场景

时间 事务A 操作
0 watch key
1 multi
2 set key 111
3 set key 222
4 exec

看起来,最后的执行结果key = 111,因为在进行set key 222的时候,事务A并没有发送exec执行事务命令。

实际上,最后的执行结果是key = 222,exec返回nil,事务取消执行。

使用watch,就可以监控key在事务执行前,有没有发生变化,

如果变化了,exec的时候,exec就会执行失败,会清空事务队列,取消watch监控;

如果没有变化,exec的时候,exec执行成功,会清空事务队列,取消watch监控。
客户端1 Redis服务器 客户端2 客户端1监控key并开始事务 WATCH balance MULTI 客户端2修改被监控的key SET balance 200 客户端1提交事务(失败) SET balance 100 EXEC (nil) 事务执行失败 检测到balance版本变化 事务队列被清空 客户端1 Redis服务器 客户端2

3.4 WATCH工作原理

watch的原理就是使用了版本号的机制,

当watch一个key的时候,就会给这个key生成一个版本号,

如果key被修改了,key的版本号就会增大,

当事务要被exec执行的时候,就会检查当前的key的版本号,是否和watch时生成的版本号一致,

如果一致,就是没有被事务外面的操作修改,正常执行事务队列的操作,

如果不一致,说明被事务外面的操作修改,exec执行失败,清空事务队列,并取消watch监控,返回nil。

  1. 版本号机制:每个被WATCH的key都有隐式版本号
  2. 变化检测:EXEC时检查版本号是否变化
  3. 事务中止:版本变化则EXEC返回nil,事务队列清空

3.5 乐观锁和悲观锁

这实际上是一种乐观锁的实现,什么是乐观锁呢?

与乐观锁对应的还有一个悲观锁。

乐观锁和悲观锁,并不是指的某个具体的锁,而是指的某一类锁的特性。

乐观锁:加锁之前,就有心理预期,接下来发生锁冲突的概率比较低;

悲观锁:加锁之前,就有心理预期,接下来发生锁冲突的概率比较高。

两种类型的锁,未来发生锁冲突的概率不同,需要采取的应对措施也不同,

乐观锁,发生冲突概率小,就用较低的成本来解决锁冲突的问题,

悲观锁,发生冲突的概率大,就需要使用较高的成本来解决锁冲突的问题。


四、Redis事务应用场景

4.1 经典场景:超卖问题

redis 复制代码
-- 防止超卖的库存扣减方案
-- 不使用事务的问题:check和decr之间可能被其他客户端插队

-- 使用事务的解决方案
127.0.0.1:6379> WATCH product:1001:stock
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> GET product:1001:stock
QUEUED
127.0.0.1:6379> DECR product:1001:stock
QUEUED
127.0.0.1:6379> EXEC
1) "10"     -- 检查库存
2) (integer) 9  -- 扣减库存

4.2 Redis事务的限制与替代方案

限制 问题描述 解决方案
不支持回滚 部分命令失败无法回滚 使用Lua脚本替代
集群不支持 Redis Cluster不支持多key事务 使用Hash Tag或Lua脚本
复杂条件判断 事务内无法条件判断 Lua脚本实现业务逻辑
性能考虑 长时间事务阻塞其他客户端 拆分事务,减少阻塞时间

Lua脚本替代方案

lua 复制代码
-- Lua脚本实现原子操作
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) > 0 then
    redis.call('DECR', KEYS[1])
    return 1  -- 扣减成功
else
    return 0  -- 库存不足
end

-- Redis执行Lua脚本
redis-cli --eval decr_stock.lua product:1001:stock

七、总结

7.1 Redis事务核心价值

优势 说明 适用场景
命令打包 多个命令作为一个单元执行 需要原子性操作的场景
防止插队 执行期间不被其他客户端打断 并发资源竞争
简单轻量 实现简单,性能开销小 不需要完整ACID的场景
乐观锁支持 通过WATCH实现版本控制 数据一致性要求较高的场景

7.2 重要限制与注意事项

  1. 不支持回滚:部分失败时需手动补偿,想要回滚需要使用Lua脚本实现
  2. 集群限制:redis集群分布式部署的时候,不支持事务
  3. 无隔离性:redis单线程模型无需隔离
  4. 持久化解耦:事务提交不等于数据持久化

Redis事务虽然确实不如MySQL事务强大,但在特定的高并发、简单操作场景下,以其轻量级、高性能的特点,仍然是解决并发问题的有效工具。

相关推荐
Hello.Reader2 小时前
KeyDB 一台“40 英尺卡车”式的 Redis 兼容高性能缓存
数据库·redis·缓存
爱好读书2 小时前
AI生成ER图|SQL生成ER图
数据库·人工智能·sql·毕业设计·课程设计
小尧嵌入式2 小时前
Linux网络介绍网络编程和数据库
linux·运维·服务器·网络·数据库·qt·php
最贪吃的虎2 小时前
MySQL调优 一:慢SQL日志
运维·数据库·后端·mysql
Data_Journal2 小时前
使用 PowerShell Invoke-WebRequest 配合代理的完整指南
数据库
最贪吃的虎2 小时前
MySQL调优 二:explain参数详解+索引优化实战
数据库·mysql
严文文-Chris2 小时前
如何让向量数据库的“查找目录”又快又准?
数据库
百***24372 小时前
GPT-Image 1.5 vs Nano Banana Pro 深度对比:国内业务落地的场景适配与避坑指南
java·数据库·gpt
代码栈上的思考3 小时前
MyBatis——动态SQL讲解
java·开发语言·数据库