目录
认识事务
说到redis事务,我们就可以联想到MySQL的事务,但是redis的事务要比MySQL的简单不少
MySQL的事务主要是转账的场景
MySQL事务涉及到:
1.原子性:把多个操作打包成一个整体(要么做,要么不做)
2.一致性:事务执行前和之后,数据都不能离谱
3.持久性:事务中做出的修改都会存在硬盘中(服务器重启,数据依然存在)
4.隔离性:事务并发执行,涉及到的一些问题
对于隔离性,我们需要在这里做一些权衡,要么隔离性提高了,数据靠谱了,效率降低了,因为并发程度降低了;如果隔离性降低了,并发程度提高了,效率提高了,但是数据开始离谱了,在这里我们需要根据实际情况,调整MySQL的隔离级别到符合我们的一个情况
相比于MySQL的事务,redis这里事务就较为朴素了
redis事务:
1.原子性: 对于redis的事务到底有没有原子性这个问题存在着争议
原子性最原本的含义,是把多个操作打包到一起,要么全都执行,要么不执行(不保证成功,如果事务中若干个操作,存在有失败的,那就失败吧,不会有回滚操作)
但是redis做到了上述的含义
只是在这一方面,MySQL走得更远,也是把多个操作打包在一起,要么全都执行成功,要么全都不执行
但是这里的区别就是:MySQL事务中如果有操作执行失败,要进行回滚,把中间已经执行的操作,全都回退了,回到起点
MySQL的标准,提高了"原子性"的门槛,这就使得人们谈到原子性的时候,更多的是想到的MySQL这种带有回滚的原子性

2.不具备一致性:redis没有约束,也没有回滚机制,事务执行过程中如果某个修改操作出现失败,就可能引起不一致的情况
3.不具备持久性:redis本身是内存数据库,数据是存储在内存中的,虽然redis也有持久化机制(rdb、aof),但要明确这个持久化机制只是锦上添花,redis以内存为主体,开启持久化机制与否,使用rdb还是aof,与事务没有什么直接关系
4.不涉及隔离性:redis是一个单线程模型的服务器程序,所有的请求/事务,都是"串行"执行的(一个执行完才执行下一个),并行才需要隔离
但是存在既有存在的道理
redis的事务,主要的意义,就是为了"打包",避免其他客户端命令,插队插到中间
例子:
你和朋友去吃烧烤,你先到,朋友后到,你先点了一部分烧烤,让老板等你朋友来了再烤,等你朋友来的时候,你朋友又加了一部分烧烤,然后你们让老板一起烤
这里的不被插队,不是先抢占位置,而是先让出位置,等到需要统一执行的时候,再一起执行
redis中实现事务,是引入了队列(每个客户端都有一个)
开启事务的时候,此时客户端输入的命令,就会发给服务器并且进入这个队列中(而不是立即执行)
当遇到了"执行事务"命令的时候,此时就会把队列中的这些任务都按照顺序依次执行(redis主线程中完成的),主线程会把事务中的操作都执行完,再处理别的客户端
Redis的事务为啥弄得这么简单,为啥不设计成和MySQL一样强大呢?
MySQL的事务,背后是要付出很大的代价的:
空间上,要花费更多的空间来存储更多的数据
时间上,也要有更大的执行开销
正因为这些问题,才有了Redis
啥时候需要用到redis的事务呢?
如果我们需要把多个操作打包进行,使用事务是比较合适的(商品秒杀)
还有一个概念(超卖):比如放货5000台,实际如果让5001个人下单成功,就是超卖
这里就需要加上限制:

不加上任何限制,就会有"线程安全"问题
如果存在两个客户端同时运行,都同时获取到为1,可以下单,依旧是超卖问题

以前是在多线程中,通过加锁的方式来避免"插队"问题,这里再redis中直接使用事务即可
以下是两个客户端对应要进行的操作

