数据库内核系列(一)- 并发控制

这次主要聊聊数据库的并发控制,以及MySQL在这方面是怎么做的 (说实话我觉得MySQL的有些理念和设计有点落后于pg和现在的一些database了,具体为啥看完这篇就应该能理解了)

事务的概念

事务是将一系列读和写组织在一起的方式,将多个sql语句当作一个语句来执行,要么全部成功,也就是我们所说的commit,要么全部失败,我们就可以执行rollback或者其他的操作。事务的引入让数据库中的容错机制变得更加简洁直接,不需要再去考虑部分成功部分失败的情况。当然,并不是所有的数据库都需要事务。抛弃事务带来的安全性保证在有些场景下可以获得更高的性能,并且也有许多其他的方式来保证和事务相似的安全性。

事务带来的安全保证:ACID

学过数据库相关理论的同学肯定都知道,ACID是数据库中一个很重要的概念,代表着atomicity,consistency,isolation和durability,这些合并起来保证了事务在各个方面的安全性。

原子性(Atomicity)

一个原子性的操作保证的是,这个操作没有办法再被分为一个更小的执行单元,而且除了执行与未执行的两种状态之外,不会再有第三种的中间状态。在ACID的概念中,原子性可以理解为一个事务中有多个写,其中一个写操作fail了,整个事务就会fail,不会出现一半修改成功一半失败的情况。在多线程编程的场景中,原子性又有了另外一层含义,一个原子操作(atomic operation)时,其他线程不会看到这个操作的中间状态。而在ACID中,这一点是由隔离性(Isolation)来保证的。

原子性的实现

MySQL中使用InnoDB的Undo Log来实现原子性。上面我们说到,事务在失败需要撤销的时候,就会利用Undo Log中的记录来进行回滚。针对insert,delete和update每一种不同的操作,Undo Log对应的结构会有些许不同,这是因为每一种操作需要记录的东西不同。

  • insert操作
    在这里插入图片描述
    在一条insert执行的时候,MySQL会向Undo Log中增加一条log,其各部分组成如上图所示。其中,undo type被设置为insert,主键各个列信息以及大小则是记录了这条被插入的记录中的所有主键的大小以及值。只记录主键的信息是因为一条插入的逆操作对应着删除,而删除我们只需要知道主键就可以了。

  • delete操作
    在这里插入图片描述
    在InnoDB中,一个页面中已经被删除的记录会被组织成一个由PAGE_FREE指向的单项垃圾链表,链表中的项就是被删除的行。然而,在执行删除操作的时候,记录不会被立马移动到垃圾链表中,而是先给一个tombstone,标记这是一条删除的数据,随后在这个事务提交之后,后台线程会自动进行删除,这个阶段叫做purge。
    与insert的log结构不同的是,delete的log结构中还包含了trx_id以及roll_pointer。这两者是在标记tombstone之前,将被标记(也就是即将被删除的行)的行的trx_id以及roll_pointer记录下来,通过roll_pointer可以很轻松的找到这一条记录的上一条Undo Log。同时相比之下,delete log还多了除聚簇索引(主键)之外的索引列信息,这些记录并不是为了进行undo而使用,而是在purge的阶段同时更新相关联的各个索引。

  • update操作
    update的情况比较复杂,对于更新主键与不更新主键有不同的处理。
    对于不更新主键的情况,分为就地更新和不就地更新。对于更新的数据大小与原来相同的情况,可以直接就地更新,否则更新就被替换成了一次删除(旧数据)和一次插入(新数据)的操作。针对不更新主键的情况,Undo Log的格式如下:
    在这里插入图片描述
    这里与之前不一样的地方在于,n_updated记录的是被更新的列的数目,后面的信息记录的是更新之前这一列的大小和旧的值,以便之后的恢复。同样的,更新的列如果包括索引列,那么同样跟delete一样会被记录下来。
    然而对于主键的更新会更为复杂,因为这同时涉及到聚簇索引的调整。这里会分为两步操作,首先对原来的数据标记tombstone,而前面对于不更新主键的操作直接delete掉就数据就好。之所以这里不要直接删除,是因为如果删除掉了,其他同时访问这条数据的事务就访问不到了。这涉及到了MVCC的内容,后面会讲到。其次第二步就是重新插入新的数据。所以我们知道,一次主键的更新会记录两条log,一条是delete的log,一条是insert的log,结合起来才可以进行这一类update的恢复。

一致性(Consistency)

更准确的来说,一致性是ACID特性中唯一一个应用层面的属性。应用定义了某种invariant,并且初始的数据库满足这个invariant,而事务则保证这个invariant在之后的执行过程中,不论出现什么情况都不会被破坏。所以我们会看到原子性,隔离性和持久性都是数据库来保证,一致性却需要应用层来确保写出正确的事务,保证其定义的invariant。

隔离性(Isolation)

如果没有隔离性,那么当两个事务访问的数据有重叠时,就会出现race condition。隔离性,正如名字所说,意思是两个不同的事务互相是独立的,看不到另一方对于数据的修改和操作(实际上这一句话不是很准确,随后在不同的隔离级别中会介绍到)。

