面试官:Redis 事务为什么不支持回滚?
你知道的越多,不知道的就越多,业余的像一棵小草!
你来,我们一起精进!你不来,我和你的竞争对手一起精进!
编辑:业余草
推荐: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 事务为什么不支持回滚?这是一道阿里的面试官。如果我们以 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 虽然保证了隔离性,但是它对事务没有隔离级别的概念,所以就不会产生我们使用关系型数据库需要关注的脏读,幻读,重复读的问题
。
持久性(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 事务!