tidb数据库隔离级别剖析

tidb数据库隔离级别剖析本文章来源于:https://github.com/Zeb-D/my-review,请star强力支持,你的支持,就是我的动力。[TOC]前言在线应用业务中,数据库是一个非常重要的组成部分,特别是现在的微服务架构为了获得水平扩展能力,我们倾向于将状态都存储在数据库中,这要求数据库能够正确、高性能处理请求,但这是一个几乎不可能达到的要求,所以数据库的设计者们定义了隔离级别这一个概念,在高…

大家好,又见面了,我是你们的朋友全栈君。

本文章来源于:https://github.com/Zeb-D/my-review ,请star 强力支持,你的支持,就是我的动力。

[TOC]



前言

在线应用业务中,数据库是一个非常重要的组成部分,特别是现在的微服务架构为了获得水平扩展能力,我们倾向于将状态都存储在数据库中,这要求数据库能够正确、高性能处理请求,但这是一个几乎不可能达到的要求,所以数据库的设计者们定义了隔离级别这一个概念,在高性能与正确性之间提供了一个缓冲地带,明确地告诉使用者,我们提供正确性差一点但是性能好一点的模式和正确性好一点但是性能差一点的模式,使用者可以按照你们的业务场景来选择使用。


本质

从本质上讲,隔离级别是定义数据库并发控制的。在应用程序的开发中,我们通常利用锁进行并发控制,确保临界区的资源不会出现多个线程同时进行读写的情况,这对应数据库的隔离级别为可串行化(最高的隔离级别)。现在发现离级别其实是和我们日常开发经常碰到的一个概念了吧,那么现在肯定会有一个问题,为什么在应用程序中可提供可串行化的隔离级别,而数据库却不能提供呢?其根本的原因是应用程序的对临界区都是内存操作,数据库要保证持久性(ACID中的Durability)需要把临界区的数据持久化到磁盘,磁盘操作比内存操作要慢好几个数量级(一次随机访问内存、SSD磁盘和SATA磁盘对应的操作时间分别为几十纳秒、几十微秒和几十毫秒),这会导致临界区持有锁时间变长,对临界区资源竞争将变的异常激烈,数据库的性能会大大降低。


隔离级别

数据库的隔离级别,SQL-92 标准定义了 4 种隔离级别:读未提交 (READ UNCOMMITTED)、读已提交 (READ COMMITTED)、可重复读 (REPEATABLE READ)、串行化 (SERIALIZABLE)。详见下表:

img

但是由于各数据库的具体实现各不相同,导致同一隔离级别可能出现的异常情况也不相同,所以本文直接从各个隔离级别会带来的异常情况来分析隔离级别的定义。


异常情况

从读未提交到可串行化,数据库可能出现的异常为:


脏写

事务a覆盖了其他事务尚未提交的写入。


脏读

事务a读到了其他事务尚未提交的写入。


读倾斜

事务a在执行过程中,对某一个值在不同的时间点读到了不同的值,也叫不可重复读。


更新丢失

两个事务同时执行读-修改-写入操作序列,出现了其中一个覆盖了另一个的写入,但是没有包含对方最新值的情况,导致了被覆盖的数据发生了更新丢失。


幻读

事务先查询了某些符合条件的数据,同时另一个事务执行写入,改变了先前的查询结果。


写倾斜

事务先查询数据库,根据返回的结果而作出某些决定,然后修改数据库。在事务提交的时候,支持决定的条件不再成立。写倾斜是幻读的一种情况,是由于读-写事务冲突导致的幻读。写倾斜也可以看做一种更广义的更新丢失问题。即如果两个事务读取同一组对象,然后更新其中的一部分:不同的事务更新不同的对象,可能发生写倾斜;不同的事务更新同一个对象,则可能发生脏写或者更新丢失。


异常避免

对应四个隔离级别,我们分别来看看他们有什么异常情况,以及怎么通过应用层的优化来避免该异常的发生:

  • 对于脏写,几乎所有的数据库都可以防止,我们用的mysql和TiDB更是没有问题,所以不讨论脏写的情况;
  • 对于脏读,提供读已提交隔离级别及以上的数据库都可以防止异常的出现,如果业务中不能接受脏读,那么隔离级别最少在读已提交隔离级别或者以上;
  • 对于读倾斜,可重复读隔离级别及以上的数据库都可以防止问题的出现,如果业务中不能接受脏读,那么隔离级别最少可重复读隔离级别或者以上;
  • 对于更新丢失,幻读,写倾斜,如果只通过数据库隔离级别来处理的话,那么只有可串行化的隔离级别才能防止问题的出现,然而在生产环境中,我们几乎是不可能开启可串行化隔离级别的,要么是数据库直接不支持,要么是数据库支持,但是性能太差。因而在实际开发中,我们只能在可重复读的隔离级别的基础上,通过一些其他的手段来防止问题的发生。


