共计 4118 个字符,预计需要花费 11 分钟才能阅读完成。
设计:小艾
审核:丁奇
编辑:宇亭
作者:罗中天(花名:德里克)
浙江大学在读硕士、StoneDB 内核研发实习生
ANSI SQL-92 规范中规定了四种事务隔离级别和三种异象:读未提交(Read Uncommitted)、读已提交(Read Committed,简称 RC)、可反复读(Repeatable Read,简称 RR)和串行化(Serializable),其中读已提交解决了脏读,可反复读解决了脏读和不可反复读,串行化解决了脏读、不可反复读和幻读。上述这些内容是为人所熟知的,故不是本文的配角。本文的配角是快照隔离级别(Snapshot Isolation,简称 SI),同时引入新的异象写偏斜(Write Skew)。SI 不属于 SQL 规范的一部分,是对 SQL 规范的补充。
在将 SI 思考进去当前,能够失去如下表格中的内容
「隔离级别」 | 「写写关系」 | 「写读关系」 | 「读写关系」 | 「存在的问题」 |
---|---|---|---|---|
失落更新 | 写不阻塞写 | 写不阻塞读 | 读不阻塞写 | 脏写 / 脏读 / 不可反复读 / 幻读 / 写偏斜 |
读未提交 | 写阻塞写 | 写不阻塞读 | 读不阻塞写 | 脏读 / 不可反复读 / 幻读 / 写偏斜 |
读已提交 | 写阻塞写 | 写阻塞读 | 读不阻塞写 | 不可反复读 / 幻读 / 写偏斜 |
可反复读 | 写阻塞写 | 写阻塞读 | 读阻塞写 | 幻读 |
快照 | 写阻塞写 | 写不(齐全)阻塞读 | 读不(齐全)阻塞写 | 写偏斜 |
串行化 | 写阻塞写 | 写阻塞读 | 读间隙阻塞写 | 无 |
留神,上表中的读已提交、可反复读中的局部内容和 innodb 中的有些不符,起因是 innodb 中的 RC 和 RR 包含快照读和以后读两种状况,具体会在上面进行剖析。
接下来本文次要围绕 SI,论述 SI 的实现形式 MVCC、SI 的异象写偏斜、将 SI 和 RR 混在一起的“罪魁祸首”——Innodb 中的 RR 等内容。
SI 的实现形式
一般而言,SI 是用多版本并发管制(Multi-Version Concurrency Control,简称 MVCC)实现的。MVCC 自身有多种实现形式,并不是所有的 MVCC 都能实现实践上的 SI,比方 Innodb 中的 MVCC 其实就没有齐全实现 SI,因为它没有齐全解决幻读,对于 Innodb 中的 MVCC 的具体分析请见本文上面的大节。除了 MVCC 之外,SI 中的每个事务须要调配 2 个工夫戳,一个在事务开始的时候调配,一个在事务完结的时候调配。
一个残缺的 MVCC 协定包含并发控制协议、多版本的存储、垃圾回收和索引治理四个局部。本文次要对并发控制协议进行论述。
记录元数据
一种并发控制协议的实现形式
在上图的记录元数据的根底上新增 READ-TS 字段示意读取这条记录最大的事务 ID。
对于读来说,事务 读取没加写锁(TRX-ID 为 0)且满足 的记录,显然这样的记录最多只有一条,如果 READ-TS 小于,就 CAS 将 READ-TS 变成,如果 CAS 失败,持续比拟,如果还是小于,持续 CAS,如果大于的话,就能够完结了。
对于写来说,事务 找到最新的记录,如果不可见,就 abort,否则,如果该记录没加写锁(TRX-ID 为 0)且 大于等于 READ-TS,就将 TRX-ID CAS 为,即加写锁,而后生成新的版本,新版本 BEGIN-TS 设为,将 END-TS 设为无穷大,而后将加锁版本(旧版本)的 END-TS 改为(原来为无穷大)。在事务提交的时候,会为事务新调配一个工夫戳,将新记录版本的 BEGIN-TS 和旧记录版本的 END-TS 批改为该工夫戳,最初开释锁。\
为什么写的时候会有 大于等于 READ-TS 的条件?这是为了 ID 更大的事务的快照的前后一致性。这个条件示意曾经有 ID 更大的事务读取了该条记录,如果事务 生成了新的版本,那么原来那个版本的 END-TS 就会被改为,如果 ID 为 READ-TS 的事务再次读取这个记录,那么读到的记录就会变成最新版本的了(依据范畴),前后就不统一了。、
产生写写或者读写抵触后会产生事务的回滚(也有可能是阻塞),在下层的利用中能够进行自旋重试的操作。
SI 的异象
从文章结尾的表格中能够看出 SI 会呈现写偏斜的异象,并且解决了幻读,这里可能会有一些反常识,至于为什么有些人会产生 SI 没有解决的 MVCC 的误会,咱们会在下一大节中进行剖析。
写偏斜
如上图所示,事务 1 想要将所有的球变黑,它会先查问出有哪些球是白的,而后更新这些球为黑球,事务 2 想要将所有的球变白,它会先查问出哪些球是黑的,而后更新这些球为白球,因为两个事务都是基于快照进行批改的,所以最初的后果不是串行化能造成的状态(全黑或者全白)中的任意一个,这就是写偏斜的异样。用更加 hign level 的语言来表述的话,写偏斜是指两个事务并发读取一个数据集,而后各自利用读到的信息批改数据集中不相交的数据项,最初并发提交事务。
如何解决的幻读
假如有两个事务 A 和 B,以后事务 A 曾经进行了一个范畴的查问,之后按程序会产生事务 B 进行一次插入操作,事务 A 进行一次同样条件的查问操作,因为事务 B 的插入操作波及的记录的 BEGIN-TS 会在事务 B 提交的时候被改为为事务 B 的完结工夫戳,那么该工夫戳必定大于事务 A 的 trx\_id(在事务 A 开始的时候调配),所以事务 B 的插入对事务 A 是不可见的。
SI 和 RR 的次要区别
大家总是会将 SI 和 RR 搞混,甚至认为这两个是雷同的货色,这背地的罪魁祸首是 Innodb(其实 Postgress 也是,但在互联网行业中 Innodb 还是占比更重的那一位),具体的起因是 Innodb 的 RR 包含了快照读和以后读两种形式。
快照读
Innodb 中的一般读(select …)就是快照读,通过 MVCC 的形式实现。
Innodb 中的 MVCC
版本链
innodb 中的 undo log 被分为两大类,TRX\_UNDO\_INSERT 和 TRX\_UNDO\_UPDATE。其中 TRX\_UNDO\_UPDATE 类型的 undo log 有一个 roll pointer 字段,指向该条记录上一次批改对应的 undo log。同时每条数据记录也有一个 roll pointer 的暗藏字段,指向该条记录上一次批改对应的 undo log。这样通过 roll pointer,每条记录都能造成一个版本链。另外,每条记录和 undo log 里都存着造成这次批改的 trx id。每条数据记录是最新的,顺着版本链,能够追溯到之前的批改版本,以及每次批改对应的事务 id。
ReadView
查问流程
顺着版本链顺次进行判断
- 如果被拜访版本的 trx\_id 和 ReadView 中的 creator\_trx\_id 雷同,就查问到以后版本
- 如果被拜访版本的 trx\_id 小于 ReadView 中的 min\_trx\_id,该版本能够被以后事务拜访
- 如果被拜访版本的 trx\_id 大于等于 ReadView 中的 max\_trx\_id,该版本不能够被以后事务拜访
- 如果如果被拜访版本的 trx\_id 大于等于 ReadView 中的 min\_trx\_id,且小于 ReadView 中的 max\_trx\_id,须要判断 trx\_id 是否在 m\_ids 中,如果在的话,该版本不能够被以后事务拜访,否则,能够拜访
- 如果该版本不能够被以后事务拜访,顺着版本链持续判断下一个
快照读不是 SI
Innodb 中的快照读不是 SI,因为快照读引入了局部的幻读问题,而 SI 按后面所讲,不会有幻读的问题,然而有写偏斜的问题。
引入局部幻读
在上图所示的状况下会引入幻读,因为在第三步的时候会讲 id=5 的那条记录的 trx\_id 批改为事务 A 的事务 id,所以在第四步的时候会依据下面查问流程中的第一条,即拜访版本的 trx\_id 和 ReadView 中的 trx\_id 雷同,所以会“无中生有”地查到 id=5 的这条记录。\
这里可能读者会有一个疑难,那么如果在下面剖析 SI 的 MVCC 解决幻读的那个例子中也退出事务 A update 的这个操作,会怎么样?在 SI 的 MVCC 中,事务在生成新版本的时候的工夫戳肯定要比旧版本更大才行,因为事务 A 看不到事务 B 插入的记录,所以将无奈执行 update 操作。
实质起因
在 innodb 中
- 事务只有一个 trx\_id,没有开始和完结都调配一个工夫戳。
- 版本链按从新到就来看,它的工夫戳(或者 trx\_id)不是从大到小的(innodb 这样设计的起因集体认为是为了缩小事务的阻塞和回滚,如果按 SI 中的 MVCC 来看,可能会呈现不少这种读写抵触的状况)
以后读
Innodb 中的 update、select…for share mode、select…for update 等语句是以后读。以后读不走 MVCC 的逻辑,而是通过两阶段锁(Two Phase Lock,简称 2PL)的形式实现 RR,其实如果抛开快照读,Innodb 的 RR 其实就是串行化,通过间隙锁的形式解决了幻读的问题。
<p align=center>2PL</p>
Innodb 中的 2PL 是强两阶段锁(strong 2PL),即所有锁(包含 X 锁和 S 锁)的开释都须要放到事务提交之后,这样就能够解决脏读和不可反复读的问题。
间隙锁
Innodb 通过间隙锁解决了幻读的问题,所以 2PL+ 间隙锁解决了所有的异象,也就是 Innodb 串行化的实现形式。\
间隙锁尽管是锁住前后两条记录之间的间隙的,然而在实现上将其归于前面那条记录。间隙锁也分为 X 锁和 S 锁,间隙锁与间隙锁之间,无论是 X 锁还是 S 锁,都不会阻塞,但在插入一条记录的时候,如果存在间隙锁,就会生成一个插入意向锁,并阻塞。
小结
这篇文章咱们介绍了快照隔离级别 SI 以及和 RR 的区别,SI 是对四种常见隔离级别的补充,可能无效解决幻读的问题,是对 SQL 规范的重要补充。更多精彩硬核技术,欢送关注 StoneDB 开源社区,咱们后续会更新更多技术研发干货~
退出 StoneDB 社区
Github: https://github.com/stoneatom/stonedb
Gitee: https://gitee.com/StoneDB/stonedb
社区官网: https://stonedb.io/
哔哩哔哩: https://space.bilibili.com/1154290084
Twitter: https://twitter.com/StoneDataBase
Linkedin: https://www.linkedin.com/in/stonedb/