Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis事务

Redis事务的使用小结

作者:一条泥憨鱼

Redis 事务通过 MULTI / EXEC 命令块实现批量命令的顺序执行,是 Redis 中保证操作原子性的核心机制,本文从基本用法出发,详细讲解了事务的完整执行流程,需要的朋友可以参考下

前言:

在日常开发中,很多开发者要么不知道 Redis 有事务,要么以为它和 MySQL 的事务是一回事。两者差挺远的,搞混了线上容易出问题。这篇文章把核心机制、坑点、乐观锁和面试常问的几个问题串一遍。

Redis 事务到底是什么

把多条命令塞进一个队列,到点了按顺序一口气执行完。执行过程中其他客户端插不进来。就这四个命令:

- MULTI — 开始攒命令
- EXEC — 队列里的命令全部执行
- DISCARD — 不玩了,清空队列
- WATCH — 盯住一个或多个 key,EXEC 之前如果有人动过,事务自动取消

上手:转账的例子

A 账户扣 100,B 加 100。先看看正常流程:

127.0.0.1:6379> SET account:A 500
OK
127.0.0.1:6379> SET account:B 200
OK

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY account:A 100
QUEUED
127.0.0.1:6379> INCRBY account:B 100
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 400
2) (integer) 300

MULTI 之后每条命令都返回 QUEUED,说明在排队。EXEC 一执行,结果按入队顺序返回。

想反悔的话:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> DISCARD
OK

队列清空,什么都没发生。

最容易踩的坑:错误处理

Redis 事务里的错误分两种,处理逻辑完全不同,搞混了线上真的会出事。

第一种:语法错误(入队时就发现了)

命令拼错了、参数不对,Redis 当场报错,EXEC 的时候整个事务直接取消:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> NOTACMD
(error) ERR unknown command 'NOTACMD'
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

k1 和 k2 都没写进去。这种还好,至少是"全有或全无"。

第二种:运行时错误(执行了才发现不对)

语法没问题,执行时栽了——比如对字符串做 INCR。Redis 会跳过那条出错的命令,其余的照常执行,已经执行的不会回滚:

127.0.0.1:6379> SET k1 "hello"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 "world"
QUEUED
127.0.0.1:6379> INCR k1
QUEUED
127.0.0.1:6379> SET k2 v2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK

k1 变成了 world,k2 也写进去了。只有 INCR 那条挂了。这就是 Redis 事务和 MySQL 事务最要命的区别——Redis 事务不支持回滚。

不支持回滚是故意的

很多人第一次知道这个会觉得很坑。说实话我第一次也这么觉得。

但 Redis 官方的逻辑是:运行时错误是程序员的 bug——你对 String 类型的 key 做 INCR,这应该在测试阶段就发现。为此让 Redis 支持回滚,意味着每次操作前要保存旧数据、出错时逐条恢复,这对一个追求极简和高性能的东西来说太重了。

所以用 Redis 事务,你得自己保证命令是对的。别把它当 MySQL 用。

WATCH:乐观锁

MULTI/EXEC 有个盲区:如果事务里的命令依赖某个 key 当前的值,而这个值在 MULTI 之后、EXEC 之前被别的客户端改了,事务照样正常执行,结果就出错了。

比如库存剩 1,两个客户端同时读到 1,都觉得自己能下单,两个都执行了 DECR,库存直接变 -1。

WATCH 解决的就是这个。

怎么用

127.0.0.1:6379> SET stock 1
OK

127.0.0.1:6379> WATCH stock
OK
127.0.0.1:6379> GET stock
"1"

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECR stock
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 0

被别人抢先改了

另一个客户端在 WATCH 之后、EXEC 之前动了 stock,你的 EXEC 返回 nil,一条命令都不会执行:

127.0.0.1:6379> EXEC
(nil)

收到 nil 就说明事务没执行。重读数据、重新判断、再试一次,这就是乐观锁的标准套路。

几个细节:
- EXEC 执行后(不管成败),WATCH 自动解除
- DISCARD 也会解除所有 WATCH
- 想主动解除可以执行 UNWATCH

ACID 四个维度怎么看

你可以这样说:

- 原子性 — 有条件的。队列命令按顺序执行、不被 插入,这部分是原子的。但运行出错不回头,不是严格意义的原子性。
- 一致性 — 语法错误全取消,不会脏写。
- 隔离性 — EXEC 前队列里的命令对其他客户端不可见,执行不被打断。
- 持久性 — 看你开了什么。AOF + appendfsync always 就有持久性;RDB 或 AOF everysec 就有可能丢数据。

Redis 事务 vs 关系型数据库

对比项Redis事务关系型数据库事务
回滚支持不支持支持
运行时错误处理跳过出错命令,继续执行回滚整个事务
隔离级别执行期间不被打断多级隔离级别可选
持久性取决于持久化配置默认持久化
使用场景简单批量操作复杂业务逻辑,强一致性要求

简单说:Redis 事务轻、快、弱一致。别拿它处理复杂业务逻辑。

实际开发中的建议

别把 Redis 事务当 MySQL 事务用。 它不回滚。如果你真的需要"要么全做要么不做",光靠 Redis 事务做不到。

复杂逻辑用 Lua 脚本。 Redis 执行 Lua 是原子的,脚本里还能加判断逻辑,比事务灵活。生产环境里,需要复杂原子操作的基本都用 Lua,很少裸写事务。

WATCH 重试要设上限。 并发高的时候可能反复失败,不设上限就是死循环。

事务里的命令尽量短。 Redis 单线程处理,你的队列越长,所有其他命令都得等着。

疑点解惑

Redis 事务是原子的吗?

有条件的原子。命令顺序执行不被打断,但出错不回头。回答时分两种情况:语法错全取消,运行错只跳当前命令。

为什么不支持回滚?

官方觉得运行错误是 bug,测试阶段就该抓到。加回滚等于给 Redis 塞一套恢复机制,又复杂又慢,跟 Redis 的设计方向冲突。

WATCH 是悲观锁还是乐观锁?

乐观锁。不加锁,不阻塞,EXEC 时检查 key 有没有被改过。被改了返回 nil,客户端自己决定要不要重试。

Redis 事务和 Lua 脚本的区别?

都能保证原子执行。Lua 脚本能做条件判断,有逻辑控制,事务就是纯命令队列。生产上用复杂原子操作,优先 Lua。

最后

Redis 事务就是命令排队 + 顺序执行,有隔离性但不回滚。WATCH 补上了乐观锁的能力。技术本身不复杂,复杂的是搞清楚什么场景该用什么工具。别把 Redis 事务想成 MySQL 事务的平替——它不是。

到此这篇关于Redis事务的使用小结的文章就介绍到这了,更多相关Redis事务内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文