执行这些操作的时候,我们就会发现,中间代码部分会统一发给服务器,服务器就会在队列记录下这些操作,但这些操作不会立即执行(只是排队),而是等到redis服务器收到执行事务的时候才会真正执行
第一个客户端做完这些之后,第二个客户端也开始发送这些操作,同样也是收到执行事务的时候也会真正执行
第二个客户端的"执行事务"命令发过来之后,服务器才真正执行第二个事务中的内容,此时第一个事务执行事务命令已经运行过了,此时,第二个事务get到的count就已经是第一个事务自减之后的结果了
这个场景中,没有加锁,也能解决超卖问题
通过上面的场景,也看得出来,确实redis的事务的应用场景,没有MySQL的事务那么多
redis如果按照集群模式进行部署,是不支持事务的
redis支持lua脚本,通过lua脚本就可以实现上述的条件判定,并且也和事务是一样是打包批量执行的
lua脚本的实现方式是redis事务的进阶版本
事务相关命令
开启事务:
MULTI
开启之后,后续再给服务器发送的操作,都会进入那个事务队列里面
执行事务:
EXEC
告诉redis服务器之前入队列的这些操作进行统一执行
放弃当前事务:
DISCARD
把当前队列中的这些命令全部丢弃掉

在服务器的事务队列中,保存了上述请求
此时如果另外开一个客户端,再尝试查询这几个key对应的数据,是没有结果的


只有执行了EXEC命令,才会执行这些操作


如果我在这里重新设置几个数值,在另外一个客户端中查询到的还是旧的值


这个时候使用discard,就相当于上述的操作没有生效


当开启事务,并且给服务器发送若干个命令之后,此时服务器重启,此时的效果就相当于discard
WATCH
监控某个key是否在事务执行之前发生了改变

在上面这个场景中,从时间上来看,客户端1是先发送了set key 222,客户端2是后发送了set key 333,由于客户端1中,得是exec执行了,才会真正执行set key 222,这个操作变成了实际更晚执行的操作,最终值是222
这个场景就可以使用watch命令来进行监控这个key
看看这个key在事务的multi和exec之间,set key之后,是否在外部被其他客户端修改了

此时,exec在执行上述事务中的命令的时候,发现key在外部有修改,于是在真正执行set key 222的时候就没有真正执行
原理
watch的实现,类似于一种"乐观锁"
乐观锁、悲观锁:不是指某个具体的锁,而是指的是某一类锁的特性
乐观锁:加锁之前,就有一个心理预期,预期接下来锁冲突的概率比较低
悲观锁:枷锁之前,也有一个心理预期,接下来锁冲突(两个线程对同一个锁加锁,一个能加锁成功,另一个阻塞等待)的概率比较高
锁冲突概率高,和锁冲突概率低,接下来要做的工作是不一样的(电脑录播的锁)
redis的watch就相当于是基于版本号这样的机制,来实现了"乐观锁"
当执行watch key的时候,就会给这个key安排一个版本号,版本号可以理解成一个"整数",每次在修改的时候,版本号都会"变大"
watch本质是给exec加了个判定条件

只要是对此处key做出了修改,就会引起版本号变大
exec:在执行事务中的命令的时候,此处就会做出判定,判定当前这个key的版本号,和最初watch的时候记录的版本号是否一致(老王看枕头判断是否出轨)
如果一致,说明当前key在事务开启到最终执行这个过程中,没有别的客户端修改,于是才能真正进行设置
如果不一致,说明key在其他客户端中改过了,因此此处就直接丢弃事务中的操作,exec返回nil
小结
redis的事务要比MySQL的事务简单得多
1.原子性:redis的事务,并不支持回滚
2.一致性:redis并不会保证事务执行前和执行后,内容统一
3.持久性:redis主要通过内存来存储数据
4.隔离性:redis自身作为一个单线程的服务器模型,上面处理的请求本质上都是串行执行的
redis中的lua脚本,也能起到类似于事务的效果,官方网站上说,事务这里的任何能实现的效果,都可以用lua脚本代替