Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MySQL 事务 #52

Open
zhongdeming428 opened this issue Jul 2, 2022 · 0 comments
Open

MySQL 事务 #52

zhongdeming428 opened this issue Jul 2, 2022 · 0 comments

Comments

@zhongdeming428
Copy link
Owner

数据库中的数据在被更新的时候不总是一次到位的,有些时候我们需要分多步进行操作,比如在转账流程中我们需要先减少支付方账户上一定数目的金额,然后在收款方账户上增加对应数目的金额。

两步操作应该保证要么同时成功要么同时失败,否则会导致总体金额增加或者减少,这就会导致银行亏损。

事务特性

为了避免这个问题,数据库中的数据操作需要保证 ACID 特性,即:

  • A(Atomicity) :数据更新操作的原子性,所有数据更新操作要么全部成功,要么全部失败,不允许部分成功或者部分失败的场景出现。
  • C(Consistency) :数据的一致性,数据操作前后数据的状态必须是一致的,比如在 a 转账给 b 100 元的转账流程中两方金额要么是(x, y),要么是 (x-100, y+100),不允许出现(x, y+100) 或者 (x-100, y) 的情况出现。
  • I(Isolation) :多个操作同时进行时必须保证它们之间不会因为互相干扰而导致数据出现不一致的情况。
  • D(Durability) :数据在更新之后是持久化的,即使出现意外情况也不会丢失

在 MySQL 中,满足以上特性的功能叫做 事务(Transaction)

为了实现上面的四个特性,MySQL 实现了锁机制和 undo log 和 redo log。其中锁机制保证了事务之间的隔离性,即使存在并发也不会导致两个事务之间的数据更新互相影响;undo log 和 redo log 保证了事务的一致性、原子性和隔离性。

四种隔离级别

事务之间实现了互相隔离的特性,但是这个隔离也是有级别的,我们可以设置数据库的默认事务隔离级别,MySQL 有以下这几种隔离级别,从上到下隔离级别逐渐增高:

  1. 读未提交(Read Uncommitted) :前一个事务尚未 commit 的修改可以被后一个事务中的查询语句读到,隔离级别最低,安全性最差,存在“脏读”的问题。
  2. 读已提交(Read Committed) :前一个事务 commit 之后的修改可以被后一个事务中的查询语句读到,没有 commit 的修改不会被读取到,隔离级别较高,安全性一般,存在“不可重复读”的问题。
  3. 可重复读(Repeatable Read) :当前事务开始之后,查询语句中查询到的所有数据都不会再改变,但是查询出来的数量可能会有出入,即所谓的“幻读”问题。隔离级别高,安全性高。
  4. 串行化(Serializable) :不允许事务并行执行,只能串行化执行,所以隔离性最好,不存在之前的各种问题,隔离级别最高、安全性最高,但是性能最差。

MySQL 的默认隔离级别是 RR(Repeatable Read)。

三种问题

前一部分有说到几种隔离级别中分别存在的问题,现在加以解释:

  • 脏读 :事务未提交的数据变更被其他事务读取到,后续如果变更被回滚,则后者事务读取到的数据就是无效的,称之为“脏读”。这一问题一般在 RU 级别出现。
  • 不可重复读 :大型事务内部可能会对同一段数据进行多次读取操作,在读已提交隔离级别中,多次数据读取可能获取到的数据不一致,这就是“不可重复读”的问题。
  • 幻读 :幻读指的是前一次读取还没有存在的数据,在下一次读取的时候就存在了,这样导致的问题是第二次如果是 insert 操作的话,会产生预期之外(预期插入成功)的报错。那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?其实这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。

根据 SQL 标准,这些问题和事务隔离级别之间的关系如下所示:

隔离级别 脏读 不可重复读 幻读
Read Uncommitted
Read Committed
Repeatable Read
Serializable

一定程度上来说,MySQL 通过 MVCC 解决了 RR 隔离级别中的幻读问题。

通过提高隔离级别,可以解决每个隔离级别自身的问题。

比如为了解决脏读问题,可以将隔离级别提高到 RC;为了解决不可重复读问题,将隔离级别提高到 RR,为了解决幻读问题,将隔离级别提高到 S。

但是我们一般不会直接使用 S 级别的隔离,因为性能确实比较差,MySQL 默认使用 RR 隔离级别。这种情况下需要解决幻读问题,就可以采用 MVCC 或者间隙锁和临键锁。

下面实践体验一下三种问题。

创建一个 table:

