何为事务? 一言蔽之,事务是逻辑上的一组操作,要么都执行,要么都不执行。 >
- 原子性 (
Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;- 一致性 (
Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;- 隔离性 (
Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;- 持久性 (
Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
周志明的软件架构课软件架构分布式系统基础设施架构演进单体架构_SOA 架构微服务_云原生-极客时间
并发事务的问题
- 丢失修改: 在事务 1 读取一个数据时,另外事务 2 也访问了该数据,那么在事务 1 中修改了这个数据后,事务 2 也修改了这个数据。这样事务 1 的修改结果就被丢失,因此称为丢失修改。
- **例:**事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20。事务 1 先修改 A=A-1,事务 2 后来也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失
- 脏读: 事务 2 读取了事务 1 未提交的数据
- **例:**事务 1 读取某表中的数据 A=20 并修改 A=A-1,事务 2 读取到 A = 19,事务 1 回滚导致对 A 的修改并未提交到数据库, A 的值还是 20
- 不可重复读: 事务 1 多次读同一数据。在事务 1 还没有结束时,事务 2 也访问该数据。那么,在事务 1 中的两次读数据之间,由于事务 2 的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
- **例:**事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 再次读取 A =19,此时读取的结果和第一次读取的结果不同
- 幻读: 幻读与不可重复读类似。它发生在一个事务读取了几行数据,接着另一个并发事务插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
- **例:**事务 2 读取某个范围的数据,事务 1 在这个范围插入了新的数据,事务 2 再次读取这个范围的数据发现相比于第一次读取的结果多了新的数据
并发事务控制方式
MySQL 中并发事务的控制方式无非就两种:锁 和 MVCC。锁可以看作是悲观控制的模式,多版本并发控制可以看作是乐观控制的模式。
InnoDB 存储引擎对 MVCC 的实现 | JavaGuide
锁 控制方式下会通过锁来显式控制共享资源而不是通过调度手段,MySQL 中主要是通过 读写锁 来实现并发控制。
- 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。 MVCC 是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。
MVCC 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log。
四个事务隔离级别
- MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
- 读已提交和 可重复读 隔离级别是基于 MVCC 实现的,可串行化 隔离级别是通过锁来实现的。
读未提交 (Read Uncommitted)
- 特点:在该隔离级别下,一个事务可以读取另一个事务尚未提交的数据。这就会导致脏读,即一个事务读取到的可能是另一个事务未提交的修改,这些修改可能会被回滚。
- 问题:事务 B 读取到事务 A 修改的值,但事务 A 回滚后,数据实际上没有变化。发生了脏读。
读已提交 (Read Committed)
特点:在该隔离级别下,一个事务只能读取到已提交事务的数据,因此脏读不会发生。
问题:事务 A 在读取两次同一数据时,第二次读取的数据可能会因其他事务的提交而发生变化 ,这就是不可重复读 。
「读已提交」事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致 ,因为可能这期间另外一个事务修改了该记录,并提交了事务。这就是不可重复读的问题
可重复读 (Repeatable Read)
- 特点:在该隔离级别下,事务会看到在事务开始时一致的数据快照,不可重复读的问题被解决。
- 问题:如果事务 A 查询范围的数据(例如某一时间段内的所有交易记录),事务 B插入 的新数据可能会被事务 A 在第二次查询中看到,导致事务 A 两次查询到的数据量不一致,这就是幻读,可重复读仅解决部分幻读。
- 用 Read View 只能保证"快照读"不幻读;一旦事务里出现"当前读"(UPDATE/DELETE/SELECT … FOR UPDATE 等),就会重新加 Next-Key Lock,这时若别的事务新插入的记录落在这个锁范围里,就可能被本事务再次看到,于是出现"部分幻读"。 「可重复读」隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用 这个 Read View ,这样就保证了在事务期间读到的数据都是事务启动前的记录。解决了不可重复读和部分幻读。
串行化 (Serializable)
- 特点:在该隔离级别下,事务是完全隔离、串行执行。其他事务必须等当前事务完成才能开始执行,这避免了所有并发问题(脏读、不可重复读和幻读),但也大大降低了性能。
幻读
- 幻读 VS 不可重复读
幻读重点在于数据是否存在。原本不存在的数据却真实的存在了,这便是幻读。引起幻读的原因在于另一个事务进行了 INSERT 操作。- 不可重复读重点在于数据值是否被改变。在一个事务中对同一条记录进行查询,第一次读取到的数据和第二次读取到的数据不一致,这便是可重复读。引起不可重复读的原因在于另一个事务进行了 UPDATE 或者是 DELETE 操作。
简单来说:幻读是说数据的条数发生了变化,原本不存在的数据存在了。不可重复读是说数据的内容发生了变化,原本存在的数据的内容发生了改变。
可重复读隔离下为什么会产生幻读
在可重复读隔离级别下,普通的查询是快照读 ,是不会看到别的事务插入的数据的,就没有幻读 。因此,幻读在 当前读( select … for update 等语句,使用临键锁**)** 下才会出现。
MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。MySQL 中如何实现可重复读
当隔离级别为可重复读的时候,事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View。也就是说:不管其他事务怎么修改数据,,对于 A 事务而言,它能看到的数据永远都是第一次 SELECT 时看到的数据。这显然不合理,如果其它事务插入了数据,A 事务却只能看到过去的数据,读取不了当前的数据。解决幻读的办法
MySQL 可重复读隔离级别,完全解决幻读了吗? | 小林 coding{#解决幻读的方法}**解决幻读的核心思想就是事务 A 在操作某张表数据的时候,另外事务 B 不允许新增或者删除这张表中的数据。**解决幻读的方式主要有以下几种:
将事务隔离级别调整为
SERIALIZABLE。在可重复读的事务级别下,给事务操作的这张表添加表锁。
在可重复读的事务级别下,给事务操作的这张表添加 临键锁。
其他情况下的幻读
在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成一个数据快照,之后事务 B 向表中新插入了一条 id=5 的记录并提交(此时是当前读)。接着,事务 A 对 id=5 这条记录进行了更新操作(看不见但是能更新),在这个时刻这条新记录的 trx id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
事务 A 先快照读,得到数据量为 3,然后事务 B 插入一条数据并提交事务,事务 A 使用当前读得到的数量就是 4 了,前后数据量不对。
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