隔离性的实现

[todo]

持久性(Durability)

我们使用数据库的最根本目的就是存储数据,而数据如果一直存在于内存中是没有办法保证这一点的。主机可能会因为各种各样的问题崩溃,从而导致内存中的数据一去不复返。因此事务的安全性保证中的最后一条就是持久性,即数据一旦被成功提交,就不会丢失。

普遍来说,数据持久化的做法都是将数据写入一个持久化的存储介质中(hard disk或者SSD)。不同的数据库有不同的做法,但是大同小异,基本上都是在Write-Ahead Log(WAL)的基础上针对刷盘时机,刷盘的数据块大小,log的组织形式等做出一系列的调整。所谓WAL,即在真正的数据写入之前,首先持久化log中的记录,这样即使在数据提交的过程中出现各种各样的问题,我们也可以从log的记录中将数据恢复回来。

持久性的实现

[todo]

数据库的隔离级别及存在的各种问题

Read Committed

Read Comitted隔离级别的实现主要是要保证两点,一是不能有dirty read,二是不能有dirty write。对于dirty write,一个最直观的做法就是行锁(这里的锁是implicit locking,也就是数据库自动上锁,不需要显式的写在sql语句中)。对于dirty read,加锁也可以保证,但是会损失一部分的性能。因为如果有一个很大的写事务,随后的很多读会被阻塞住。所以对于大部分数据库而言,解决dirty read的方式是:如果一个数据被修改,那么就先把它修改之前的值记录下来供其他的事务读,这样就不会有冲突。(注意这里是只记录先前的一个数据,要和下面snapshot isolation的mvcc做区分)。

Snapshot Isolation / Repeatable Read

Snapshot Isolation最重要的一个特性就是读不阻止写,写不阻止读。它通过在事务开始的时候打一个快照,来保证这个事务的过程中看到的内容是一致的,同时通过MVCC解决了不可重复读的问题。

上面在Read Committed的部分说过,对于dirty read的问题,数据库会记录一个修改之前的数据来让其他的事务读,这里MVCC可以看作是它的泛化的版本。实际上,数据库通常也会用MVCC来支持read committed,例如MySQL。MVCC的实现主要是通过以下的几个方面(这里讲的是MySQL的实现,pg的实现略有不同但是总体的概念是差不多的):

  • trx_id
    每一个事务会被分配一个系统自增的事务id,这个事务id在读的时候用来决定哪些数据是可以读到的,哪些数据需要被隐藏。通常来说,在事务A开始的时候,其他没有被commit的事务会被记录下来,这些事务随后的修改不会被事务A看到。另外,任何事务id大于A的事务id的事务中的修改都不会被A看到。

  • ReadView
    ReadView是真正决定在不同隔离级别下,数据可见性的重要一环。ReadView的数据结构主要包括三个部分:事务开始时全局的活跃事务id list,当前已经被创建的事务中最早一个还没被提交的事务id(up_limit_id)和当前除了自己之外已经创建的最大的事务id(low_limit_id)。判断一条数据可不可见的规则如下:

    1. 小于up_limit_id的事务一定不可见
    2. 大于low_limit_id的事务一定可见
    3. 如果在它们之间,则查找是否在list中。如果在,则说明在这个readview创建的时候这条数据还在被修改,因此不可见,否则可见。

    那么为什么都用ReadView,RR可以保证可重复读,RC却没办法呢?这就是因为两者生成ReadView的时机不同。对于RC来说,每一次select都会重新生成一个ReadView,所以如果两个select之间有其他事务提交,读取到的数据就不一样了。而对于RR来说则是首次select的时候就生成,剩下的select都会复用这一个ReadView。

    在MySQL中,每一个活跃的事务都会被挂到trx_sys的事务链表中,每一次ReadView的创建都会首先获取trx_sys的mutex,然后遍历整个事务链表来记录活跃的事务id,然后生成ReadView。显而易见的,这样做的缺点在于每一次获取和释放锁的开销是非常大的,即使事务链表的长度为0。所以在MySQL的后续版本中,使用空闲链表的方式对可以重复使用的ReadView进行管理,减少了获取trx_sys的锁的开销。

    另外一个优化在于,每一次生成ReadView都要进行一次事务链表的遍历,这样做的开销也是很大的。针对这样的场景,MySQL的优化方式是将事务id放在一个单独的活跃事务数组中单独管理,这样每一次创建视图只需要执行一次memcpy即可。

  • Undo Log
    这个前面有详细的讲过,这里就不过多赘述了。
    对于MySQL来说,repeatable read实际上就是通过Snapshot Isolation来实现的,它解决不了lost update的问题。实际上,除了serializable,其他的隔离级别也都解决不了这个问题。

Serializable

MySQL使用了2PL来实现serializability。除此之外,还有几种其他的方式来实现serializable级别的隔离。

  • 2PL
    [todo]

  • SSI
    [todo]

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dabtwice

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值