一、事务特性(ACID):

原子性(Atomicity)

        指事务内所有操作要么一起执行成功,要么都一起失败(或者说是回滚);如事务经典转账案例:A给B转账,A把钱扣了,但B没有收到;可见这种错误是不能接受的,最终会回滚,这也是原子性的重要性。

一致性(Consistency)

        指事务执行前后的状态一致,如事务经典转账案例:A给B互相转账,不管怎么转,最终两者钱的总和还是不变;

持久性(Durability)

        指事务一旦提交,数据就已经永久保存了,不能再回滚;

隔离性(Isolation)

        指多个并发事务之间的操作互不干扰,但是事务的并发可能会导致数据脏读、不可重复读、幻读问题,根据业务情况,采用事务隔离级别进行对应数据读问题处理。

二、四种并发一致性问题:

丢失更新(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

三、事务隔离级别:

读未提交(Read uncommitted)

        指一个事务读取到其他未提交事务的数据。可能导致数据脏读。

        转账案例:A正在给B转账,本来转的1000,A多输入了个0,变成10000,但此事务还未提交,但此时B查询到转入的是10000,但A取消事务回滚之后,B又查询不到转入的数据。这种情况就是脏读。

这个隔离级别可以阻止:丢失更新。

读已提交(Read committed)

        指一个事务只能读取到其他事务已提交的数据,从而解决了脏读的问题。但可能导致数据不可重复读。

        转账案例:A要给B转账1000,A先查看了一下余额,有1000,然后开始给B转钱,但此时A家里电费通过开启的自动缴费功能,自动从A账户扣除200缴纳电费,并提交;当A转账准备提交,再次确认余额时,钱少了200。这样就导致同一个事务中多次查询的结果不一致,这种情况就是不可重复读。

这个隔离级别可以阻止:丢失更新 + 脏读。

可重复读(Repeatable read)

        指事务只要一开启,就不允许其他事务进行修改操作,从而解决了不可重复读问题。一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。或者简单来说,事务在执行期间看到的数据前后是一致的。但可能导致数据幻读。

        转账案例:A经常给B转账,到年底了,需要查账,然后开启了一个事务进行查询统计,刚开始查询只是10条转账记录,正准备统计时,因为紧急情况A需要给B转一笔钱应急,从而新增了一条新记录,并提交;而查账事务正在统计中,最后发现转账额和看到的10条转账记录不匹配。这种情况就是幻读。

这个隔离级别可以阻止:丢失更新 + 脏读 + 不可重复读。

序列化(Serializable )

        顾名思义,强制事务串行执行,对于同一行记录,“写” 会加 “写锁”,“读” 会加 “读锁”,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。这样多个事务互不干扰,不会出现并发一致性问题这样就解决了幻读的问题,但是这种级别的并发性能不高,非特殊需求,这种级别一般不用。

这个隔离级别可以阻止:丢失更新 + 脏读 + 不可重复读 + 幻读。

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_数据库

四、举例说明 

 1.丢失更新 Last To Modify
丢失更新非常好理解,简单来说其就是一个事务的更新操作会被另一个事务的更新操作所覆盖,从而导致数据的不一致。
举个例子:
1)事务 T1 将行记录 r 更新为 v1,但是事务 T1 并未提交
2)与此同时,事务 T2 将行记录 r 更新为 v2,事务 T2 未提交
3)事务 T1 提交
4)事务 T2 提交
如下图所示,显然,事务 T1 丢失了自己的修改。

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_02

 但是,事实上,这种情况准确来讲并不会发生。因为我们说过对于行进行更新操作的时候,需要对行或其他粗粒度级别的对象加锁,因此当事务 T1 修改行 r 但是没提交的时候,事务 T2 对行 r 进行更新操作的时候是会被阻塞住的,直到事务 T1 提交释放锁。
所以,从数据库层面来讲,数据库本身是可以帮助我们阻止丢失更新问题的发生的。
不过,在真实的开发环境中,我们还经常会遇到逻辑意义上的丢失更新。举个例子:
1)事务 T1 查询一行数据 r,放入本地内存,并显示给一个用户 User1
2)事务 T2 也查询该行数据,并将取得的数据显示给另一个用户 User2
3)User1 修改了行记录 r 为 v1,更新数据库并提交
4)User2 修改了行记录 r 为 v2,更新数据库并提交
显然,最终这行记录的值是 v2,User1 的更新操作被 User2 覆盖掉了,丢失了他的修改。

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_03

 可能还是云里雾里,我来举个更现实点的例子吧,一个部门共同查看一个在线文档,员工 A 发现自己的性别信息有误,于是将其从 “女” 改成了 “男”,就在这时,HR 也发现了员工 A 的部门信息有误,于是将其从 ”测试“ 改成了 ”开发“,然后,员工 A 和 HR 同时点了提交,但是 HR 的网络稍微慢一点,再次刷新,员工 A 就会发现,我的性别怎么还是 ”女“?

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_不可重复读_04

