1.1.1. 事务的隔离级别

事务并发可能发生的问题

脏写(Dirty Write)

如果一个事务修改了另一个未提交事务修改过的数据,就意味着发生了「脏写」。

脏写

session A B 各开了一个事务,session a 修改了 name 为张飞,而 session b 修改成了 关羽,然后 session a 提交事务,本来应该成功,但是 session b 执行了回滚,导致数据还原成最开始的样子,对于 session a 就跟没修改过一样。

脏读(Dirty Write)

如果一个事务读取到另一个未提交事务修改过的数据,就意味着发生了「脏读」。 脏读

同样 session a b 都开启了事务,session b 将 name 修改为了 关羽但是未提交,然后 session a 就会读取到关羽这个脏数据。然后 session b 回滚了事务,导致实际上 name 未曾被修改。

不可重复读(Non-Repeatable Read)

如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每次对该数据进行修改,该事务都能读取到最新的值,就意味着发生了「不可重复读」。

不可重复读

同样 session a b,session a 每次都能读取到 session b 修改过数据的的最新值。就意味着发生了「不可重复读」

幻读(Phantom)

如果一个事务根据条件查询出一些数据,而另外一个事务又向该表插入了符合刚才查询条件的数据,导致原来的事务再次根据相同条件查询数据是,会查出刚刚另一个事务插入的数据,则意味着发生了幻读。

幻读

session a 根据条件查询出一条刘备的记录,然后 session b 往这个表中插入了曹操,当 session a 再次根据相同条件查询时,会出现 刘备和曹操 两条记录。

如果 session b 是删除某一条信息,session a 查出来会少一条信息,这个不称之为幻读。 幻读强调一个事务按照某个条件多次查询,后读取到了之前没有得记录。

问题严重排序

脏写 > 脏读 > 不可重复度 > 幻读

四种隔离级别

Read Uncommited:事务之间可以读取到彼此未提交的数据。(该级别的锁会在写操作后立即释放,而不像其他隔离级别在事务提交后释放) Read Commited:该级别将锁的释放时机放在了事务提交之后。即一个事务需要等另一个事务提交后才能读取数据。 Repeatable Read:可以重复读,事务开始时,就不允许其他事务再修改数据。 Serializble:序列化,事务必须串行执行,可以避免脏读、幻读、不可重复读。但是效率低下。

Transaction-Isolation-Matrix

四种隔离级别都不允许脏写。

性能:RU > RC > RR > SE

切换语法:

SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [level];

查询当前隔离级别:

show variable like "%transaction_isolation%";

MVCC 原理

版本链

版本链:对于 InnoDB 的聚簇索引来说,有两个必要的隐藏列。

  • trx_id:每次一个事务对某条聚簇索引记录进行改动时(update、delete、insert)都会把该事务的 「事务 id」赋值给 「trx_id」 隐藏列。
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入 undo 日志中,然后这个列作为指向修改前版本记录的指针。

如现在有这样一张表和记录,并假设它的「事务 id」是 80:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 刘备   ||
+--------+--------+---------+
1 row in set (0.07 sec)

1

假设之后两个「事务id」分别为 100、200 的事务对这条记录进行UPDATE操作,操作流程如下:

1

每次对记录进行改动,都会产生一条 undo 日志,每条 undo 日志都有一个 roll_pointer (最初的 insert 没有这个属性,因为没有更早的记录了)属性,将这些 undo 日志连接起来,形成一个链表。这个链表称之为「版本链」,在这个版本链的头部,记录着当前记录最新的值。

1

ReadView

对于 RU 隔离级别来说,可以读取其他事务未提交的记录,所以直接读取最新的记录就行了。 对于 SERIALIZABLE 隔离级别来说,通过加锁形式访问。 那么对于 RC 和 RR 级别来说 通过 ReadView 来判断版本链中哪些记录对于当前事务来说可见,有 4 个重要内容:

  1. m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的「事务 id」列表。
  2. min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的「事务 id」,即 m_ids 的最小值。
  3. max_trx_id:表示生成 ReadView 时应该分配给下一个事务的 id 值(这个 max_trx_id 并不是 m_ids 的最大值,事务id是递增分配的。比方说现在有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id的值就是 1,max_trx_id的值就是 4。)。
  4. creator_trx_id:表示生成 ReadView 的事务的 「事务 id」(只有在执行 insert update delete 才会为事务分配事务 id,否则一个只读事务的「事务 id」默认为 0)。

有了 ReadView 那么,根据下面的规则来判断某个版本是否对当前事务可见:

  1. 如果被访问的 trx_id = ReadView 中的 creator_trx_id,那么表示,当前事务在访问自己修改过的记录,可以访问
  2. 如果被访问的 trx_id < ReadView 中的 creator_trx_id,那么表示,生成该版本事务在当前事务生成 ReadView 之前就已经被提交了,可以访问。
  3. 如果被访问的 trx_id > ReadView 中的 creator_trx_id,那么表示,生成该版本事务在当前事务生成 ReadView 后才开启,不可以访问。
  4. 如果被访问的版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,就需要判断 trx_id 在不在 m_ids 中,如果在,则表示在 ReadView 创建时该版本的事务还是活跃的,该版本不可以被访问。否则,说明创建 ReadView 时该版本的事务已经被提交了,所以可以访问。
举例说明 ReadView

需要区分两种 RC 和 RR 两种隔离级别。它俩的主要区别是生成 ReadView 的时机不同。

首先,先初始化一个数据:

mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name   | country |
+--------+--------+---------+
|      1 | 刘备   ||
+--------+--------+---------+
1 row in set (0.07 sec)
RC 隔离级别 - 每次读取数据前就会生成一个 ReadView

即:只要在事务中 select 就会生成一个 ReadView。

比方说现在有两个事务 100、200。

# Transaction 100
BEGIN;

UPDATE hero SET name = '关羽' WHERE number = 1;

UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200
BEGIN;

# 更新了一些别的表的记录(只有做修改操作才会单独分配一个「事务 id」)
...

此时的版本链是: 1

好了,此时如果有一个 RC 的事务开始执行:

# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

这个 SELECT1 过程:

  1. 在执行 SELECT 时,会先生成一个 ReadView,其 m_ids 列表为「100,200」,min_trx_id=100,max_trx_id=201,creator_trx_id=0
  2. 然后在版本链中挑选出可见记录,最新的是张飞,版本号为 100,在 m_ids 中,不符合可见要求,然后 roll_pointer 会跳到下一个版本。
  3. 第二个版本是关于,版本号也是 100,跟张飞一样,所以也不符合。
  4. 下个版本是刘备,版本号是 80,小于 min_trx_id 的 100,所以符合要求。所以最终返回给用户的 name 是刘备。

然后我们提交 select1。

# Transaction 100
BEGIN;

UPDATE hero SET name = '关羽' WHERE number = 1;

UPDATE hero SET name = '张飞' WHERE number = 1;

COMMIT;

然后再到 事务id 为 200 的事务中更新一下表 hero 中 number 为 1 的记录:

# Transaction 200
BEGIN;

# 更新了一些别的表的记录
...

UPDATE hero SET name = '赵云' WHERE number = 1;

UPDATE hero SET name = '诸葛亮' WHERE number = 1;

现在版本链就变成这样了: 2

我们在回到刚刚 RC 级别事务中继续查找

# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞'

这个 select2 的执行过程:

  1. 由于是 RC 级别,每次执行 SELECT 时都会重新生成一个 ReadView,该 ReadView 的 m_ids=「200」(事务id 100 已经提交了),min_trx_id 为 200,max_trx_id 为 201,creator_trx_id 为 0。
  2. 从版本链第一条开始查询,最新是诸葛亮,trx_id=200,在 m_ids 中,所以不符合可见性要求,移动到下一个。
  3. 下一个版本是 赵云,同上,继续移动到下一个。
  4. 下一个是 张飞,trx_id=100,小于 min_trx_id ,所以符合要求,返回给用户。

总结:在 RC 隔离级别的事务,每次查询开始时都会生成一个独立的 ReadView。

RR 隔离级别 - 在第一次读取数据时生成一个 ReadView

即:在同一个事务里,只会在第一次使用 select 时 生成 ReadView,后面的都不会在生成了。

比方说现在有两个事务 100、200。

# Transaction 100
BEGIN;

UPDATE hero SET name = '关羽' WHERE number = 1;

UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200
BEGIN;

# 更新了一些别的表的记录(只有做修改操作才会单独分配一个「事务 id」)
...

此时的版本链是: 1

好了,此时如果有一个 RC 的事务开始执行:

# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

这个 SELECT1 过程:

  1. 在执行 SELECT 时,会先生成一个 ReadView,其 m_ids 列表为「100,200」,min_trx_id=100,max_trx_id=201,creator_trx_id=0
  2. 然后在版本链中挑选出可见记录,最新的是张飞,版本号为 100,在 m_ids 中,不符合可见要求,然后 roll_pointer 会跳到下一个版本。
  3. 第二个版本是关于,版本号也是 100,跟张飞一样,所以也不符合。
  4. 下个版本是刘备,版本号是 80,小于 min_trx_id 的 100,所以符合要求。所以最终返回给用户的 name 是刘备。

然后我们提交 select1。

# Transaction 100
BEGIN;

UPDATE hero SET name = '关羽' WHERE number = 1;

UPDATE hero SET name = '张飞' WHERE number = 1;

COMMIT;

然后再到 事务id 为 200 的事务中更新一下表 hero 中 number 为 1 的记录:

# Transaction 200
BEGIN;

# 更新了一些别的表的记录
...

UPDATE hero SET name = '赵云' WHERE number = 1;

UPDATE hero SET name = '诸葛亮' WHERE number = 1;

现在版本链就变成这样了: 2

我们在回到刚刚 RC 级别事务中继续查找

# 使用READ COMMITTED隔离级别的事务
BEGIN;

# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞'

看到这里发现与 RC 隔离级别没什么区别。但是从 SELECT2 这个开始,就不一样了。

#

SELECT2 的执行过程:

  1. 因为当前事务级别是 RR,而之前 SELECT1 已经生成过一次 ReadView 了,所以直接拿来复用。之前的 ReadView 的 m_ids 列表的内容就是 「100, 200」 ,min_trx_id 为 100,max_trx_id 为 201,creator_trx_id 为 0。
  2. 从版本链开始查找第一条数据是 诸葛亮,trx_id=200,在 m_ids 中,不符合可见性要求。
  3. 下一个是,赵云,trx_id=200,同上。
  4. 下一个是,张飞,trx_id=100,也在 m_ids 中,不符合可见性要求。
  5. 下一个是,关于,trx_id=100,同上。
  6. 下一个是,刘备,trx_id=80,小于 min_trx_id。所以符合可见性要求,所以返回给用户。
Copyright © Kagami丶 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 21:47:13

results matching ""

    No results matching ""