怎么避免更新丢失

  • 如果数据库提供原子写操作,那么一定要避免在应用层代码中完成“读-修改-写”操作,应该直接通过数据库的原子操作来执行,这样就可以避免更新丢失的问题。数据库的原子操作例如关系数据库中的 udpate table set value=value+1 where key=*,mongodb也提供类似的操作。数据库的原子操作一般通过独占锁来实现,相当于可串行化的隔离级别,所以不会有问题。不过在使用ORM框架的时候,就很容易在应用层代码中完成“读-修改-写”的操作,导致无法使用数据库的原子操作。
  • 另外一个情况,如果数据库不支持原子操作,或者在某一些场景,原子操作不能处理的时候,可以通过对查询结果显示加锁来解决。对于mysql来说,就是 select for update,通过for update告诉数据库,查询出来的数据行一会是需要更新的,需要加锁防止其他的事务也来读取更新导致更新丢失。
  • 一种更好的避免更新丢失的方式是数据库提供自动检测更新丢失的机制。数据库先让事务都并发执行,如果检测到有更新丢失的风险,直接中止当前事务,然后业务层在重试即可。目前PostgreSQL和TiDB的可重复读,Oracle的可串行化等都提供自动检测更新丢失的机制,但是mysql的InnoDB的可重复读并不支持。
  • 在某一些情况下,还可以通过原子比较和设置来实现,例如:update table set value=newvalue where id=* and value=oldvalue。但是该方式有一个问题,如果where条件的判断是基于某一个旧快照来执行的,那么where的判断是没有意义的。所以如果要采用原子比较和设置来避免更新丢失,那么一定要确认数据库比较-设置操作的安全运行条件。


怎么避免幻读中的写倾斜

在前面的讨论中,我们提供了很多种方式来避免更新丢失,那么在写倾斜的时候可以使用吗?

  • 原子操作上不行的,因为涉及到多个对象的更新;
  • 所有的数据库几乎都没有自动检测写倾斜的机制;
  • 数据库自定义的约束功能对于多个对象也基本不支持;
  • 显式加锁方式上可以的,通过select for update,可以确保事务以可串行化的隔离级别,所以这个方案上可行的。但这不是对于所有的情况下都适用,例如select for update 如果在select的时候不能查询到数据,那么这个时候数据库无法对数据进行加锁。例如:在订阅会议室的时候,select的时候会议室还没有被订阅,所以查询不到,数据库也没有办法进行加锁,update的时候,多个事务都可以update成功。所以,显式加锁对于写倾斜不能适用的情况是因为在select阶段没有查询到临界区的数据,导致无法加锁。在这种情况下,我们可以人为的引入用于加锁的数据,然后通过显式加锁来避免写倾斜的问题。比如在订阅会议室的问题中,我们为所有的会议室的所有时间都创建好数据,每一个“时间-会议室”一条数据,这个数据没有其他的意义,只是用来select for update的时候由于select 查询到数据,用于数据库来加锁。
  • 另外一种方式是在数据库提供可串行化隔离级别,并且性能满足业务要求时,直接使用可串行化的隔离级别。


TiDB的隔离级别[1]

TiDB 实现了快照隔离 (Snapshot Isolation, SI) 级别的一致性。为了与 MySQL 保持一致,又称其为“可重复读”。该隔离级别不同于 ANSI 可重复读隔离级别和 MySQL 可重复读隔离级别。

当事务隔离级别为可重复读时,只能读到该事务启动时已经提交的其他事务修改的数据,未提交的数据或在事务启动后其他事务提交的数据是不可见的。对于本事务而言,事务语句可以看到之前的语句做出的修改。对于运行于不同节点的事务而言,不同事务启动和提交的顺序取决于从 PD 获取时间戳的顺序。处于可重复读隔离级别的事务不能并发的更新同一行,当时事务提交时发现该行在该事务启动后,已经被另一个已提交的事务更新过,那么该事务会回滚并启动自动重试。示例如下:

