面试官:Redis 事务为什么不支持回滚?

业余草

共 2832字,需浏览 6分钟

 ·

2021-10-11 18:55

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,我们一起精进!你不来,我和你的竞争对手一起精进!

编辑:业余草

推荐:https://www.xttblog.com/?p=5281

今年十一期间原本打算更新多篇原创文章的,奈何事情太多,任务都被延期了。原本计划内部培训的 PPT 也耽搁了,只能后面补上了!

我们都知道 Redis 是支持事务的,但是它里面的事务竟然不支持回滚!而且我拿这个问题,问了很多程序员,基本没有回答上来的。今天我们一起聊聊,为什么 Redis 中的事务不支持回滚!

我们都知道,事务有 4 大特性。分别是:原子性(Atomicity)一致性(Consistency)隔离性(Isolation)持久性(Durability)

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的 SQL 语句,要么全部执行成功,要么全部执行失败。

然而,Redis 中的事务,如果在执行中间失败了,在事务开始之前到遇到命令执行失败这中间执行的命令不会回滚。

这就导致了,Redis 的事务没有保证原子性。

下面看一个例子:

redis 127.0.0.1:7000> multi
OK
redis 127.0.0.1:7000> set a aaa
QUEUED
redis 127.0.0.1:7000> set b bbb
QUEUED
redis 127.0.0.1:7000> set c ccc
QUEUED
redis 127.0.0.1:7000> 业余草 www.xttblog.com
QUEUED
redis 127.0.0.1:7000> exec
1) OK
2) OK
3) OK
4)-ERR Operation against a key holding the wrong kind of value

虽然上面这段命令执行过程中会遇到错误,但是不会回滚。set a、set b 等命令操作执行成功了。可以通过 get 取到对应的值。具体我就不贴代码了。

Redis 执行事务过程

Redis 客户端提供了管道操作。管道可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作;中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

Redis 事务为什么不支持回滚?这是一道阿里的面试官。如果我们以 MySQL 等数据库的经验来回答,那你基本上 100% 错误。

然而 Redis 中没有回滚的事务也是有优点的:

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

有种观点认为 Redis 处理事务的做法会产生 bug,然而需要注意的是,在通常情况下,回滚并不能解决编程错误带来的问题。举个例子,如果你本来想通过 INCR 命令将键的值加上 1,却不小心加上了 2,又或者对错误类型的键执行了 INCR,回滚是没有办法处理这些情况的。

鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

一致性(Consistency)

Redis 事务的命令主要是 multi(开启事务) exec(执行事务) discard(丢弃事务)。

Redis 事务在执行的过程中,不会处理其它命令,而是等所有命令都执行完后,再处理其它命令。因此在 Redis 事务在执行过程中发生错误或进程被终结,都能保证数据的一致性。

隔离性(Isolation)

前面也说了,Redis 的事务在执行的过程中,不会处理其它命令,而是等所有命令都执行完后,再处理其它命令。不管用不用管道,只要执行了 multi,就会阻塞其他操作。因此 Redis 事务是满足隔离性的。

Redis的事务没有隔离级别

况且 Redis 是一个单线程的。另外需要注意的是:Redis 虽然保证了隔离性,但是它对事务没有隔离级别的概念,所以就不会产生我们使用关系型数据库需要关注的脏读,幻读,重复读的问题

持久性(Durability)

这个特性可谈可不谈,因为大部分情况下,Redis 是用来做缓存的。很多公司是没有做持久化的,因此可以说 Redis 事务的持久性是不支持的。

Redis 事务不过是用队列包裹起了一组 Redis 命令,并没有提供任何额外的持久性功能,所以事务的持久性由 Redis 所使用的持久化模式决定:

  • 在单纯的内存模式下,事务肯定是不持久的。
  • 在 RDB 模式下,服务器可能在事务执行之后、RDB 文件更新之前的这段时间失败,所以 RDB 模式下的 Redis 事务也是不持久的。
  • 在 AOF 的总是 SYNC模式下,事务的每条命令在执行成功之后,都会立即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。但是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,所以从命令执行成功到数据保存到硬盘之间,还是有一段非常小的间隔,所以这种模式下的事务也是不持久的。
  • 其他 AOF 模式也和总是 SYNC模式类似,所以它们都是不持久的。

因此,我们可以说 Redis 的事务是不支持持久化的,或者说持久化是有缺陷的。就像 Redis 的分布式锁一样。

watch 机制实现乐观锁

虽说 Redis 不支持直接回滚,但我们可以通过 Redis 提供的一个命令来实现回滚

这个命令就是 watch,该命令可以为 Redis 事务提供 check-and-set (CAS)行为。

我们可以使用 watch 命令来监视一个或多个 key,如果被监视的 key 在事务执行前被修改过那么本次事务将会被取消,也就是所谓的回滚。

只有确保被监视的 key,在事务开始前到执行 这段时间内未被修改过事务才会执行成功(类似乐观锁)

如果一次事务中存在被监视的 key,无论此次事务执行成功与否,该 key 的监视都将会在执行后失效 也就是说监视是一次性的。

总结

总的来说:Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。没有用过或了解过 Redis 事务的网友,千万别拿它和数据库事务相比较,否则面试中肯定会吃亏!

如果你想让几个 Redis 的命令保证原子性,那我建议你使用 Lua 脚本,而不是 Redis 事务!

浏览 157
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报