本文正在参加「金石方案 . 瓜分6万现金大奖」

假定当时数据库里有下面这张表。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

老规矩,以下内容仍是默许发生在innodb引擎的可重复读阻隔等级下。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

咱们能够看到,线程1,相同都是读 age >= 3 的数据。第一次读到1条数据,这个是原始状况。这之后线程2将id=2的age字段也改成了3。

线程1此刻再读两次,一次读到的成果仍是原来的1条,另一次读的成果却是2条差异在于加没加for update。

为什么相同条件下,都是读,读出来的数据却不相同呢?

可重复读不是要求每次读出来的内容要相同吗?


要回答这个问题。

我需求从盘古是怎样开天辟地这个论题开端聊起。


不好意思。

失态了。

那就从业务是怎样回滚的开端聊起吧。


业务的回滚是怎样完成的

咱们在履行业务的时分,一般都是下面这样的格局

begin;
操作1;
操作2;
操作3;
xxxxx
....
commit;

在提交业务之前,会履行各种操作,里面能够包括各种逻辑。

只要是履行逻辑,那就有或许会报错。

回想下业务的ACID里有个A原子性,整个业务便是个全体,要么一同成功,要么一同失利。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

假如失利了的话,那就要让履行到一半的业务有才能回到没履行业务前的状况,这便是回滚

履行业务的代码就相似写成下面这样。

begin;
try:
	操作1;
  操作2;
  操作3;
  xxxxx
  ....
  commit;
except Exception:
	rollback;

假如履行rollback能回到业务履行前的状况的话,那阐明mysql需求知道某些行,履行业务前的数据长什么姿态。

那数据库是怎样做到的呢?

这就要提到undo日志了,它记录了某一行数据,在履行业务前是怎样样的。

比方id=1那行数据,name字段从**”小白”更新成了“小白debug”**,那就会新增一个undo日志,用于记录之前的数据。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

由于一起并发履行的业务能够有很多,所以或许会有很多undo日志,日志里参加业务的id(trx_id)字段,用于标明这是哪个业务下发生的undo日志。

一起将它们用链表的方法组织起来,在undo日志里参加一个指针(roll_pointer),指向上一个undo日志,所以就形成了一条版别链

都是同样条件的mysql select语句,为什么读到的内容却不一样?

有了这个版别链,当某个业务履行到一半发现失利时,就直接回滚,这时分就能够顺着这个版别链,回到履行业务前的状况。


当时读和快照读是什么

有了上面的undo日志版别链之后,咱们能够看到最新的数据在表头,在这之后的都是一个个旧的数据版别。不论是最新的,仍是旧的数据版别,咱们都叫它数据快照

当时读,读的便是版别链的表头,也便是最新的数据

快照读,读的便是版别链里的其间一个快照,当然假如这个快照正好便是表头,那此刻快照读和当时读的成果相同。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

咱们平时履行的一般select句子,比方下面这种,便是快照读

select * from user where phone_no=2

而特别的select句子,比方在select后边加上lock in share modefor update,都归于当时读

除此之外insert,update,delete操作都归于写操作,已然写,那必定是写最新的数据,所以都会引发当时读。


那么问题来了。

当时读,读的是版别链的表头,那么履行当时读的时分,有没有或许恰好有其他业务,生成更加新的快照,代替当时表头,成为新的表头呢,那这时分岂不是读的不是最新数据了?

答案是不会,不论是select … for update这些(特别的)读操作,仍是insert、update这些写操作,都会对这行数据加锁。而生成undo日志快照,也是在写操作的情况下生成的,履行写操作前也需求取得锁。所以写操作需求阻塞等候当时读完成后,取得锁后才能更新版别链。


read view

数据库里能够一起并发履行非常多的业务, 每个业务都会被分配一个业务ID, 这个 ID 是递增的,越新的业务,ID 越大。

而数据表里某行数据的undo日志版别链,每个undo日志上面也有一个业务id (trx_id),它是创立这个undo日志的业务id

并不是一切业务都会生成undo日志,也便是说某行数据的undo日志版别链上只要部分业务的id。可是,一切业务都有或许会访问这行数据对应的版别链。而且版别链上虽然有很多undo日志快照,但也不是一切undo日志都能被读,毕竟有些undo日志,创立它们的业务还没提交呢,人家随时或许失利并回滚。

