今日面试之快问快答:Redis篇
1. Redis 支持事务吗?
Redis 是支持“事务”的,但和我们在数据库里理解的事务不太一样。
-
Redis 事务通过
MULTI
、EXEC
、DISCARD
、WATCH
这些命令来实现。 -
在
MULTI
和EXEC
之间,你输入的命令只是“排队”,直到EXEC
时才会一次性顺序执行。
2. Redis 的事务能回滚吗?
严格来说:Redis 事务不能像数据库那样进行回滚。
-
如果事务里的某个命令语法错误(比如拼写错了),Redis 在
EXEC
时会发现并且整个事务都不会执行。 -
但如果是运行时错误(比如对一个 string 执行 list 操作),Redis 只会在那条命令执行时报错,前面的命令依然执行,后面的命令也继续执行,不会回滚。
这就意味着 Redis 的事务是 原子性地执行命令队列,而不是保证所有命令都成功才提交。
3. 为什么 Redis 不支持事务回滚?
这里涉及 Redis 的设计理念:
-
简单性优先:Redis 核心目标是高性能和简单模型,不想像数据库那样维护复杂的回滚日志。
-
数据操作幂等性:Redis 鼓励开发者在设计时保证操作幂等或用
WATCH
乐观锁机制去控制。 -
性能考虑:事务回滚需要额外的存储和计算成本,Redis 为了保持快速执行,选择放弃传统回滚机制。
4. 那开发中要怎么办?
常见几种做法:
-
使用
WATCH
实现乐观锁:监控关键 key,如果在事务执行前被修改过,就放弃事务。 -
保证命令幂等:比如重复执行不会产生错误状态。
-
Lua 脚本:把多条命令写到一段 Lua 脚本里,Redis 会保证脚本原子执行。
结论(一句话)
Redis 不支持传统意义上的事务回滚(undo/rollback):MULTI/EXEC
不会在执行过程中自动回滚已经生效的命令;只有在“队列阶段”检测到错误时才可能拒绝并丢弃整个队列(以及 WATCH
能在冲突时中止事务)。
核心机制(你需要知道的行为差别)
-
MULTI / EXEC 的工作流
-
MULTI
之后命令被 排队(服务器回复QUEUED
),直到EXEC
才真正执行。EXEC
会按顺序逐条执行这些命令并返回每条命令的结果数组。
-
-
两种错误时机很关键(影响是否会执行/回滚)
-
队列阶段发现错误(queue-time):比如语法/参数错误,会在你发命令时立即报错并不把该命令入队;从 Redis 2.6.5 起,如果积累命令时检测到错误,
EXEC
会拒绝执行并丢弃队列。 -
执行阶段失败(exec-time):例如对 string 做 list 操作会在执行时返回
WRONGTYPE
错误;但其它已排队并成功执行的命令不会回滚,EXEC
会把每条命令的结果(包括错误)返回给客户端。
-
WATCH(乐观锁)能做什么
WATCH
会监视 key:如果在 WATCH
后到 EXEC
前这些 key 被别人改了,EXEC
会返回 null
(事务被中止),这是一种乐观并发控制,能避免并发冲突导致部分执行。注意这不是“回滚”,而是“在冲突时不执行任何入队命令”。Redis
Lua 脚本(EVAL)——更接近“原子性”的替代,但有重要 caveat
-
Lua 脚本在执行期间阻塞其它客户端,保证隔离(其它客户端不会看到脚本执行期间的中间状态)。这是 Redis 文档所称的脚本“原子执行”的含义。
-
但不要把这个“隔离”误认为传统数据库那种带回滚的原子性——如果脚本在中间抛错,很多实践和讨论表明之前已经执行的写入不会被自动回滚(社区对这点有不少讨论/示例)。也就是说脚本可以保证“别人不会看到部分执行的中间态”,但脚本内部如果发生运行时错误,可能会留下部分更改。
-
(额外复杂性:脚本的复制/持久化有不同模式,和 AOF/复制方式的选择有关,出错/崩溃下的恢复语义有细节,官方文档里有说明。)
为什么 Redis 不做 rollback(设计理由)
-
简单与性能优先:维护 undo 日志、回滚机制会显著增加复杂度和开销,和 Redis 作为内存、高性能数据结构服务器的设计目标冲突。
-
鼓励应用层解决:Redis 提供
WATCH
、Lua 脚本、以及原子命令(如INCR
,SETNX
)来满足大多数并发/原子性需求;复杂的事务语义通常建议交给关系型数据库或在应用层实现补偿逻辑。
实践建议(针对Java 后端开发者)
-
需要“全或无”且必须严格回滚 → 用关系型数据库或支持事务回滚的存储。Redis 不适合作为需要完整 ACID 回滚保证的主存储。
-
大多数多命令原子需求 → 用 Lua 脚本(EVAL),把逻辑放在服务器端一次运行(注意错误处理与超时/性能)。但记得:如果脚本内部可能抛错并且你不能接受部分写入,脚本中要自己实现补偿/回滚逻辑(记录旧值并在出错时恢复),或者在脚本里事前校验所有前置条件,尽量避免中途出错。
-
并发冲突场景 → 用 WATCH + MULTI(乐观重试),读取—计算—
MULTI
—排队写—EXEC
,若EXEC
返回 null 就重试。 -
设计为幂等 + 补偿:在业务层尽量让操作幂等,或设计可补偿操作(补偿事务),这样即便部分写入发生也能安全恢复。