在上一讲中,我们已经学习了数据库恢复技术,从这讲开始我们将围绕并发控制技术这个主题进行分析。
我们都知道,目前千万级以上用户流量的数据库并发访问,类似于 12306 网站的购票系统,已经成为互联网公司的普遍现象。实现这样的高并发系统,在数据库层面,无论是实现主从复制的读写分离,还是实现分库分表技术的水平扩展,都需要一个前提保证:单数据库的并发事务执行不会出现数据的读写异常。
这讲我们主要讲解并发事务下,容易产生的 5 种常见数据读写异常,以及如何才能避免这些异常。
多个事务的执行方式
多个用户的读写请求对应到数据库里面就是多个事务,多个事务的执行一般有三种方式:完全串行、并发执行、并行执行。
1.完全串行
完全串行比较容易理解,当同时有多个事务执行时,这些事务按照串行的方式一个接一个顺序执行。这意味着,任意时刻只有一个事务运行,其他事务必须要等待这个事务执行完毕之后才能开始。
如下图所示,我们假设一个虚拟抢票系统,用户“购票”事务包括三个步骤:创建订单(A)、扣减账户余额(B)、扣减总库存(C)。当三个用户同时“购票”时,总的执行顺序是:用户 1 创建订单 → 用户 1 扣减账户余额 → 扣减总库存 → 用户 2 创建订单 → 用户 2 扣减账户余额 → 扣减总库存 → 用户 3 创建订单 → 用户 3 扣减账户余额 → 扣减总库存。

完全串行执行顺序示意图
完全串行的方式,事务之间肯定不会发生冲突和数据异常,但是执行效率太低,因此一般不使用这种方式。
2.并发执行
并发执行的特点是,不存在冲突的操作可以同时执行,存在冲突的操作通过加锁的方式串行执行。还是以上面的虚拟抢票系统为例,用户“购票”事务包括三个步骤:创建订单(A)、扣减账户余额(B)、扣减总库存(C)。三个用户对应的三个事务操作,用户创建订单、扣减账户余额这两个步骤不存在冲突,是可以同时操作的,只有扣减总库存步骤事务间有相互的依赖关系,需要依次串行执行库存扣减才能保证数据的正确性。
这里的同时操作,是指通过单个 CPU 时间片轮流切换来同时处理多个事务的多个操作,这样可以减少 CPU 的空闲时间,提高CPU的利用率。
如下图所示,三个事务并发执行时总的顺序是:先同时执行用户 1 创建订单、用户 2 创建订单、用户 3 创建订单三个操作步骤;执行完之后,再同时执行用户 1 扣减账户余额、用户 2 扣减账户余额、用户 3 扣减账户余额这三个步骤;接着依次按顺序执行事务 1 的扣减总库存、事务 2 的扣减总库存、事务 3 的扣减总库存。

并发执行顺序示意图
这样的执行顺序比上面的完全串行执行有一些好处,提高了整体的并发度和执行效率。
3.并行执行
并行执行一般在多核服务器中,每个 CPU 分别处理一个事务,多个事务可以同时并行执行,这是并行度和效率最高的方式,但是由于受制于硬件条件的限制,事务的控制也更加复杂,所以目前还不是主流。
我们下面介绍的,都是考虑在单 CPU 环境下,多个事务并发执行的时候,可能出现的 5 种常见的数据读写异常。
并发读写异常(不一致)分类
以一个虚拟购票系统为例,当同一时刻有成百上千的人购买车票时,可能出现的 5 种异常:
丢失更新
脏读
不能重复读
幻读
脏写
1.丢失更新(Lost Update)
丢失更新,是指后一个事务的更新操作覆盖了前一个事务的更新操作,导致前一个事务的更新操作丢失了。出现丢失更新的执行步骤如下:

(1)事务 A 查询获取数据库中的所剩总的余票个数,假设有 100 张,从数据库中读取这个值后加载到内存中。
(2)事务 B 正好也在查询余票数,因此事务 B 几乎跟事务 A 同时也获取到余票数 100,并将其加载到内存中。
(3)接着事务 A 在内存中对余票数进行扣减操作,100 减 1 之后,内存中余票值变更为 99。
(4)事务 B 紧随其后,也变更为 99。这个时候事务 A 和事务 B 在内存中的余票数都修改为 99。
(5)事务 A 将修改后的数值 99 写入数据库物理磁盘中。
(6)事务 B 同样将修改后的数值 99 写入到数据库物理磁盘中。这个时候,错误就发生了,两个事务 A、B 都进行了扣减票数的操作,但是最终数据库中的数值却只扣减了一次。
这个例子中模拟了丢失更新所导致的严重后果,实际卖出了两张票,库存里最终只扣减了一张,出现了“超卖”现象,这在电商系统中是绝不允许发生的。
2.脏写(Dirty Write)
脏写,是指一个事务的回滚操作不仅还原掉了自己之前的数据修改,也把另外一个事务提交的数据给回滚掉了。执行步骤如下图所示:

