简单来说,事务是用来保证一组数据库操作,要么全部成功,要么全部失败。
在 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:
mysql> create table T(c int) engine=InnoDB;
mysql> insert into T(c) values (1);
事务 A | 开启事务 | 查询 得到值 1 |
查询 得到值 V1 |
查询 得到值 V2 |
提交事务 | 查询 得到值 V3 |
|||
---|---|---|---|---|---|---|---|---|---|
事务 B | 开启事务 | 查询 得到值 1 |
将 1 修改为 2 |
提交事务 |
读未提交:V1 = 2
、V2 = 2
、V3 = 2
。
读已提交:V1 = 1
、V2 = 2
、V3 = 2
。
可重复读:V1 = 1
、V2 = 1
、V3 = 2
。
串行化:V1 = 1
、V2 = 1
、V3 = 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
当前事务在读取数据时:
- 数据的 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 | ✅ |
当前事务开启,快照后视图内数据如下:
{
"m_ids": [2, 5],
"min_trx_id": 2,
"max_trx_id": 7,
"creator_trx_id": 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 语句前创建的视图。