2.脏读 Dirty Read
所谓脏读,就是说一个事务读到了另外一个事务中的 "脏数据",脏数据就是指事务未提交的数据。
如下图所示,在事务并没有提交的前提下,事务 T1 中的两次 SELECT 操作取得了不同的结果:

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_05

注意,如果想要再现脏读这种情况,需要把隔离级别调整在 Read UnCommitted(读取未提交)。所以事实上脏读这种情况基本不会发生,因为现在大部分数据库的隔离级别都至少设置成 READ COMMITTED。
3.不可重复读 Unrepeatable read
不可重复读是指在一个事务内多次读取同一数据集合。在这个事务还没有结束时,另外一个事务也访问该同一数据集合,并做了一些修改操作。因此,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。
举个例子:事务 T1 读取一行数据 r,T2 将该行数据修改成了 v1。如果 T1 再次读取这行数据,此时读取的结果和第一次读取的结果是不同的。

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql事务的隔离级别的实现方式_06

不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的却是已经提交的数据,但是其违反了事务一致性的要求。
4.幻读 Phantom Read
幻读本质上是属于不可重复读的一种情况,区别在于,不可重复读主要是针对数据的更新(即事务的两次读取结果值不一样),而幻读主要是针对数据的增加或减少(即事务的两次读取结果返回的数量不一样)。
举个例子:事务 T1 读取某个范围的数据,事务 T2 在这个范围内插入了一些新的数据,然后 T1 再次读取这个范围的数据,此时读取的结果比第一次读取的结果返回的记录数要多。

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_数据_07

五、四种隔离级别的具体实现

读取未提交和可串行化的实现没什么好说的,一个是啥也不干,一个是直接无脑加锁避开并行化 让你啥也干不成。
重头戏就是读取已提交和可重复读是如何实现的。这就是我们要说的 MVCC 了,也就是面试中的超级高频题。
我先来简单说一下,对于这两个隔离级别,数据库会为每个事务创建一个视图 (ReadView),访问的时候以视图的逻辑结果为准:
在 “读取已提交” 隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的;
在“可重复读”隔离级别下,这个视图是在事务启动时就创建的,整个事务存在期间都用这个视图(这就是为什么说在可重复读隔离级别下,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的)。
那么问题了就来了,已经执行了这么多的操作,事务该如何重新回到之前视图记录的状态?数据库会通过某种手段记录这之间执行的种种操作吗?
这就是 undo log 版本链做的事

undo log 版本链
在 MySQL 中,每条记录在更新的时候都会同时记录一条回滚操作(也就是 undo log),当前记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
简单理解,undo log 就是每次操作的反向操作,比如比如当前事务执行了一个插入 id = 100 的记录的操作,那么 undo log 中存储的就是删除 id = 100 的记录的操作。
也就是说,B+索引树上对应的记录只会有一个最新版本,但是InnoDB可以根据undo log得到数据的历史版本。同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_不可重复读_08

那么,还有个问题,undo log 是如何和某条行记录产生联系的呢?换句话说,我怎么能通过这条行记录找到它拥有的 undo log 呢?
具体来说,InnoDB 存储引擎中每条行记录其实都拥有两个隐藏的字段:trx_id 和 roll_pointer
trx_id 就是最近更新这条行记录的事务 ID
roll_pointer 就是指向之前生成的 undo log 的指针
掏出我们的 user 表,来举个例子,假设 id = 100 的事务 A 插入一条行记录(id = 1, username = "Jack", age = 18),那么,这行记录的两个隐藏字段 trx_id = 100 和 roll_pointer 指向一个空的 undo log,因为在这之前并没有事务操作 id = 1 的这行记录。如图所示:

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql事务的隔离级别的实现方式_09

然后,id = 200 的事务 B 修改了这条行记录,把 age 从 18 修改成了 20,于是,这条行记录的 trx_id 就变成了 200,rooll_pointer 就指向事务 A 生成的 undo log :

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql事务的隔离级别的实现方式_10

接着,id = 300 的事务 C 再次修改了这条行记录,把 age 从 20 修改成了 30,如下图:

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_11

可以看到,每次修改行记录都会更新 trx_id 和 roll_pointer 这两个隐藏字段,之前的多个数据快照对应的 undo log 会通过 roll_pointer 指针串联起来,从而形成一个版本链
那么问题又来了,一个记录会被一堆事务进行修改,一个记录上会存在许许多多的 undo log,那么对于其中某一个事务来说,它能看见哪些 undo log?或者说,对于其中某一个事务来说,它能够根据哪些 undo log 执行回滚操作?
让我们来详细解释一下这个视图(ReadView)机制
ReadView 机制
ReadView 机制就是用来判断当前事务能够看见哪些版本的,一个 ReadView 主要包含如下几个部分:
m_ids:生成 ReadView 时有哪些事务在执行但是还没提交的(称为 “活跃事务”),这些活跃事务的 id 就存在这个字段里
min_trx_id:m_ids 里最小的值
max_trx_id:生成 ReadView 时 InnoDB 将分配给下一个事务的 ID 的值(事务 ID 是递增分配的,越后面申请的事务 ID 越大)
creator_trx_id:当前创建 ReadView 事务的 ID
接下来,再掏出 user 表,通过一个例子来理解下 ReaView 机制是如何做到判断当前事务能够看见哪些版本的:
假设表中已经被之前的事务 A(id = 100)插入了一条行记录(id = 1, username = "Jack", age = 18),如图所示:

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_12

