文章目录
- 什么是事务
- 优势
-
- [与 MySQL 对比](#与 MySQL 对比)
- 用处
- 事务相关命令
-
- 开启事务------MULTI
- 执行事务------EXEC
- 放弃当前事务------DISCARD
- [监控某个 key------WATCH](#监控某个 key——WATCH)
- 事务总结
什么是事务
MySQL
事务:
- 原子性:把多个操作,打包成一个整体
- 一致性:事务执行之前和之后,数据都不能离谱(变化前后能对上号)
- 持久性:事务中做出的修改都会存硬盘
- 隔离性:事务并发执行,涉及到的一些问题
原子性
相比于 MySQL
来说,Redis
的事务就是个弟弟:
- 原子性:
Redis
的事务,到底有没有原子性?存在争议- 最原本的含义是把多个操作打包到一起,要么都执行,要么都不执行
Redis
做到了上述的含义,但是MySQL
这里的原子性走的更远MySQL
也是把多个操作打包到一起,要么全都执行成功 ,要么都不执行。如果事务中有操作执行失败的,就要进行回滚,把中间已经执行的操作,全部回退Redis
事务中的如干操作,要是有失败的,无所谓。
Redis
把多个操作打包到一起执行,已经可以称为是原子性了,只是 MySQL
标杆,提高了"原子性 "门槛,这就使人们谈到原子性的时候,更多的想到的是 MySQL
这样带回滚的原子性
所以,一般说到 Redis
的事务有没有原子性,更多的倾向于没有(或者弱化的原子性)
一致性
Redis
没有约束,也没有回滚机制,事务执行过程中如果某个修改操作出现失败,就可能引起不一致的情况
- 所以
Redis
事务不具备一致性
持久性
reids
本身就是内存数据库,数据是存储在内存中的,虽然 Redis
也有持久化机制(AOF
),但这里的持久化机制和事务没有什么关系
- 像
MySQL
那边,事务百分百有持久性,Redis
这边把持久化机制关了。这是不一样的 - 所以
Redis
事务不具备持久性
隔离性
Redis
是一个单线程模型的服务器程序,所有的请求/事务都是"串行"执行的。而谈到隔离性,都是并发执行才会涉及到的
- 所以
Redis
事务不涉及隔离性
优势
Redis
事务,主要的意义就是为了"打包",避免其他客户端的命令,插队插到中间
Redis
中实现事务,是引入了一个队列(每个客户端都有一个)- 开启事务的时候,此时客户端输入的命令,就会发给服务器,并且进入这个队列中(而不是立即执行)。当遇到了"执行事务 "命令的时候,此时就会把队列中的这些任务都按顺序依次执行
- "按顺序依次执行 "是在
Redis
主线程中完成的,主线程会把事务中的操作都执行完,再处理别的客户端
- "按顺序依次执行 "是在
与 MySQL 对比
Redis
的事务为什么就设计的这么简单,而不设计成和 MySQL
一样强大呢?
MySQL
的事务,在背后付出了很大的代价- 空间上,要花费更多的空间来存储更多的数据(实现回滚,就要额外开辟空间去存储必要的日志...)
- 时间上,也要有更大的执行开销 (需要做更多额外的动作)
正是因为 MySQL
上述的问题,才有 Redis
上场的机会(简单高效的优势)
用处
什么时候需要用到 Redis
事务呢?
- 如果我们需要把多个操作打包进行操作,使用事务是比较合适的
比如一个商品秒杀出售场景 :
一个货品 A,进行秒杀出售,市场火爆。此时最重要的就是不能出现"超卖"的情况(超卖:放货 5000 台,卖出了 5001 台)
一个典型的程序写法:
获取仓库中剩余的商品个数
if(个数 > 0) {
下单成功;
个数--;
}
- 如果不加上任何限制,就可能会存在"线程安全"问题
- 所以我们得让
下单成功
和个数--
这两个操作是原子的- 以前在多线程中,是通过加锁的方式,来避免插队
- 在
Redis
中,就直接使用事务即可
使用事务之后的写法:
redis
开启事务
get count
if count > 0
decr count
执行事务
- 在
redis
接收到命令的时候,不会立即执行,只会将其按顺序放在队列中。当收到"执行事务 "操作的时候,才会开始按顺序执行命令 - 第二个客户端的"执行事务 "命令发过来之后,服务器才真正执行第二个事务中的内容。此时第一个事务执行命令已经运行过了,此时第二个事务
get
到的count
就已经是第一个事务自减之后的结果了
这个场景中,没加锁,也能解决上述"超卖"问题
redis
命令里能进行条件判定吗?
redis
原生命令中确实没有这种条件判断定,但是redis
支持lua
脚本
lua
是另外一种编程语言,特点是小巧,很多程序都可以内嵌lua
语言,从而去执行其他的语言- 通过
lua
脚本,就能实现上述的"条件判定",并且也和事务一样是打包批量执行的lua
脚本的实现方式,是redis
事务的进阶版本
确实,redis
的事务的应用场景,没有 MySQL
的事务那么多(有点鸡肋的感觉)。redis
如果是按照集群模式部署,就不支持事务
事务相关命令
开启事务------MULTI
(猫体,不是马体)
开启一个事务,执行成功返回 OK
- 此时只是在服务器的事务队列中,保存了上述请求,并没有真正执行命令
- 此时如果另外开一个客户端,尝试查询这几个
key
,是查询不到的
执行事务------EXEC
真正执行事务
- 此时客户端才会真正把上述操作发给客户端,此时就可以获取到
key
的值了
放弃当前事务------DISCARD
放弃当前事务,此时直接清空事务队列,之前的操作都不会真正执行到
- 前面输入的命令,都被丢弃了
当我们开启事务,并且给服务器发送若干命令之后,此时重启服务器,会怎么样?
- 此时的效果就等同于
discard
- 事务队列终归是内存中的结构,重启之后,自然是没有了
监控某个 key------WATCH
在执行事务的时候,如果某个事务中修改的值,被别的客户端修改了,此时就容易出现数据不一致的问题
作用场景

从时间上来看,客户端 1 是先发送了 set key 222
,客户端 2 是后发送了 set key 333
- 由于客户端 1 中,得是
exec
执行了,才会真正执行set key 222
。这个操作变成了实际上更晚执行的操作,最终key
的值就是222
使用方法
在刚才的场景中,就可以使用 watch
命令来监控这个 key
,看看这个 key
在事务的 multi
和 exec
之间,set key
之后,是否在外部被其他客户端修改了
- 被监控的
key
被修改之后,exec
之后返回值为nil
实现原理
watch
的实现,类似与一个"乐观锁"
- 乐观锁/悲观锁不是指某个具体的锁,而是指的是某一类锁的特征
- 乐观锁:加锁之前,就有一个心理预期,接下来锁的冲突概率较低
- 悲观锁:加锁之前,也有一个心理预期,接下来锁的冲突概率较高
- 冲突:两个线程针对同一个锁加锁,一个能加锁成功,另一个就得阻塞等待
- 锁冲突概率高低,接下来要做的工作是不一样的
乐观锁在 https://yeeear.blog.csdn.net/article/details/141102212 这篇文章中有详细解释
redis
的 watch
就相当于是基于版本号 这样的机制,来实现了"乐观锁 "(就是 CAS
中的 ABA
问题的解决方法 )
watch
必须搭配事务使用,并且必须在multi
之前使用- 如果
watch
的版本号和exec
的版本号- 一致:说明当前
key
在事务开启到最终执行这个过程中,没有被别的客户端修改,于是才能真正进行设置 - 不一致:说明
key
在其他客户端中改过了,因此此处就直接丢弃事务中的操作,exec
返回nil
- 一致:说明当前
所以,watch
本质上就是给 exec
加了一个判定条件
事务总结
Redis
的事务,要比 MySQL
的事务,简单很多
- 原子性:
Redis
的事务,并不支持回滚 - 一致性:
Redis
并不会保证事务执行前后的内容统一 - 持久性:
Redis
主要通过内存来存储数据 - 隔离性:
Redis
自身作为一个单线程的服务器模型,上面处理的请求本质上都是串行执行的
四个关于事务的命令:
- 开启事务------
nulti
- 执行事务------
exec
- 放弃当前事务------
discard
- 监控某个
key
是否被修改------watch