create table t1(id int);
insert into t1 values(0);

start transaction;              |               start transaction;
select * from t1;               |               select * from t1;
update t1 set id=id+1;          |               update t1 set id=id+1;
commit;                         |
                                |               commit; -- 事务提交失败,回滚


与 ANSI 可重复读隔离级别的区别

尽管名称是可重复读隔离级别,但是 TiDB 中可重复读隔离级别和 ANSI 可重复隔离级别是不同的。按照 A Critique of ANSI SQL Isolation Levels 论文中的标准,TiDB 实现的是论文中的快照隔离级别。该隔离级别不会出现狭义上的幻读 (A3),但不会阻止广义上的幻读 (P3),同时,SI 还会出现写偏斜,而 ANSI 可重复读隔离级别不会出现写偏斜,会出现幻读。


与 MySQL 可重复读隔离级别的区别

MySQL 可重复读隔离级别在更新时并不检验当前版本是否可见,也就是说,即使该行在事务启动后被更新过,同样可以继续更新。这种情况在 TiDB 会导致事务回滚,导致事务最终失败,而 MySQL 是可以更新成功的。MySQL 的可重复读隔离级别并非快照隔离级别,MySQL 可重复读隔离级别的一致性要弱于快照隔离级别,也弱于 TiDB 的可重复读隔离级别。


总结

本文我们讨论了数据库出现隔离级别这个概念的根本原因是数据库设计者因为要保证持久性,因而有大量的磁盘操作,导致临界区变长,性能急剧下降,提出的一个trade-off的方案,让使用者根据自己的业务场景来选择不同的隔离级别,然后我们讨论了不同的隔离级别导致的异常情况的处理方法,确保可以写出高性能并且正确的程序,最后我们介绍了tidb隔离级别的情况。


参考

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/141508.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • python微信机器人制作教程+源码[通俗易懂]

    python微信机器人制作教程+源码[通俗易懂]python微信机器人,定时机器人,监控机器人。

  • 微信公众号网页开发和小程序开发之路哪个好_如何制作微信公众号

    微信公众号网页开发和小程序开发之路哪个好_如何制作微信公众号在微信公众号网页和小程序开发的过程中,难免会遇到一些知识外的兼容问题。在此记录我所遇到的问题,以便给需要的前端开发者查阅。

    2022年10月12日
  • IP地址(分类)、子网掩码、网络号、主机号、子网号

    IP地址(分类)、子网掩码、网络号、主机号、子网号IP地址IP地址被用来给Internet上的电脑一个编号。大家日常见到的情况是每台联网的PC上都需要有IP地址,才能正常通信。我们可以把“个人电脑”比作“一台电话”,那么“IP地址”就相当于“电话号码”,而Internet中的路由器,就相当于电信局的“程控式交换机”。IP地址是一个32位的二进制数,通常被分割为4个“8位二进制数”(也就是4个字节)。IP地址通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d都是0~255之间的十进制整数。例:点分十进IP地址(100.4.5.6),

  • 微商分销功能不能用了

    微商分销功能不能用了“三级以上分销将会被停止支付功能和封停账号”,日前一则“不利”消息彻底引爆微商的主要阵地——微信朋友圈。一时间关于分销三级变二级、微商寒冬将至的说法再次疯传。自央视曝光部分微商涉嫌传销之后,微信今年接连对微商们“动刀”,强化管理意在行业正规化发展,失去多级分销之后,依靠内容深度揽客的方式成为微商转型的方向。微商连遭重创刚刚过去的一周,微商们再次体验到了人生的跌宕起伏。认证为腾讯微信

  • IntelliJ idea自定义模板Live Templates[通俗易懂]

    IntelliJ idea自定义模板Live Templates[通俗易懂]1、点击File–>Setting(Ctrl+Alt+S)–>Live Template,点击右侧的+号,选择Template Group2、输入MyGroup(也可以输入自定义的名称。我写的是My),然后点击OK3、选中My之后,再次点击右侧的+号,选择Live Template下图是 正确是 public static void main(String[] args)   粗心…

  • switch中的continue和break区别[通俗易懂]

    switch中的continue和break区别[通俗易懂]今天c程设期末考试,突然有一道选择题是关于switch中的break和continue问题。若switch外部没有循环,则break和continue没有区别。若switch外部还有循环,{一.若break,continue在switch外部,则二者作用的是外部循环。二.若break,continue在switch内部,则break作用于switch,continue作用于外部循环。…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号