接下来,有两个事务 B(id = 200) 和 C(id = 300)过来并发执行,事务 B 想要更新(update)这行 id = 1 的记录,而事务 C(select)想要查询这行数据,这两个事务都执行了相应的操作但是还没有进行提交:

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_数据_13

如果现在事务 B 开启了一个 ReadView,在这个 ReadView 里面:

  • m_ids 就包含了当前的活跃事务的 id,即事务 B 和事务 C 这两个 id,200 和 300
  • min_trx_id 就是 200
  • max_trx_id 是下一个能够分配的事务的 id,那就是 301
  • creator_trx_id 是当前创建 ReadView 事务 B 的 id 200

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_数据库_14

现在事务 B 进行第一次查询(select 操作不会生成 undo log 的哈),会把这行记录的隐藏字段 trx_id 和 ReadView 的 min_trx_id 进行下判断,此时,发现 trx_id 是 100,小于 ReadView 里的 min_trx_id(200),这说明在事务 B 开始之前,修改这行记录的事务 A 已经提交了,所以开始于事务 A 提交之后的事务 B、是可以查到事务 A 对这行记录的更新的

row.trx_id < ReadView.min_trx_id

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_数据_15

接着事务 C 过来修改这行记录,把 age = 18 改成了 age = 20,所以这行记录的 trx_id 就变成了 300,同时 roll_pointer 指向了事务 C 修改之前生成的 undo log:

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_不可重复读_16

那这个时候事务 B 再次进行查询操作,会发现这行记录的 trx_id(300)大于 ReadView 的 min_trx_id(200),并且小于 max_trx_id(301)

row.trx_id > ReadView.min_trx_id && row.trx_id < max_trx_id

这说明一个问题,就是更新这行记录的事务很有可能也存在于 ReadView 的 m_ids(活跃事务)中。所以事务 B 会去判断下 ReadView 的 m_ids 里面是否存在 trx_id = 300 的事务,显然是存在的,这就表示这个 id = 300 的事务是跟自己(事务 B)在同一时间段并发执行的事务,也就说明这行 age = 20 的记录事务 B 是不能查询到的。

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_17

既然无法查询,那该咋整?事务 B 这次的查询操作能够查到啥呢?没错,undo log 版本链

这时事务 B 就会顺着这行记录的 roll_pointer 指针往下找,就会找到最近的一条 trx_id = 100 的 undo log,而自己的 id 是 200,即说明这个 trx_id = 100 的 undo log 版本必然是在事务 B 开启之前就已经提交的了。所以事务 B 的这次查询操作读到的就是这个版本的数据即 age = 18。

通过上述的例子,我们得出的结论是,通过 undo log 版本链和 ReadView 机制,可以保证一个事务不会读到并发执行的另一个事务的更新。

那自己修改的值,自己能不能读到呢?

这当然是废话,肯定可以读到呀。上面的例子我们只涉及到了 ReadView 中的前三个字段,而 creator_trx_id 就与自己读自己的修改有关,所以这里还是图解出来让大家更进一步理解下 ReadView 机制:

假设事务 C 的修改已经提交了,然后事务 B 更新了这行记录,把 age = 20 改成了 age = 66,如下图所示:

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_18

然后,事务 B 再来查询这条记录,发现 trx_id = 200 与 ReadView 里的 creator_trx_id = 200 一样,这就说明这是我自己刚刚修改的啊,当然可以被查询到。

row.trx_id = ReadView.creator_trx_id

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_数据库_19

那如果在事务 B 的执行期间,突然开了一个 id = 500 的事务 D,然后更新了这行记录的 age = 88 并且还提交了,然后事务 B 再去读这行记录,能读到吗?

mysql事务的隔离级别的实现方式 mysql事务的隔离级别举例_mysql_20

答案是不能的。

因为这个时候事务 B 再去查询这行记录,就会发现 trx_id = 500 大于 ReadView 中的 max_trx_id = 301,这说明事务 B 执行期间,有另外一个事务更新了数据,所以不能查询到另外一个事务的更新。

row.trx_id > ReadView.max_trx_id

那通过上述的例子,我们得出的结论是,通过 undo log 版本链和 ReadView 机制,可以保证一个事务只可以读到该事务自己修改的数据或该事务开始之前的数据