现在的问题就成了,现在有一个业务,经过快照读的方法去读undo日志版别链,那它能读哪些快照?而且它应该读哪个快照?

这儿就要引进一个read view的概念。它就像是一个有上下鸿沟的滑动窗口。

整个数据库里有那么多业务,这些业务分为现已提交(commit)的,和没提交的。没提交的,意味着这些业务还在进行中,也便是所谓的活泼业务。一切的活泼业务的id,组成m_ids。而这其间最小的业务id便是read view的下鸿沟,叫min_trx_id。

发生read view的那一刻,一切业务里最大的业务id,加个1,便是这个read view的上鸿沟,叫max_trx_id。

概念太多,有点乱?没事的,继续往下看,后边会有比如的。


业务能读哪些快照

有了这些根底信息之后,咱们先看下业务在read view下,他能读哪些快照呢?

记住一个大前提:业务只能读到自己发生的undo日志数据(业务提不提交都行),或者是其他业务现已提交完成的数据

现在业务(假定就叫业务A吧)有了read view之后,不论看哪个undo日志版别链,咱们都能够把read view往版别链上一放。版别链就被分成了好几部分。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

  • 版别链快照的trx_id < read view的min_trx_id

    从上面的描绘中,咱们能够知道read view的m_ids来源于数据库一切活泼业务的id,而最小的min_trx_id便是read view的下鸿沟,因为业务id是依据时刻递增的,所以假如版别链快照的trx_id比 min_trx_id 还要小,那这些肯定都是非活泼(现已提交)的业务id,这些快照都能被业务A读到。

  • 版别链快照的trx_id >= read view的max_trx_id

    max_trx_id是在业务A创立read view的那一刻发生的,它比那时分一切数据库已知的业务id都还要大。所以假如undo日志版别链上的某个快照上含有比 max_trx_id 还要大的 trx_id,那阐明这个快照现已超出业务A的”理解规模了”,它不该被读到。

  • read view的min_trx_id <= 版别链快照的trx_id < read view的max_trx_id

    • 假如版别链快照的trx_id正好便是业务A的id,那正好是它自己生成的undo日志快照,那不论有没有提交,都能读
    • 假如版别链快照的trx_id正好在活泼业务m_ids中, 那这些业务数据都还没提交,所以业务A不能读到它们
    • 除了上面两种情况外,剩下的都是现已提交的业务数据,能够放心读。

业务会读哪个快照

上面提到,业务在read view的可见规模里,有时机能读到N多快照。但那么多快照版别,业务具体会读哪个快照呢?

业务会从表头开端遍历这个undo日志版别链,它会拿每个undo日志里的trx_id去跟自己的read view的上下鸿沟去做判别。第一个出现的小于max_trx_id的快照

  • 假如快照是自己发生,那提不提交都行,就决议是读它了。
  • 假如快照是别人发生的,且现已提交完成了,那也行,决议读它了。

比方下图,undo日志1正好小于max_trx_id,且业务现已提交,那么就读它了。

都是同样条件的mysql select语句,为什么读到的内容却不一样?


MVCC是什么

像上面这种,保护一个多快照的undo日志版别链,业务依据自己的read view去决议具体读那个undo日志快照,最理想的情况下是每个业务都读自己的一份快照,然后在这个快照上做自己的逻辑,只要在写数据的时分,才去操作最新的行数据,这样读和写就被分开了,比起单行数据没有快照的方法,它能更好的处理读写抵触,所以数据库并发功用也更好。其实这便是面试里常问的MVCC,全称Multi-Version Concurrency Control,即多版别并发控制。

都是同样条件的mysql select语句,为什么读到的内容却不一样?


四个阻隔等级是怎样完成的

之前的写的一篇文章终究留了个问题,四个阻隔等级是怎样完成的。

知道了undo日志版别链MVCC之后,咱们再回过头来看下这个问题。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

读未提交,每次读到的都是最新的数据,也不论数据行地点的业务是否提交。完成也很简单,只需求每次都读undo日志版别链的链表头(最新的快照)就行了。

与读未提交不同,读提交和可重复读阻隔等级都是依据MVCC的read view完成的,反过来说, MVCC也只会出现在这两个阻隔等级里

