Redis事务(Transactions)
1,认识redis事务
redis的事务,类似于MySQL中的事务,但是相比于MySQL,redis的事务简单了不少。
关于MySQL中的事务,简单回顾下:
原子性:把多个操作打包成一个整体。
一致性:事务执行前后,数据都必须是正确的。
持久性:事务中做出的修改都会存硬盘,保证服务器在重启之后 ,数据仍然存在。
隔离性:事务的并发执行,会涉及到的一系列问题(比如脏读,幻读,不可重复读等)。
redis事务:
弱化原子性:MySQL的原子性,保证事务在执行过程中,要么全部执行成功,要么不执行,也就是有"回滚"操作。而redis事务的原子性,没有"回滚"进制,也就是redis也将一系列操作打包成一个事务,但是事务的执行结果是否正确,是不知道的。
没有一致性:redis没有约束,也没有"回滚"机制,事务在执行过程中,如果某个修改操作出现失败,就可能引起数据不一致的情况。
不具备持久性:redis本身是内存数据库,数据是存储在内存中的。redis本身也是具有持久化机制的,比如上面的RDB和AOF,但是这里的持久化,和事务没有关系。redis收到一条命令(或事务)是进行操作内存的,而MySQL是操作硬盘的。
不涉及隔离性:并发执行事务才会涉及到隔离性,而redis是单线程模型的服务器程序,所有的请求/事务,都是"串行"执行的。
2,redis事务的了解
redis的事务,主要的意义,就是"打包",避免其他客户端的命令,插队插到中间。
redis中实现事务,是引入了队列(每个客户端都有一个)。
开启事务的时候,此时客户端输入的命令,就会发给服务器并且进入这个队列中(而不是立即执行)。
当遇到了"执行事务"的命令,此时就会把队列中的这些命令按照顺序依次推送给服务器去执行。
3,相关命令
事务的开启与执行
开启事务:MULTI
执行事务:EXEC
放弃当前事务:DISCARD
WATCH和UNWATCH
在redis事务中,还提供了WATCH和UNWATCH两个命令。
首先引入下面的场景:
从时间上看,客户端1是先发送了set key 222,客户端2后发送了set key 333,按理来说应该是后发送的生效,也就是key的最终值是 333。
但实际上并非如此,由于客户端1中,必须是exec执行了,才会真正执行set key 222。因此这个操作变成了更晚的操作,所以key的最终值是222,而这也符合事务的特性。
此时就产生了歧义,这时就可以使用WATCH来监视这个key,看看这个key在事务的MULTI和EXEC之间,set key 之后,是否有外部 其他的客户端修改这个key。如果有,会给出提示。
相对的,还有一个UNWATCH命令,这个就是解除对某个key的监视。
效果演示:
在执行exec后,在执行上述事务中的命令时,发现key在外部有修改,那么在执行set key 222时,就没有真正执行,返回接轨是nil,不是ok。
WATCH的实现
WATCH是如何知道其他客户端修改了这个key呢?也就是WATCH是如何实现的。
WATCH的实现,类似于一个"乐观锁"。
所谓乐观锁和悲观锁,不是指某个具体的锁,而是指某一类锁的特性。
乐观锁:加锁之前,就有一个心理预期,预期接下来的锁冲突(锁竞争)概率比较低。
悲观锁:加锁之前 ,也有一个心理预期,预期接下来的锁冲突(锁竞争)概率比较高。
而锁冲突概率高,和所冲突概率低,接下来要做的工作是不一样的。
redis的WATCH就使用相当于"版本号"这样的机制,来实现了"乐观锁"。依旧是上面的例子,不过此时加上了WATCH来监控key 。
当执行WATCH的时候,就会给这个key安排一个"版本号",可以理解为一个整数。每次在修改这个key的时候,这个key的版本号就会变大。
所以当客户端2在执行set key 333时,就会修改这个key的版本号。
当客户端1执行到exec时,在执行事务中命令的时候,此时就会做出判定。判定当前key的版本号和最初WATCH时候记录的版本号是否一致。如果一致,说明当前key在事务开启到执行的整个过程中,没有其他客户端修改,于是才能进行真正的设置;如果发现不一致,说明key在其他客户端中改过了,因此此时就会丢弃该操作,exec返回一个nil。
所以,WATCH本质是给exec加了一个判定条件,这个过程也可以视作是一种加锁的过程。C++中的std::mutex,这是一个悲观锁,不满足条件会进行阻塞等待。而这里的锁,不满足条件就直接丢弃该操作,就相当于执行失败了。这个锁更加简单,也更加轻量,而出现上述情况的概率比较低,所以就没有使用像std::mutex这样复杂的机制,所以说WATCH的实现,类似于一个"乐观锁"。