目录
1. MySQL和Redis事务的区别
1.1 MySQL的事务
MySQL事务复习:
MySQL数据库⑨_事务(四个属性+回滚提交+隔离级别+MVCC)_mysql 回滚-CSDN博客
- 原子性: 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中如果发生错误,则会自动回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
- 持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
- 隔离性: 数据库允许多个事务同时访问同一份数据,隔离性可以保证多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致。
- 一致性: 在事务开始之前和事务结束以后,数据库的完整型没有被破坏,这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联型以及后续数据库可以自发性地完成预定的工作。
1.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 是按照集群模式部署的话,是不支持事务的。
2. Redis事务操作
2.1 MULTI multi
开启一个事务,执行成功返回 OK。
2.2 EXEC exec
真正执行事务。
每次添加一个操作,都会提示 "QUEUED",说明命令已经进入客户端的事务队列中。此时如果另外开一个客户端,再尝试查询这几个 key 对应的数据,是没有结果的:
只有当真正执行 EXEC 的时候,客户端才会真正把上述操作发送给服务器,此时就可以获取到上述 key 的值了。
此时另一个客户端再次查询结果也是如此。
2.3 DISCARD discard
放弃当前事务,此时直接清空事务队列,之前的操作都不会真正执行到。
当开启事务并给服务器发送若干个命令之后,服务器重启,那么此时这个事务怎么办呢?
此时的效果就等同于 discard。
2.4 WATCH 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 最后执行:
xec 在执行上述事务中的命令时,此处就会做出判定。对比版本发现客户端的 key 的版本号是 0,服务器上的版本号是 1,版本不一致,说明有其他客户端在事务中间修改了 key,说明事务被取消了,于是真正执行 set key 222 的时候就没有真正执行。
客户端 2 执行:
watch 的实现原理:
watch 的实现类似于一个 “乐观锁”(不是指某个具体的锁,而指的是某一类锁的特性)。
- 乐观锁(成本低):加锁之前就有一个心理预期,预期接下来锁冲突的概率比较低。
- 悲观锁(成本高):加锁之前就有一个心理预期,预期接下来锁冲突(两个线程针对同一个锁加锁,一个能加锁成功,另一个就得阻塞等待)的概率比较高。
锁冲突概率高和冲突概率低,意味着接下来要做的工作是不一样的。
C++ Linux 中涉及到的锁 mutex / std::mutex 都是悲观锁,Java synchronized 则是可以在悲观和乐观之间自适应。
3. 事务常见面试题
怎么理解Redis事务?
答:事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。(Redis 事务本质上是在服务器上搞了一个 “事务队列”,每次客户端在事务中进行一个操作,都会把命令先发给服务器,放到 “事务队列” 中,但并不会立即执行,而是在收到 EXEC 命令后,才按照顺序依次执行队列中的所有操作(在 Redis 主线程中完成的,主线程会把事务中的操作都执行完,再处理别的客户端)。)
Redis 事务相关的命令有哪几个?
答:MULTI、EXEC、DISCARD、WATCH。
Redis事务的使用场景有哪些?
答:Redis事务适用于需要将多个操作打包执行的场景。例如,商品抢购活动、用户积分扣除等操作,通过Redis事务可以确保操作的原子性,避免并发问题1。然而,由于Redis事务不支持回滚和持久化保证,因此在需要强一致性保证的场景下,Redis事务可能不是最佳选择2。
Redis事务与MySQL事务的区别有什么?
答:Redis事务相比于MySQL事务,功能较为简单。Redis事务不保证原子性、一致性、隔离性和持久性(ACID特性),而是通过简单的命令打包来避免并发问题。MySQL事务则需要更多的资源来保证这些特性,因此在处理大量数据时可能会更耗时和耗资源1。
本篇完。
下一篇是Redis存储⑪主从复制_分布式系统解决单点问题