这是我参与更文挑战的第2天,活动详情查看:更文挑战
一、前言
事务里,多个 SQL
语句执行,如图:
- 脏读
无论是脏写还是脏读,都是因为一个事务去更新或查询了另外一个还没提交的事务更新过的数据。
因为另外一个事务还没提交,所以他随时可能会反悔会回滚,那么必然导致你更新的数据就没了,或者你之前查询到的数据就没了。
- 不可重复读
针对的是已经提交的事务修改的值,被你事务给读到了,你事务内多次查询,多次读到的是别的已经提交的事务修改过的值,这就导致不可重复读了。
- 幻读
幻读:指的是查询到之前查询没看到过的数据!
一个事务用一样的
SQL
多次查询,结果每次查询都会发现查到一些之前没看到过的数据。
二、隔离级别
SQL
标准中规定了 4 种事务隔离级别:
即:多个事务并发运行的时候,互相是如何隔离的,从而避免一些事务并发问题。
-
read uncommitted
读未提交 -
read committed
读已提交:这个级别在别的事务已经提交之后读到他们修改过的值就可以了,但是别的事务没提交的时候,绝对不会读到人家修改的值。 -
repeatable read
可重复读 -
serializable
串行化
read uncommitted
(RU
) :是不允许发生脏写的
不可能两个事务在没提交的情况下去更新同一行数据的值,但是在这种隔离级别下,可能发生脏读,不可重复读,幻读。
read committed
(RC
) :不会发生脏写和脏读
即:别人事务没提交的情况下修改的值,你是绝对读不到的。
但是,可能会发生不可重复读和幻读问题,因为一旦人家事务修改了值然后提交了,你的事务就会读到。
repeatable read
(RR
) : 可重复读级别
这种级别下,不会发生脏写、脏读和不可重复读的问题。
serializable
: 串行
根本就不允许多个事务并发执行,只能串行起来执行。
修改 MySQL
的默认事务隔离级别:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
复制代码
@Transactional(isolation=Isolation.DEFAULT)
: 默认是 DEFAULT
值,就是 MySQL
默认支持什么隔离级别就是什么隔离级别,MySQL
默认是 RR
级别。
当然可以手动改为 Isolation.READ_UNCOMMITTED
级别,此时就可以读到别人没提交事务修改的值了。
锁机制,解决的是 多个事务同时更新一行数据。
MySQL
实现 MVCC
机制的时候,是基于 undo log
多版本链条 + ReadView
机制,默认是 RR
隔离级别。
依托这套机制实现
RR
级别,除了避免脏写、脏读、不可重复读,还能避免幻读可能。
undo log
版本链的故事:
理解:多个事务串行更新一行数据的时候,
txr_id
和roll_pointer
两个隐藏字段的概念,包括undo log
串联起来的多版本链条的概念。
每条数据都有两个隐藏字段:
-
trx_id
: 最近一次更新这条数据的事务id
-
roll_pointer
: 指向了你更新这个事务之前生成的undo log
- 事务 A,插入一条数据
- 事务 B,修改事务 A对应的数据
- 事务 C,修改事务 B对应的数据
基于 undo log
多版本链条实现 ReadView
机制
执行一个事务,就会生成一个 ReadView
,里面包含 4 个关键东西:
-
m_ids
: 说明哪些事务在MySQL
里执行还没提交 -
min_trx_id
:m_ids
最小的值 -
max_trx_id
:mysql
下一个要生成的事务id
,就是最大事务id
-
creator_trx_id
: 指当前事务id
假设原来数据库就有一行数据,如图:
此时有两个事务并发执行:
-
一个事务 A(
id = 45
) :去读取这行数据 -
一个事务 B(
id = 59
) :去更新这行数据
事务A 开启一个 ReadView
,这个 ReadView
如下:
// 数字均为事务 Id
m_ids = 45, 59
min_trx_oid = 45
max_trx_id = 60
creator_trx_id = 45
复制代码
这时候事务A 第一次查询这行数据: 判断一下当前这行数据的 trx_id
是否小于 ReadView
中的 min_trx_id
。
此时发现
txr_id = 32
,小于ReadView
里的min_trx_id = 45
; 说明事务A开启之前,修改这行数据的事务早就提交了,所以此时可以查到这行数据。
如图:
事务B 把这行数据的值修改为值B,然后这行数据的 txr_id
设置为自己的 id = 59
,同时 roll_pointer
指向了修改之前生成的一个 undo log
,接着这个事务B 就提交了。
如图:
事务A再次查询,发现数据行里的 txr_id = 59
,那么这个 txr_id > min_txr_id = 45
(ReadView
),同时 txr_id < max_trx_id = 60
,说明更新这条数据的事务,很可能就跟自己差不多同时开启的,于是会看一下这个 txr_id = 59
是否在 ReadView
的 m_ids
列表中?确实在 ReadView
的 m_ids
列表里,所以事务A对这行数据是不能查询的!
如图:
既然这行数据不能查询,那就顺着这条数据的 roll_pointer
的 undo log
日志链条往下查找,找到一条 trx_id < min_trx_id = 45
,即 trx_id = 32
,说明这个 undo log
版本必然是在事务A 开启之前就执行且提交了的。
这样,就直接查询那个 undo log
里的值,如图:
这就是
undo log
多版本链条的作用,可以保存一个快照链条,让你可以读到之前的快照值。
三、Read Committed
(RC
) 隔离级别如何基于 ReadView
机制实现?
RC
隔离级别:事务运行期间,只要别的事务修改数据并提交了,就可以读到别人修改的数据。
所以这会发生不可重复读、幻读问题。
当一个事务设置他处于 RC
隔离级别的时候,它是每次发起查询,都重新生成一个 ReadView
。
假设数据库里情况如下:
-
事务
id = 50
已经新增 -
活跃的事务A
id = 60
-
活跃的事务B
id = 70
如图:
事务B 执行了一次 update
操作(还未提交),更新了这条数据,如图:
事务A 发起一次查询操作,此时就会生成一个 ReadView
,ReadView
内容如图:
min_trx_id = 60
max_trx_id = 71
creator_trx_id = 60
复制代码
事务B 提交commit
,那么事务B 不会活跃于数据库了。
按照
RC
隔离级别的定义,事务B 一旦提交了,说明事务A下次再查询,就可以读到事务B修改过的值了。
事务A 再次发起查询,此时会再次生成一个 ReadView
,如图:
此时,数据库内活跃的事务只有事务A了,因此
min_trx_id = 60
、max_trx_id = 71
、m_ids = [60]
发现这条数据的trx_id = 70
,但不在m_ids
中,说明事务B 在生成本次ReadView
之前就已经提交了。
四、Read Repeatable
(RR
)隔离级别如何基于 ReadView
机制实现?
RR
隔离级别下,这个事务读一条数据,无论读多少次,都是一个值,别的事务修改并提交之后,均看不到,这就避免了不可重复读的问题。
同时如果别的事务插入了一些新的数据,同样读取不到,就可以避免幻读问题。
假设数据库里情况如下:
-
事务
id = 50
已经新增 -
活跃的事务A
id = 60
-
活跃的事务B
id = 70
如图:
事务A 发起一次查询操作,第一次查询就会生成一个 ReadView
,可以查询到,如图:
因为这条数据的
trx_id = 50 < min_trx_id
,说明发起查询之前,这条数据的操作已经提交。
min_trx_id = 60
max_trx_id = 71
creator_trx_id = 60
m_ids = [60, 70]
复制代码
事务B 发起更新操作并提交了,此时会修改 trx_id = 70
,同时生成一个 undo log
,事务B结束了,如图:
此时
ReadView
中的m_ids = [60, 70]
。
因为ReadView
一旦生成了就不会改变了,事务B 已经结束了,但是事务A 的ReadView
里还是会有60和70两个事务id
这时候,事务A再次查询,id
为 70的这个事务B还是在运行的,然后由这个事务B 更新了这条数据,所以此时事务A 是不能查询到事务B 更新的这个值的,因此这个时候继续顺着指针往历史版本链条上去找,如图:
不可重复读如何解决?
由于
ReadView
只生成一次,所以事务A 多次读同一个数据,每次读到的都是一样的值,除非是它自己修改了值,否则读到的一直会是一样的值。
幻读如何解决?
由于
ReadView
只生成一次 且 只能读取小于max_trx_id
的事务id
,所以不会出现这个问题。