Redis知识6之事务
这个时候其实就容易引起歧义。因此,即使不保证严格的隔离性,至少也要告诉用户,当前的操作可能存在风险。watch 命令就是用来解决上述这个问题的,watch 在该客户端上监控一组具体的 key,看看这个 key 在事务的 multi 和 exec 之间,set key 之后,是否在外部被其他客户端修改了。当开启事务的时候,如果对 watch 的 key 进行修改,就会记录当前 key 的 “版本号”
Redis 的事务和 MySQL 的事务概念上是类似的,都是把一系列操作绑定成一组,让这一组能够批量执行。
一、Redis 的事务和 MySQL 事务的区别
1、MySQL 事务
- 原子性:把多个操作打包成一个整体。(要么全都做,要么都不做)
- ⼀致性:事务执行前和执行后,数据得保持相同。
- 隔离性:事务并发执行涉及到的一些问题(脏读、幻读等)。
- 持久性:事务中做出的修改都会存储到硬盘中。
2、Redis 的事务
弱化的原子性:redis 没有 “回滚机制”,只能做到这些操作 “批量执行”,不能做到 “一个失败就恢复到初始状态”,也就是无法保证执行成功。(网上有的说 Redis 事务有原子性(只是打包一起执行),有的说没有原子性(打包一起执行 + 带有回滚 —— 打包一起正确执行))
不保证⼀致性:不涉及 “约束”,也没有回滚(MySQL 的一致性体现的是运行事务前和运行后,结果都是合理有效的,不会出现中间非法状态)。事务在执行过程中如果某个修改操作出现失败,就可能引起不一致的情况。
不需要隔离性:也没有隔离级别,因为不会并发执行事务(Redis 是一个单线程模型的服务器程序,所有的请求 / 事务都是 “串行” 执行的)。
不需要持久性:Redis 本身就是内存数据库,数据是存储在内存中的。虽然 Redis 也有持久化机制,但是否开启持久化是 redis-server 自己的事情,和事务无关。
Redis 事务本质上是在服务器上搞了⼀个 “事务队列”,每次客户端在事务中进行一个操作,都会把命令先发给服务器,放到 “事务队列” 中,但并不会立即执行,而是在收到 EXEC 命令后,才按照顺序依次执行队列中的所有操作(在 Redis 主线程中完成的,主线程会把事务中的操作都执行完,再处理别的客户端)。
因此,Redis 的事务的功能相比于 MySQL 来说,是弱化很多的。只能保证事务中的这几个操作是 “连续的”,不会被别的客户端 “加塞”,仅此而已。
为什么 Redis 不设计成和 MySQL 一样强大呢?
MySQL 的事务付出了很大的代价:
- 在空间上,需要花费更多的空间来存储更多的数据。
- 在时间上,也要有更大的执行开销。
正是因为 Redis 简单、高效的特点,才能够在分布式系统中弥补一些 MySQL 不擅长的场景。
什么时候需要使用到 Redis 事务呢?
如果我们需要把多个操作打包进行,使用事务是比较合适的。之前在多线程中是通过加锁的方式来避免 “插队” 的,而在 Redis 中直接使用事务即可。
Redis 命令里能够进行类似上图中的条件判断吗?
Redis 原生命令中确实没有这种条件判断,但是 Redis 支持 lua 脚本。通过 lua 脚本就可以实现上述的条件判定,并且也和事务一样是打包批量执行的。
lua 脚本的实现方式是 Redis 事务的进阶版本,此处对 lua 脚本不做过多的讨论。
注意:如果 Redis 是按照集群模式部署的话,是不支持事务的。
二、事务操作
1、MULTI
开启一个事务,执行成功返回 OK。

2、EXEC
真正执行事务。

每次添加一个操作,都会提示 "QUEUED",说明命令已经进入客户端的事务队列中。此时如果另外开一个客户端,再尝试查询这几个 key 对应的数据,是没有结果的:
只有当真正执行 EXEC 的时候,客户端才会真正把上述操作发送给服务器,此时就可以获取到上述 key 的值了。

此时,另一个客户端再次查询结果也是如此。

3、DISCARD
放弃当前事务,此时直接清空事务队列,之前的操作都不会真正执行到。

当开启事务并给服务器发送若干个命令之后,服务器重启,那么此时这个事务怎么办呢?
此时的效果就等同于 discard。
4、WATCH
在执行事务的时候,如果某个事务中修改的值被别的客户端修改了,此时就容易出现数据不一致的问题。
客户端 1 先执行:

客户端 2 再执行:

客户端 1 最后执行:

此时 key 的值是多少呢?
从输入命令的时间看,是客户端 1 先执行的 set key 222,客户端 2 后执行的 set key 333。但是从实际的执行时间来看,是客户端 2 先执行的,客户端 1 后执行的。
由于客户端 1 得是 exec 执行了,才会真正执行 set key 222,所以这个操作实际上更晚执行,最终值就是 222.
这个时候其实就容易引起歧义。因此,即使不保证严格的隔离性,至少也要告诉用户,当前的操作可能存在风险。watch 命令就是用来解决上述这个问题的,watch 在该客户端上监控一组具体的 key,看看这个 key 在事务的 multi 和 exec 之间,set key 之后,是否在外部被其他客户端修改了。
当开启事务的时候,如果对 watch 的 key 进行修改,就会记录当前 key 的 “版本号”(版本号可以理解成一个整数,每次修改都会使版本变大,服务器来维护每个 key 的版本号情况)
在真正提交事务的时候,如果发现当前服务器上的 key 的版本号已经超过了事务开始时的版本号,就会让事务执行失败(事务中的所有操作都不执行)。
客户端 1 先执行:

watch 本质上是给 exec 加一个判定条件。
key 进行修改,从服务器获取 key 的版本号是 0,记录 key 的版本号(还没真的修改,版本号不变)
这里只是入队列,但是不提交事务执行。
客户端 2 再执行:

修改成功,使服务器端的 key 的版本号 0 -> 1
客户端 1 最后执行:

exec 在执行上述事务中的命令时,此处就会做出判定。对比版本发现客户端的 key 的版本号是 0,服务器上的版本号是 1,版本不一致,说明有其他客户端在事务中间修改了 key,说明事务被取消了,于是真正执行 set key 222 的时候就没有真正执行。
客户端 2 执行:

(1)watch 的实现原理
watch 的实现类似于一个 “乐观锁”(不是指某个具体的锁,而指的是某一类锁的特性)。
乐观锁(成本低):加锁之前就有一个心理预期,预期接下来锁冲突的概率比较低。
悲观锁(成本高):加锁之前就有一个心理预期,预期接下来锁冲突(两个线程针对同一个锁加锁,一个能加锁成功,另一个就得阻塞等待)的概率比较高。
锁冲突概率高和冲突概率低,意味着接下来要做的工作是不一样的。
5、UNWATCH
取消对 key 的监控,相当于 WATCH 的逆操作。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)