在面试中,面试官只要问MySQL的ACID,然后可以立马背出来八股文来(还有部分人估计都还回答不上来)。更可恶的是,有些面试官不按套路出牌,会继续问了,MySQL到底是如何实现ACID的呢?
蒙圈了吧,实话实说,这道题能劝退95%的人。
今天,本文主要探讨MySQL InnoDB
引擎下ACID的实现原理,对于诸如什么是事务,隔离级别的含义等基础知识不做过多阐述。
ACID
MySQL 作为一个关系型数据库,以最常见的 InnoDB 引擎来说,是如何保证 ACID 的。
- (Atomicity)原子性: 事务是最小的执行单位,不允许分割。原子性确保动作要么全部完成,要么完全不起作用;
- (Consistency)一致性: 执行事务前后,数据保持一致;
- (Isolation)隔离性: 并发访问数据库时,一个事务不被其他事务所干扰。
- (Durability)持久性: 一个事务被提交之后。对数据库中数据的改变是持久的,即使数据库发生故障。
隔离性
先说说隔离性,首先是四种隔离级别。
隔离级别 | 说明 |
---|---|
读未提交 | 一个事务还没提交时,它做的变更就能被别的事务看到 |
读提交 | 一个事务提交之后,它做的变更才会被其他事务看到 |
可重复读 | 一个事务中,对同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。InnoDB默认级别。 |
串行化 | 事务串行化执行,每次读都需要获得表级共享锁,读写相互都会阻塞,隔离级别最高,牺牲系统并发性。 |
不同的隔离级别是为了解决不同的问题。也就是脏读、幻读、不可重复读。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可以出现 | 可以出现 | 可以出现 |
读提交 | 不允许出现 | 可以出现 | 可以出现 |
可重复读 | 不允许出现 | 不允许出现 | 可以出现 |
序列化 | 不允许出现 | 不允许出现 | 不允许出现 |
那么不同的隔离级别,隔离性是如何实现的,为什么不同事物间能够互不干扰?答案是 锁 和 MVCC。
锁
先来说说锁, MySQL 有多少锁。
粒度
从粒度上来说就是表锁、页锁、行锁。表锁有意向共享锁、意向排他锁、自增锁等。行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁。
行锁的种类
在 InnoDB 事务中,行锁通过给索引上的索引项加锁来实现。这意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:共享锁和排他锁,以及加锁前需要先获得的意向共享锁和意向排他锁。
- 共享锁:读锁,允许其他事务再加S锁,不允许其他事务再加X锁,即其他事务只读不可写。
select...lock in share mode
加锁。 - 排它锁:写锁,不允许其他事务再加S锁或者X锁。
insert、update、delete、for update
加锁。
行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
行锁的实现算法
Record Lock
单个行记录上的锁,总是会去锁住索引记录。
Gap Lock
间隙锁,想一下幻读的原因,其实就是行锁只能锁住行,但新插入记录这个动作,要更新的是记录之间的“间隙”。所以加入间隙锁来解决幻读。
Next-Key Lock
Gap Lock + Record Lock, 左开又闭。
锁之于隔离性
大致介绍了下锁,可以看到。有了锁,当某事务正在写数据时,其他事务获取不到写锁,就无法写数据,一定程度上保证了事务间的隔离。但前面说,加了写锁,为什么其他事务也能读数据呢,不是获取不到读锁吗?
MVCC
前面说到,有了锁,当前事务没有写锁就不能修改数据,但还是能读的,而且读的时候,即使该行数据其他事务已修改且提交,还是可以重复读到同样的值。这就是MVCC,多版本的并发控制,Multi-Version Concurrency Control。
版本链
Innodb 中行记录的存储格式,有一些额外的字段:DATA_TRX_ID和DATA_ROLL_PTR。
- DATA_TRX_ID:数据行版本号。用来标识最近对本行记录做修改的事务 id。
- DATA_ROLL_PTR:指向该行回滚段的指针。该行记录上所有旧版本,在
undo log
中都通过链表的形式组织。
undo log : 记录数据被修改之前的日志,后面会详细说。
ReadView
在每一条 SQL 开始的时候被创建,有几个重要属性:
- trx_ids: 当前系统活跃(未提交)事务版本号集合。
- low_limit_id: 创建当前 read view 时“当前系统最大事务版本号+1”。
- up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
- creator_trx_id: 创建当前read view的事务版本号;
开始查询
现在开始查询,一个 select 过来了,找到了一行数据。
-
DATA_TRX_ID = low_limit_id:
说明该数据是在当前read view 创建后才产生的,数据不显示。
- 不显示怎么办,根据 DATA_ROLL_PTR 从 undo log 中找到历史版本,找不到就空。
-
up_limit_id