(1)事务 A 查询获取数据库中的所剩的总的余票个数,假设有 100 张,从数据库中读取这个值后加载到内存中。
(2)事务 A 修改这个值为 99,并将 99 这个新值写入数据库中。此时数据库中票被扣减了一次。
(3)接着,事务 B 读取到修改后的票余额 99,也开始进行扣减操作,将 99 变为 98,并写回数据库中。
(4)事务 A 此时突然发现自己之前的修改操作有错误,开始进行数据回滚,撤销之前的 99,还原回初始值 100。
当事务 A 回滚回 100 之后,事务 B 在其中扣减的操作也一并被还原了。导致事务 B 提交的数据丢失。所以对于事务 A 而言,回滚掉了不是自己修改的数据,也就是在事务 A 上发生了脏写现象。
3.脏读(Dirty Read)
脏读,指一个事务读取到另外一个事务临时写的中间状态的数据,或者读到了另外一个事务写的将来可能会被撤销的数据,如下图所示:

(1)事务 A 查询获取数据库中的所剩的总的余票个数,假设有 100 张,从数据库中读取这个值后加载到内存中。
(2)事务 B 正好也在查询余票数,因此事务 B 几乎和事务 A 同时也获取到余票数 100,并将其加载到内存中。
(3)接着,事务 A 在内存中对余票数进行扣减操作,100 减 1 之后,内存中余票值变更为 99。
这三个步骤和上面的丢失更新操作是一模一样的,下面的操作发生了一些变化。
(4)事务 A 直接将修改的值 99 写入数据库中。
(5)事务 B 此时正好又重新读取数据库中的余票数,这次读取发现值是 99,事务 B 以为 99 是最新的修改后正确的值。
(6)不料在事务 B 读取完之后,事务 A 发现上一步中修改的 99 的操作不正确,进行了事务的撤销操作,将 99 还原回 100 的值。
由于事务 A 已经将值还原回原始值 100,正确的数据应该是 100,可是事务 B 一直使用的是中间临时的数据 99。所以,事务 B 读取的是脏数据,这种情况也称为脏读。
4.不能重复读(Non-Repeatable Read)
不能重复读是指,某事务读取数据之后,另外一个事务执行了修改操作,导致再次读取之前的数据时,无法复现之前的读取结果。两事务产生不可重复读的步骤为:先读 - 后更新 - 再读,如下图所示:

事务 B 第一次读取余票数结果为 100,中间这段时间内,事务 B 没有做任何操作,而再次读取余票数的时候,发现已经变为了 99,事务两次读取的结果不一致,产生了不能重复读异常。
5.幻读(Phantom Row)
幻读跟不可重复读类似,可以看成是不可重复读的特例,幻读具体分为两种情况。
情况一:先读 - 后删除 - 再读
这一情况具体的步骤如下图所示:

同上面的不可重复读操作类似,事务 B 先读取满足“购买去东北的火车票”条件的用户数是 100,事务 A 在事务 B 读取之后删除了其中一条记录,变为了 99 个用户。当事务 B 再次按照这个条件来读取的时候发现,满足条件结果记录数变为了 99,和先前读取的结果产生了矛盾,好像一种幻影现象(Phantom Row),因此把产生这种现象的操作称为幻读。
情况二:先读 - 后插入 - 再读
这种情况和情况一的事务执行步骤完全相同,不同之处是事务 A 在期间执行的不是删除操作,而是插入操作,最后导致的幻象是一样的。
五种数据读写异常产生的原因是什么呢?
事务间的读读操作肯定不会产生异常,异常要么发生在读写之间,要么发生在写写之间。上面分析的五种数据读写异常可以分成两类:一类是读写相关的异常,一类是写写相关的异常。

异常之所以产生,根本原因就是并发操作破坏了事务的隔离性。
并发操作可以带来执行效率的提升,但是也会破坏事务之间的隔离性,影响数据的正确性,因此需要在效率和正确性之间取得一种平衡。
至于如何取得平衡,这就引入了事务不同隔离级别的区分,这一部分会在下一讲重点讲解。
总结
这一讲我们详细地介绍了五种常见的事务并发读写异常。相信你仔细看完,一定会有所收获,是不是之前很长时间搞不懂的、背不下来的内容,已经都明白了呢?
看到这么多的读写异常,有的同学可能感到很担心,万一我们平时开发中遇到这些异常怎么办?
其实你一点也不用担心,在实际的 MySQL 开发中,并不会发生脏读、脏写、丢失更新、不能重复读这四种读写异常,因为 MySQL 数据库默认的 InnoDB 存储引擎已经采用了默认的 RR(可重复读)隔离级别,这种隔离级别是可以有效避免上述四种异常的。
至于幻读这种异常情况,在 MySQL 中实际是采用 MVCC 和间隙锁来解决的。所以你大可放心使用 MySQL,不用担心各种数据异常的产生。
这里给你留一个思考题:事务的隔离性和隔离级别有什么区别?欢迎在评论区和我交流。
关于 MySQL 中事务的隔离级别和 Spring 中事务注解等内容,我会在下一讲“Spring 中的事务隔离级别和传播机制”中详细讲解,下次课不见不散!

相关文章推荐:
01 | 如何实现自我基础设施新重建-从掌握一致性与分布式事务开始
03 | 分布式事务的产生原因及常见的解决方案(NewSQL、Distributed SQL等)
09 | 概念辨析:分布式系统中的一致性和ACID中一致性概念异同
10 | 从乐观的思路(OCC、MVCC)来看并发控制的技术实现