读已提交阻隔等级,每次履行一般select,都会从头生成一个新的read view,然后拿着这个最新的read view到某行数据的版别链上挨个遍历,找到第一个合适的数据。这样就能做到每次都读到其他业务最新已提交的数据。

可重复读阻隔等级下的业务只会在第一次履行一般select时生成read view,后续不论履行几回一般select,都会复用这个 read view。这样就能坚持每次读的时分都是在同一规范下进行读取,那读到的数据也会是相同的。

串行化目的便是让并发业务看起来就像单线程履行相同,那完成也很简单,和读未提交阻隔等级相同,串行化阻隔界别下业务只读undo日志链的链表头,也便是最新版别的快照,而且就算是一般select,也会在版别链的最新快照上参加读锁。这样其他业务想写,也得等这个读锁释放掉才行。一切对这行数据进行操作的业务,都老老实实地阻塞等候加锁,一个接一个进行处理,从效果上看就跟单线程处理相同。


再看文章最初的比如

咱们用上面提到的概念,从头回到文章最初的比如,整理一遍。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

咱们假定数据库一开端的三条数据,都是由trx_id=1的业务insert生成的。

所以数据表一开端长下面这样。每行数据只要一个快照。注意快照里,trx_id填的是创立它们的业务id,也便是刚刚提到的业务1roll_pointer本来应该指向insert发生的undo日志,为了简化,这儿写为null(insert undo日志在业务提交后能够被清理掉)。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

下面这个图,仍是文章最初的图,这儿放出来是为了便利咱们,不必划回去看了。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

线程1发动业务,咱们假定它的业务trx_id=2第一次履行一般select,是快照读,在可重复读阻隔等级,会生成一个read view。当时这个数据库,活泼业务只要它一个,那m_ids =[2]。 m_ids里最小的id,也便是min_trx_id=2。max_trx_id是当时最大数据库业务id(只要它自己,所以也是2),加个1,也便是max_trx_id=3

都是同样条件的mysql select语句,为什么读到的内容却不一样?

此刻线程1的业务,拿着这个read view去读数据库表。

因为这三条数据的trx_id=1都小于min_trx_id=2,都归于可见规模,因此能读到这三条数据的一切快照,终究回来符合条件(age>=3)的数据,有1条。


这时分业务2,假定它的业务trx_id=3,履行更新操作,生成新的undo日志快照。

都是同样条件的mysql select语句,为什么读到的内容却不一样?

此刻线程1第2次履行一般select,仍是快照读,由所以可重复读,会复用之前的read view,再履行一次读操作,这儿重点关注id=2的那行数据,从版别链表头开端遍历,第一个快照trx_id=3 >= read view的max_trx_id=3,因此不可读,遍历下一个快照trx_id=1 < min_trx_id=2,可读。所以id=2的那行数据,仍是拿到age=2,而不是更新后的age=3,因此快照读成果仍是只要1条数据符合age>=3。

可是线程1第三次读,履行select for update,就成了当时读了,直接读undo日志版别链里最新的那行快照,所以能读到id=2,age=3,所以终究成果回来符合age>=3的数据有2条

总的来说便是,由于快照读和当时读,读数据的规则不同,咱们看到了不相同的成果。


看到这儿,咱们应该理解了,所谓的可重复读每次读都要读到相同的数据,这儿头的**”读”,指的是快照读**。

假如下次面试官问你,可重复读阻隔等级下每次读到的数据都是相同的吗?

你该知道怎样回答了吧?


总结

  • 业务经过undo日志完成回滚的功用,然后完成业务的原子性(Atomicity)。
  • 多个业务生成的undo日志构成一条版别链。快照读时业务依据read view来决议具体读哪个快照。当时读时业务直接读最新的快照版别。
  • mysql的innodb引擎经过MVCC提升了读写并发。

终究

最近原创更文的阅读量稳步跌落,思前想后,夜里翻来覆去。

我有个不成熟的恳求。

都是同样条件的mysql select语句,为什么读到的内容却不一样?


脱离广东好长时刻了,良久没人叫我靓仔了。

咱们能够在评论区里,叫我一靓仔吗?

我这么善良质朴的愿望,能被满足吗?

假如真实叫不出口的话,能帮我点下右下角的点赞和在看吗?


别说了,一同在常识的海洋里呛水吧