TIDB和MySQL事务隔离级别对比
1事务ACID概念
- A(Atomicity) : 原子性,事务中的一系列操作要么全部完成,要么全部不完成,不能做了一半不做了。这个好理解,比如转账不能扣完A的钱,不给B加钱。
- I(Isolation) : 隔离性,多个事务之间相互隔离的特性。首先不同业务对事务的隔离等级要求不一样,有的严格要求隔离,有的并不是那么严格。因此数据库系统都会实现多种隔离级别,从技术角度讲,每种隔离级别都需要不同的技术手段来保证,通常来说涉及各种锁和MVCC机制。
- D(Durability) : 持久性,事务一旦提交,所修改的数据就会被持久化,后面即使发生任何异常都不会出现数据丢失。这个容易理解,要么数据直接落盘,要么数据操作日志落盘。但是通常情况下数据库系统也一般会根据数据重要性提供多种持久化策略供客户端选择使用,比如对于重要数据,就会要求数据同步落盘之后才能算事务完成,这是严格的持久化策略;而对于部分不重要数据,可能只会要求数据异步落盘就算事务完成。
- C(Consistency) : 一致性,要求事务必须始终保持系统处于一致的状态。比如A转账给B,转账前账户总额和转账后账户总和需要保持一致。
2隔离性简述
- 数据操作过程中利用数据库的锁机制或者多版本并发控制机制获取更高的隔离等级。但是,随着数据库隔离级别的提高,数据的并发能力也会有所下降。所以,如何在并发性和隔离性之间做一个很好的权衡就成了一个至关重要的问题。
- 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
- 我们讨论隔离级别的场景,主要是在多个事务并发的情况下。
3.TIDB的事务隔离级别(官方文档)
- 根据官方文档:TIDB实现了读已提交和可重复读。
- TiDB 使用percolator事务模型,当事务启动时会获取全局读时间戳,事务提交时也会获取全局提交时间戳,并以此确定事务的执行顺序,如果想了解 TiDB 事务模型的实现可以详细阅读以下两篇文章:TiKV 的 MVCC(Multi-Version Concurrency Control)机制,Percolator 和 TiDB 事务算法。
3.1TIDB的可重复读
- 可重复读是 TiDB 的默认隔离级别,当事务隔离级别为可重复读时,只能读到该事务启动时已经提交的其他事务修改的数据,未提交的数据或在事务启动后其他事务提交的数据是不可见的。对于本事务而言,事务语句可以看到之前的语句做出的修改。
- 对于运行于不同节点的事务而言,不同事务启动和提交的顺序取决于从 PD 获取时间戳的顺序。
- 处于可重复读隔离级别的事务不能并发的更新同一行,当时事务提交时发现该行在该事务启动后,已经被另一个已提交的事务更新过,那么该事务会回滚并启动自动重试。
3.2与 ANSI 可重复读隔离级别的区别
- 尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的,按照A Critique of ANSI SQL Isolation Levels论文中的标准,TiDB 实现的是论文中的 snapshot 隔离级别,该隔离级别不会出现幻读,但是会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。
3.3与MySQL可重复读隔离级别的区别
- MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚并后台重试,重试终可能会失败,导致事务终失败,而 MySQL 是可以更新成功的。 MySQL 的可重复读隔离级别并非 snapshot 隔离级别,MySQL 可重复读隔离级别的一致性要弱于 snapshot 隔离级别,也弱于 TiDB 的可重复读隔离级别。
3.4读已提交
- 读已提交隔离级别和可重复读隔离级别不同,它仅仅保证不能读到未提交事务的数据,需要注意的是,事务提交是一个动态的过程,因此读已提交隔离级别可能读到某个事务部分提交的数据。
- 不推荐在有严格一致要求的数据库中使用读已提交隔离级别。
3.5事务重试
- 对于 insert/delete/update 操作,如果事务执行失败,并且系统判断该错误为可重试,会在系统内部自动重试事务。
4.Mysql和TIDB的隔离级别对比
4.1Read uncommitted
- 读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
- 未提交读的数据库锁情况:
- 事务在读数据的时候并未对数据加锁。
- 事务在修改数据的时候只对数据增加行级共享锁。
- 表现
- 事务1读取某行记录时,事务2也能对这行记录进行读取、更新;当事务2对该记录进行更新时,事务1再次读取该记录,能读到事务2对该记录的修改版本,即使该修改尚未被提交。
- 事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。
- 刚开始,1号事务和2号事务看到的A都是A0,接着1号事务将A0更新为A1,再接着2号事务就读到A1新值,1号事务将A1回滚回了A0。
- 对比(set session transaction isolation level read uncommitted;)
- MySQL
- TIDB(这个图片后面有点问题,TIDB的结果应该是20,手误)
4.2 read committed
- 提交读,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
- 提交读的数据库锁情况:
- 事务对当前被读取的数据加 行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;
- 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
- 很显然,Read Committed是与Read Uncommitted是相对的,意思是说1号事务可以在2号事务提交之后看到2号事务修改的数据。这种隔离级别可以避免脏读,但是又引入了一个新的问题:不可重复读,如下图所示:
- 但是从上面的例子中我们也看到,事务一两次读取的结果并不一致,所以提交读不能解决不可重复读的读现象。
- 简而言之,提交读这种隔离级别保证了读到的任何数据都是提交的数据,避免了脏读。但是不保证事务重新读的时候能读到相同的数据,因为在每次数据读完之后其他事务可以修改刚才读到的数据。
- 对比(set session transaction isolation level read committed;)
- MySQL:
- TIDB:
4.3.repeatable read
- 重复读,就是在开始读取数据(事务开启)时,不再允许修改操作 。
- 重复读可以解决不可重复读问题。不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。
- 可重复读的数据库锁情况
- 事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加 行级共享锁,直到事务结束才释放。
- 事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
- 从字面意思来看这种隔离级别修复了不可重复读这样的问题,表现如下图所示:
- 可以看出,无论1号事务如何更新A,2号事务在随后的进程中看到的A值都是事务开始次看到的A值(A0)。虽然解决了不可重复读的问题,但是还有一个问题-幻读:
- 上图中1号事务在事务过程中插入了一个大于B0的新值B2,2号事务在插入操作前后读取B > 0的时候读到的值却不同。
- 对比1(set session transaction isolation level repeatable read;)
- MySQL和TIDB行为一致:
- 对比2(set session transaction isolation level repeatable read;)
- MySQL:
- TIDB:
- 对比3(set session transaction isolation level repeatable read;)
- MySQL:
- TIDB:
- 对比4(set session transaction isolation level repeatable read;)
- MySQL:
- TIDB:
- 对比5(set session transaction isolation level repeatable read;)
- MySQL:
- TIDB:
4.4Serializable
- 串性化是隔离严格的一种形式,要求有读写冲突的事务必须严格串行执行。如下图所示,2号事务要读取1号事务修改的记录A,这就导致2号事务必须等待1号事务提交之后才能开启执行。通过这种形式可以避免之前所提到脏读、不可重复读和幻读。虽说如此,几乎所有数据库业务都不会开启这种隔离级别,因为这会带来严重的锁冲突。
- 可序列化的数据库锁情况
- 事务在读取数据时,必须先对其加 表级共享锁 ,直到事务结束才释放
- 事务在更新数据时,必须先对其加 表级排他锁 ,直到事务结束才释放。