本文主要是讨论redis的事务和传统事务(mysql)在设计上的区别和背后的设计决策的借鉴意义
可能大多数人都会觉得这个不是很正常,两个不一样的数据库,有各自的优势和不同的应用场景,事务处理上有点区别有什么奇怪的,这。。。确实没错,but....了解采取这种决策方式背后的意图和对相同实现方式不同场景下的设计决策对比的思考,对我们现实的项目设计中采取哪些方案是很有帮助的。
先来看看redis和mysql对于事务处理涉及到的相关命令:
当然mysql在事务回滚上还有更多的玩法,例如:SAVEPOINT
从这里你至少会发现:redis没有提供和回滚有关的命令,或者你会觉得还能接受,至少如果在事务执行过程中出现问题了,redis整个的事务还能回滚,然而,实际并不是这样的,redis的事务中,就算其中一个执行失败了,后面的事务还是会继续进行!并不是你理想中的,整个事务就失败了。
来看看redis的官方文档是如何定义redis的原子性:
Either all of the commands or none are processed, so a Redis transaction is also atomic.
意思是:事务中的命令要么全部被执行,要么全部都不执行,所以redis的事务也是原子性
所以,你会看到文档中强调:
It's important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands.
而mysql事务的原子性的定义:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
到这里,我们整理一下redis和mysql的事务的区别:
1,没有提供回滚相关的命令,无法像mysql一样提供任意时刻任意点的回滚
2,事务执行过程中有某条/某些命令执行失败了, 事务队列中的其他命令仍然会继续执行 —— Redis 不会停止和回滚执行事务中的命令。
所以从这两点上看出,redis必须保证每条命令成功执行,否则这个事务就不符合事务的标准:1,无法保证原子性 2,无法保证一致性
通过redis对事务介绍的文档中看出设计者对于redis事务设计的两个特征:
1,不考虑事务执行命令时外界上下文逻辑
2,基于redis单线程模型实现的事务处理
不考虑事务执行命令的外界上下文逻辑
redis执行事务的过程:MULTI 命令开启一个事务后, 客户端向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 队列中的命令才会被执行。
在这里,你可以发现redis触发事务的执行时是脱离外部业务逻辑的上下文关系的,这一点和mysql完全不同:mysql的每个语句是可以充分的结合到整个业务过程中,也就是事务开启时,外界上下文的触发的每一步操作都是同步执行的, 会对访问同一资源的其他事务造成影响,这也是mysql事务应用广泛之处,并通过多版本控制实现不同隔离级别状态的任意的回滚。
而redis这种简单粗暴的事务方式的带来的直接好处是:
1,摆脱的来自于外界的阻塞,阻塞这一点对于要求高性能的redis来说是绝对不可接受的,而在mysql上,你会时不时看到长事务导致了整个系统的性能问题。
2,剥离了外部不可控的因素,redis事务就只是命令的集合,所以redis设计者发现传统事务都支持的回滚(roll back)这种操作完全是多余的:
Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.Redis is internally simplified and faster because it does not need the ability to roll back.
从上面可以看到设计者的理由:
1,Redis 事务的报错只会因为错误的语法而失败或是命令用在了错误类型的键上面,这也就是说,失败的命令是由编程错误造成(人为)的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
好像很有道理。。。说白了redis不想为你的人为失误买单。。
2,因为不需要回滚,所以redis内部更简洁和高效
每个系统在设计时当然都希望更健壮,可以考虑更多的异常情况和有相应的处理机制,但随之而来就让系统更加的复杂和性能损耗,在事务处理上:事务的隔离级别越高,性能就越低,数据库系统也只能在这两个中进行权衡和取舍,从这里我们也可以看出: redis选择了性能,而放弃了可能因为外部人为问题而导致数据不一致问题(mysql事务刚好相反)。
基于redis单线程模型实现的事务处理
可能你会疑问上面redis会有隔离级别而导致性能问题?
redis确实没有,因为这些为redis的高性能让步,redis可以说是天然的串行化的隔离级别,redis事务是保证在执行事务中的一连串操作时不受其他客户端的命令打断,达到串行执行的目的。配合上 WATCH就可以实现和mysql的串行化隔离级别的效果
而从这一执行效果可以看到,redis还是基于现有的单线程模型来实现事务,这更能佐证一点:设计者为了保证高性能,宁愿放弃一些外部因素导致事务的数据不一致问题,虽然redis可以通过多线程来实现来引入事务回滚并能确保事务过程中的redis服务的可用性。
总结
到这里,我们知道了,redis的事务只要正确执行(没有人为失误),也是同样满足传统事务的四大特性(ACID),也了解了redis事务的使用场景以及做出这些决策背后的原因,可见:
数据库系统事务设计中,保证一个事务完整执行,原本数据一致性应该是首要位置,因此,系统设计中必然要考虑到更多的异常的不可控的情况和相应的处理机制,从而保证系统自身的数据安全, 而在redis中,我们看到了另外一种可能:为了保证redis的高性能而在设计决策上所做的妥协和调整, 虽然保证严格的一致性似乎对于严谨的事务设计方案来说会更让人接受,但这对于redis来说,其实仅仅只是一个局部的功能,从整个系统的角度上,高性能和效率才是redis的初衷,从而也理解redis事务的特殊之处和其应用场景。