简单来说,事务是用来保证一组数据库操作,要么全部成功,要么全部失败。
在 MySQL 中,事务的支持是在引擎层实现的,但并不是所有的引擎都支持事务,比如 MyISAM(MySQL 的原生引擎)就不支持事务。

由事务引发的问题

为保证数据的一致性,我们使用了事务。但当数据库上有多个事务同时读写同一数据时,会导致如下问题:

Dirty read / 脏读

脏读是指读到了其它事务未提交的数据,未提交意味着这个数据可能最终不会存到数据库中,那么这个数据就是不存在的数据,读到了不存在的数据,就是脏读。

Non-repeatable read / 不可重复读

不可重复读是指在同一事务内,不同时期读取同一批数据可能会得到不一样的结果,侧重于 Update。

如:当前事务查询数据 a 的值为 1,这时有其它事务将 a 的值修改为 2 并提交了事务,当前事务再次读取数据 a 时,它的值变成了 2

Phantom read / 幻读

大致与不可重复读相同,专指 “新插入的行”。

如:当前事务在数据库中未找到数据 a 后,执行 Create 操作创建数据 a,但在提交事务时,由于其它事务创建了数据 a,导致当前事务无法正确提交

为了解决这些问题,就有了“隔离级别”的概念。

隔离级别

所谓事务的隔离级别,就是将各个事务以不同的级别隔离开来,从而解决上述问题。

Read uncommitted / 读未提交

可读取其它事务已修改但未提交的数据。

Read committed / 读提交

只可读取其它事务已提交的数据。
可解决:脏读

Repeatable read / 可重复读

只可读取其它事务已提交的数据,且事务中读取的数据与事务开始时的数据一致。
可解决:脏读、不可重复读

Serializable / 串行化

行级读写锁。
可解决:脏读、不可重复读、幻读

Example:

  1. mysql> create table T(c int) engine=InnoDB;
  2. mysql> insert into T(c) values (1);
事务 A 开启事务 查询
得到值 1
查询
得到值 V1
查询
得到值 V2
提交事务 查询
得到值 V3
事务 B 开启事务 查询
得到值 1
将 1
修改为 2
提交事务

读未提交V1 = 2V2 = 2V3 = 2
读已提交V1 = 1V2 = 2V3 = 2
可重复读V1 = 1V2 = 1V3 = 2
串行化V1 = 1V2 = 1V3 = 2

因为事务 B 执行修改操作时,需等待事务 A 提交,所以 V1 & V2 = 1。在事务 A 提交事务后,事务 B 继续执行修改操作,所以 V3 = 2

事务隔离的实现

事务隔离有读写锁和 MVCC(多版本并发控制)两种实现方式。串行化由读写锁实现,而读提交、可重复读则由 MVCC 实现。

何为 MVCC ? MVCC 又称“多版本并发控制”。所谓“多版本”是指一条记录在数据库中存在多个版本,每个以事务 id(trx_id)作为版本号,而事务 id 按申请顺序递增。

可重复读为例,在事务开启时事务系统会为当前事务分配一个事务 id(trx_id), InnoDB 为当前事务创建一个全局的一致性视图(当前事务的快照,以下简称视图),这个视图决定了当前视图可见的数据版本。
视图内主要包含以下四个主要内容:

  • m_ids:表示视图生成时,所有活跃的事务 id
  • min_trx_id:表示视图生成时,活跃的最小事务id
  • max_trx_id:表示视图生成时,事务系统应该分配给下一事务的 id
  • creator_trx_id:表示当前事务的 id

image.png
当前事务在读取数据时:

  • 数据的 trx_id 小于 min_trx_id,说明该数据的事务已提交,当前事务内可见;
  • 数据的 trx_id 大于等于 max_trx_id,说明该数据是在事务快照后产生的,当前事务内不可见;
  • 数据的 trx_id 在 min_trx_id 和 max_trx_id 范围内,且不等于 creator_trx_id,说明该数据所在事务处于活跃状态,当前事务内不可见;
  • 数据的 trx_id 等于 creator_trx_id,说明该数据为当前事务创建或修改,当前事务内可见;

Example:

name trx_id 事务是否处于活跃状态
张一 1
张二 2
张三 3
张四 4
张五 5

当前事务开启,快照后视图内数据如下:

  1. {
  2. "m_ids": [2, 5],
  3. "min_trx_id": 2,
  4. "max_trx_id": 7,
  5. "creator_trx_id": 6
  6. }

其它事务将 name 修改为“张六”后

name trx_id 事务是否处于活跃状态
张一 1
张二 2
张三 3
张四 4
张五 5
张六 7

当前数据读取 name 数据时:

  • 张六的 trx_id 为 7, >= max_trx_id,故当前事务内张六不可见;
  • 张五的 trx_id 为 5,包含在 m_ids 内,故当前事务内张五不可见;
  • 张四的 trx_id 为 4,< max_trx_id 且不包含在 m_ids 内,故当前事务内张四可见;
  • 张三的 trx_id 为 3,< max_trx_id 且不包含在 m_ids 内,故当前事务内张三可见;
  • 张二的 trx_id 为 2,包含在 m_ids 内,故当前事务内张二不可见;
  • 张一的 trx_id 为 1,< min_trx_id,故当前事务内可见;

以上,就是在“可重复读”时,事务隔离的实现。
“读提交”的事务隔离实现与之一致,唯一的区别就是:可重复读是在事务开启时创建视图,读提交是在每次 SELECT 语句前创建的视图。