前言前几天有读者说他在面试中被问到Redis事务。面试的时候被问到Redis的业务,很不爽。读者虽然最后通过了面试,但是在薪资方面并没有得到自己理想中的薪水。其实这也很正常。一般面试,如果问你烂大街,谁会问你?他们特地挑一些不常见的问题问你,就是为了压你的工资。所以在这里写一篇文章详细讲解Redis事务。估计这篇文章足以让你从理解到原理了解Redis事务。以后再也不用担心面试被问到Redis的事情了。本文主要讲解Redis事务的原理和实战练习。在理解理论的同时,还要通过实际操作来验证理论。事务介绍Redis事务是一组命令的集合,将多个命令封装起来,然后这些命令会依次加入到队列中,并依次执行这些命令。“Redis事务中没有像Mysql关系型数据库事务那样的事务隔离级别的概念,不能保证原子操作,也没有像Mysql那样执行事务失败的回滚操作。”这与Redis的特性密切相关:“快速高效”,“因为一些系列的回滚操作,像事务隔离级别的加锁和解锁是非常耗性能的”。因此,在Redis中执行一个事务的过程只需要以下三个简单的步骤:启动一个事务(MULTI)命令入队执行一个事务(EXEC),取消一个事务(DISCARD)Redis中一个事务的实现主要是实现通过以下命令:命令功能描述MULTI“命令启动事务”。执行该命令后,后续的“针对Redis数据类型的操作命令会依次放入队列”。队列中的命令只有在执行完EXEC命令后才会执行。DISCARD“放弃执行队列中的命令”,可以理解为Mysql的回滚操作,“将当前状态从事务状态变为非事务状态”。EXEC执行命令后,“表示顺序执行队列中的命令”,执行后在客户端显示结果,“将当前状态从事务状态变为非事务状态”。如果一个键被WATCH命令执行,并且在执行这个命令之前被其他客户端修改,那么队列中的所有命令将被放弃,并在客户端显示错误信息,如果不修改,队列中的所有命令将被删除执行。WATCHkey表示指定监听某个key,“此命令只能在MULTI命令之前执行”,如果监听的key被其他客户端修改,“EXEC将放弃执行队列中的所有命令”UNWATCH取消前通过WATCHmonitoringThekeymonitoredbythecommand”,在执行EXEC和DISCARD这两个命令之前监控的key也会被取消监控。以上是一个Redis事务执行过程中包含的命令。下面将详细解释这些命令。启动事务的MULTI命令指示事务的开始。当你看到OK的时候,就说明你进入了事务状态:命令执行后,客户端会将“当前状态从非事务状态变为事务状态”。这个状态切换是改变客户端这个命令可以理解关系型数据库的BEGINTRANCATION语句Mysql:命令入队执行MULTI命令后,后面要执行的5种Redis命令都会进入命令队列按顺序,这部分也是真正的业务逻辑部分。Redis客户端的命令执行后,如果当前状态为事务状态,则命令进入队列,返回QUEUED字符串,表示该命令进入命令队列,“事务队列保存在先进先出(FIFO)入伍顺序的形式”。如果当前状态为非事务状态,则立即执行命令,并将结果返回给客户端。在事务状态下,“执行操作事务的命令会被立即执行”,如EXEC、DISCARD、UNWATCH。结合以上分析,Redis执行命令的流程如下图所示:事务命令队列中有三个参数:“待执行命令”、“命令参数”、“参数个数”。例如:通过执行如下命令:redis>MULTIOKredis>SETname"Lidu"QUEUEDredis>GETnameQUEUED则上述队列中对应的三个参数如下表所示:执行命令的参数个数commandSET["name","lidu"]2GET["name"]1Executiontransaction当客户端执行EXEC命令时,上面的命令队列会按照先进先出的顺序执行,当然执行的结果可能是成功还是失败,这个稍后分析。上面提到,当客户端处于非事务状态时,发送给服务器的命令会立即执行。如果客户端处于事务状态,命令将被放入命令队列。当命令进入队列时,会按顺序进入队列,队列以先进先出的特点执行队列中的命令。如果客户端处于事务状态,EXEC、DISCARD、UNWATCH等执行事务的命令也会立即执行。正常执行还是上面的例子,执行如下代码:redis>MULTIOKredis>SETname"LiDu"QUEUEDredis>GETnameQUEUED所有命令进入队列,最后执行EXEC时,会先执行SET命令,再执行GET命令被执行,执行后结果也会存入一个队列,最后返回给客户端:最后,像这样的结果说明,这也是一个事务执行成功的过程。至此,一个事务执行完毕,此时客户端也从事务状态变为非事务状态。放弃交易当然你也可以放弃交易的执行,只要你再次执行DISCARD操作,就会放弃本次交易的执行。具体代码如下:redis>MULTIOKredis>SETname"Lidu"QUEUEDredis>GETnameQUEUEDredis>DISCARD//放弃事务的执行。非事务状态。“Redis事务是不可重复的。”当客户端处于事务状态,再次向服务器发送MULTI命令时,会直接向客户端返回错误。WATCH命令WATCH命令在MULTI命令之前执行,意思是监听任意数量的key,它对应的命令是UNWATCH命令,取消监听的key。WATCH命令有点“类似于乐观锁机制”。事务执行时,如果被监听的key有变化,则不会执行队列中的命令,客户端直接返回(nil)表示事务执行失败。下面演示一下WATCH命令的运行过程。具体实现代码如下:redis>WATCHnumOKredis>MULTIOKredis>incrbynum10QUEUEDredis>decrbynum1QUEUEDredis>EXEC//执行成功。这是WATCH命令的正常运行过程。如果是在其他客户端,修改任何监听到的key都会放弃事务的执行,如下图:Client1Client2WATCHnumMULTIincrbynum10getnumdecrbynum1EXEC执行失败,返回(nil)并在WATCH命令的底层实现中保存watched_keys字典,"字典的key存储的是被监控的key,value是一个链表,链表中每个节点的value存储的是监控的客户端钥匙。”如果客户端不再监视某个密钥,则该客户端将离开链表。比如client3,通过执行UNWATCH命令,不再监听key1:错误处理上面说到Redis没有回滚机制,所以在执行过程中,如果不小心输入了错误的命令,Redis发送给服务器的命令是没有立即执行。所以暂时没有发现错误。那么Redis中的错误处理主要分为两类:“语法错误”和“运行错误”。下面主要说明这两类错误的区别。语法错误例如执行命令时,命令不存在或输入错误的命令、参数个数不正确等,都会造成语法错误。下面来演示一下,执行以下4条命令,前后两条命令正确,中间两条命令错误,如下图:127.0.0.1:6379>multiOK127.0.0.1:6379>setnum1QUEUED127。0.0.1:6379>setnum(error)ERRwrongnumberofargumentsfor'set'command127.0.0.1:6379>sssetnum3(error)ERRunknowncommand'ssset'127.0.0.1:6379>setnum2QUEUED127.0.0.1:6379>exedisercauser.transactoftransacted语法错误可以Redis语法检测时会发现,所以当你执行错误的命令时,即使是也会返回错误提示。最后,即使命令进入队列,只要有语法错误,队列中的命令也不会被执行,直接返回事务执行失败的提示给客户端。当执行过程中使用不同类型的操作命令来操作不同的数据类型时,就会出现运行时错误。不执行命令Redis是无法发现这种错误的。127.0.0.1:6379>multiOK127.0.0.1:6379>setnum3QUEUED127.0.0.1:6379>saddnum4QUEUED127.0.0.1:6379>setnum6QUEUED127.0.0.1:6379>exec1)OK2)(错误)WRONGTYPEOperationagainstakeyholdingof7.w0kindof7.w0kindof7.w0kindofvalue3).1:6379>getnum"6"会导致正确的命令执行,错误的命令不会执行,这也说明Redis事务不能保证数据的一致性,因为中间有错误,有些语句仍然被执行。这样的结果只能由程序员根据之前执行过的命令一步步改正,所谓烂摊子自己收拾。Redis事务与Mysql事务我们知道,关系型数据库Mysql具有事务的四大特性:“原子性、一致性、隔离性、持久性”。但是,为了保证Redis除了客户端请求之外的高效,Redis的事务省去了传统关系型数据库中的“事务回滚、加锁、解锁”等耗性能的操作。Redis事务很容易实现。Redis事务在原子性上只能保证单个命令的原子性,不能保证多个命令的原子性,比如上面索道的运行时错误,即使中间出现运行时错误,后面正确的命令也会正确执行,并且没有返回滚动操作。由于没有原子性,就无法保证数据的一致性,这一切都需要程序员手动实现。当Reids正在运行一个事务时,它不会被中断,直到事务结束。它也有一定的隔离性,Redis也可以持久化数据。