CREATE TABLE `trx_lock_tab` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `a` int NOT NULL,
  `b` int NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_a` (`a`),
  KEY `idx_b` (`b`)
);

表中存在的数据如下:

image

当前读和快照读

在事务中的查询语句分为两种,分别是当前读和快照读。

当前读的意思是在 select 语句执行的时候先给要查询的记录加上悲观锁,这样可以确保查询出来的数据是最新的,不会被在事务执行期间发生变化。当前读也叫做加锁读或者阻塞读。MySQL 的 RR 隔离级别下,当前读通过间隙锁或者临键锁解决幻读问题。

快照读的意思是在 select 语句执行的时候生成数据快照(实际上是通过 MVCC 的 ReadView 记录了事务的活跃情况),在 RR 隔离级别下第一个 select 语句会生成 ReadView 快照,之后不再生成;在 RC 隔离级别中每一个 select 语句都会生成 ReadView。所以 RC 隔离级别下每次 select 都可能读到的数据不一样(读取了其他事务的已提交修改,不可重复读),RR 隔离级别下的快照读在事务执行过程中读取到的数据始终是一样的,所以说 MySQL RR 隔离级别下的快照读通过 MVCC 解决了幻读问题。

关于 MVCC

MVCC(MultiVersion Concurrency Control) :多版本并发控制。通过数据版本链来解决并发场景下的快照读-写(或者写-快照读)时不可重复读或者幻读的问题,避免了加锁带来的开销,提升了 DB 性能。

MVCC 的基本功能

对于快照读场景(select 语句不加锁),RC 和 RR 隔离级别下需要确保读取到的数据不包含未被事务提交的修改记录(RC)以及确保读取到的数据在事务执行过程中始终一致。MVCC 能够确保这两种隔离级别下的快照读可以满足要求。

MVCC 的基本原理

MVCC 的前提条件:

  1. 事务 id 是递增分配的,并且只有在遇到 insert、delete、update 等类型的语句时才会分配事务 id,否则事务的 id 是 0,所以对于只读类型的事务(只有 select 语句),其事务 id 始终为 0。
  2. 聚簇索引上每条数据记录都有两个隐藏字段:trx_id 和 roll_pointer,前者对应的是修改这条数据的事务 id,后者对应的是指向当前数据修改的前一个版本的指针,指针会指向 undo 日志。

版本链示意图:

image_1d8poudrjdrk4k0i22bj10g82q.png-78.6kB

基于这两个前提,MySQL 的设计者提出了 ReadView 的概念。

在事务执行的特定阶段(后续补充),会生成对应的 ReadView,ReadView 的内容包括:

  • m_ids:表示 ReadView 创建时,系统中的活跃的读写事务的 id 集合。
  • min_trx_id:在生成 ReadView 时,系统中最小的(其实也就是创建最早的)活跃读写事务 id。
  • max_trx_id:在生成 ReadView 时,系统中预备分配给下一个读写事务的 id 的值。
  • creator_trx_id:生成当前 ReadView 的事务的 id。

所以当食物中的快照读语句开始执行时,需要基于当前生成的 ReadView 和对应数据行的 trx_id 进行对比,可能的对比场景如下:

  1. 数据行中的 trx_id 等于 ReadView 的 creator_trx_id,说明该行数据的修改者是当前事务本身,可以被查询出来。
  2. 数据行中的 trx_id 小于 ReadView 的 min_trx_id,说明该行数据被修改的时候当前事务的 ReadView 还没有生成(其他事务已经提交了对这行数据的修改),可以被查询出来。
  3. 数据行中的 trx_id 大于或者等于 ReadView 的 max_trx_id,说明修改该数据行的事务是在 ReadView 创建之后才开始执行的,不应该被当前事务查询到这条修改记录,所以应该顺着 roll_pointer 往上搜索旧版本记录,直到找到 trx_id 符合条件的记录。
  4. 数据行中的 trx_id 在 min_trx_id 和 max_trx_id 区间内,说明修改这条数据行的事务在创建当前 ReadView 的时候就是活跃的,这时候需要继续判断数据行中的 trx_id 是否还在活跃的读写事务 id 集合中(m_ids),如果不在了则说明事务已经提交,数据可以被查询返回;如果还在则说明对应事务还没有被提交,不可以被查询到这条数据,应该顺着 roll_pointer 往回找旧版本数据继续进行对比。

当对应数据行的 trx_id 不满足要求而导致不能返回查询数据时,应该依据 roll_pointer 指针往旧版本数据中进行查找,知道找到符合条件的版本的数据进行返回或者返回为空。

那么 MVCC 是如何区分 RC 和 RR 两种隔离级别的呢?

答案是 ReadView 在两种隔离级别中生成的时机不一样,在 RC 隔离级别中,只需要确保未被提交的数据不能读取到就好了,所以每次 select 快找查询都会生成 ReadView。所以如果事务中存在多次读取,第二次读取可能会返回和第一次读取不一样的数据,因为两次读取的间隔时间段内可能有其他事务修改并提交了事务。

在 RR 隔离级别中,要确保事务内的每一次 select 快找查询都依据统一版本数据进行查找,所以 ReadView 只有在事务中的第一条 select 语句执行时才会被创建,之后的每次 select 语句执行都会基于已经创建的 ReadView 进行版本判断。所以 RR 级别事务中的所有 select 语句读取到的数据都来自同一个版本,这有点儿像 Git Tag。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant