介绍
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种用于实现数据库并发访问控制的机制。它允许多个用户同时读写同一数据项,从而提高了数据库在高并发环境下的性能和响应速度。以下是具体介绍:
- 基本概念:
- MVCC通过为每行数据维护多个版本来工作,每个版本都有自己的创建时间和事务ID。当一个事务需要读取数据时,它会读取对应版本的数据,而不是最新的数据。
- MVCC避免了使用传统的锁机制来管理数据的并发访问,减少了锁竞争,从而提升了系统的吞吐量和响应时间。
- 工作机制:
- MVCC中,每个事务开始时都会创建一个ReadView,这是一个数据快照,记录了当前活跃的所有事务ID。当事务进行查询操作时,它会利用这个快照来判断哪些数据是可见的。
- 对于INSERT、UPDATE或DELETE操作,系统会创建新的数据版本,并链接到旧版本上形成版本链。每个版本都保存了创建该版本的事务ID和时间戳。
- 关键组件:
- 系统中的每个事务都会被分配一个唯一的事务ID。这些ID用于追踪数据的版本以及决定哪些版本对特定事务是可见的。
- 除了用户定义的数据列外,数据库还会为每个记录维护一些隐藏的元数据列,如行ID、事务ID和回滚指针等。这些信息用于支持MVCC的内部运作。
- 隔离级别:
- 在读已提交(READ COMMITTED)和可重复读(REPEATABLE READ)隔离级别下,MVCC通过生成数据的时间点快照来避免脏读、不可重复读和幻读现象的发生。不同的隔离级别决定了事务如何定义它们的ReadView。
- 应用场景:
- MVCC特别适用于具有高读/写比率的数据库环境,如联机事务处理系统(OLTP),可以显著提高系统的并发性能和数据一致性。
- 优缺点:
- 提高了并发处理能力,减少了等待锁释放的时间。
- 降低了死锁的风险,因为减少了锁的使用。
- 增加了系统的复杂性,可能会增加设计和调试的难度。
- 在某些情况下,可能需要额外的存储空间来保存多版本的数据。
总的来说,MVCC是现代关系型数据库管理系统中一个至关重要的功能,它通过允许并行访问数据的方式来优化数据库的性能。理解其工作原理和实现方式有助于更好地设计和维护数据库应用,特别是在需要处理大量并发事务的环境中。
解决的MySQL问题背景
1.1 什么是数据库事务,为什么要有事务
事务,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。
假如A转账给B 100 元,先从A的账户里扣除 100 元,再在 B 的账户上加上 100 元。如果扣完A的100元后,还没来得及给B加上,银行系统异常了,最后导致A的余额减少了,B的余额却没有增加。所以就需要事务,将A的钱回滚回去,就是这么简单。
为什么要有事务呢? 就是为了保证数据的最终一致性。
1.2 事务包括哪几个特性?
事务四个典型特性,即ACID,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部都执行,要么都不执行。
- 一致性:指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
- 隔离性:多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务干扰,多个并发事务之间要相互隔离。。
- 持久性:表示事务完成提交后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。
1.3 事务并发存在的问题
1.3.1 脏读
当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
1.3.2 不可重复读
比如在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
1.3.3 幻读
幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读,他和不可重复读的主要区别在于一个每次读取是数据不一样,一个是读取的数量不一样。
1.4 四大隔离级别
为了解决并发事务存在的脏读、不可重复读、幻读等问题,数据库大叔设计了四种隔离级别。分别是读未提交,读已提交,可重复读,串行化(Serializable)。
1.4.1 读未提交
读未提交隔离级别,只限制了两个数据不能同时修改,但是修改数据的时候,即使事务未提交,都是可以被别的事务读取到的,这级别的事务隔离有脏读、重复读、幻读的问题;
1.4.2 读已提交
读已提交隔离级别,当前事务只能读取到其他事务提交的数据,所以这种事务的隔离级别解决了脏读问题,但还是会存在重复读、幻读问题;
1.4 3 可重复读
可重复读隔离级别,限制了读取数据的时候,不可以进行修改,所以解决了重复读的问题,但是读取范围数据的时候,是可以插入数据,所以还会存在幻读问题;
1.4.4 串行化
事务最高的隔离级别,在该级别下,所有事务都是进行串行化顺序执行的。可以避免脏读、不可重复读与幻读所有并发问题。但是这种事务隔离级别下,事务执行很耗性能。
1.5 数据库是如何保证事务的隔离性的呢?
数据库是通过加锁,来实现事务的隔离性的。这就好像,如果你想一个人静静,不被别人打扰,你就可以在房门上加上一把锁。
加锁确实好使,可以保证隔离性。比如串行化隔离级别就是加锁实现的。但是频繁的加锁,导致读数据时,没办法修改,修改数据时,没办法读取,大大降低了数据库性能。
那么,如何解决加锁后的性能问题的?
答案就是,MVCC多版本并发控制!它实现读取数据不用加锁,可以让读取数据同时修改。修改数据时同时可读取。
MVCC实现原理
主要依赖于记录中的三个隐藏字段、undolog,read view来实现的。
1、隐藏字段
每行记录,除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段:
- DB_ROW_ID:6字节,隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个6字节的row_id
- DB_TRX_ID:6字节,最近修改事务id,记录创建这条记录或者最后一次修改该记录的事务id
- DB_ROLL_PTR:7字节,回滚指针,用于配合undo日志,指向上一个旧版本
假设有一条数据如下:
2、undolog
2.1 概念
回滚日志,表示在进行insert,delete,update操作的时候产生的方便回滚的日志。
2.2 说明
- 当进行insert操作的时候,产生的undolog,只在事务回滚的时候需要用到,并且在事务提交之后可以被立刻丢弃
- 当进行update和delete操作的时候,产生的undolog,不仅仅在事务回滚的时候需要,在快照读的时候也需要,所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
当数据发生更新和删除操作的时候,实际只是设置了旧记录的deleted_bit,并不是将过时的记录删除,因为为了节省磁盘空间,innodb有专门的purge线程来清除deleted_bit为true的记录,如果某个记录的deleted_id为true,并且DB_TRX_ID相对于purge线程的read view 可见,那么这条记录就是可以被清除的。
2.3 undolog生成的记录链表
(1)假设有一个事务编号为1的事务向表中插入一条记录,那么此时行数据如下,主键id=1,事务id=1
(2)假设有第二个事务(编号为2)对该记录的name做出修改,改为lisi
底层操作:在事务2修改该行记录数据时
1、对该数据行加排他锁
2、把该行数据拷贝到undolog中,作为旧记录
3、修改该行name为lisi,并且修改事务id=2,回滚指针指向拷贝到undolog的副本记录中
4、提交事务,释放锁
(3)假设有第三个事务(编号为3)对该记录的age做了修改,改为32
底层操作:在事务3修改该行记录数据时
1、对该数据行加排他锁
2、把该行数据拷贝到undolog中,作为旧记录,发现该行记录已经有undolog了,那么最新的旧数据作为链表的表头,插在该行记录的undolog最前面
3、修改该行age为32岁,并且修改事务id=3,回滚指针指向刚刚拷贝的undolog的副本记录
4、提交事务,释放锁
上述的一系列图中,可以发现,不同事务或者相同事务的对同一记录的修改,会导致该记录的undolog生成一条记录版本链表,undolog的表头就是最新的旧记录,表尾就是最早的旧记录。
3、read view
Read View是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,系统会生成一个此刻的快照,记录并维护系统此刻活跃事务的id,用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取到的是当前行记录的undolog中某个版本的数据
1)可见性算法
将要被修改的数据的最新记录中的DB_TRX_ID(当前事务id)取出来,与系统此刻其他活跃事务的id去对比,如果DB_TRX_ID跟Read View的属性做了比较,不符合可见性,那么就通过DB_ROLL_PTR回滚指针去取出undolog中的DB_TRX_ID做比较,即遍历链表中的DB_TRX_ID,直到找到满足条件的DB_TRX_ID,这个DB_TRX_ID所在的旧记录就是当前事务能看到的数据。
2)可见性规则
首先要知道Read View中的三个全局属性:
- trx_list:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID(1,2,3)
- up_limit_id:记录trx_list列表中事务ID最小的ID(1)
- low_limit_id:Read View生成时,系统即将分配的下一个事务ID(4)
具体的比较规则如下:
- 首先比较DB_TRX_ID < up_limit_id
如果小于,则当前事务能看到DB_TRX_ID所在的记录
如果大于等于,则进入下一个判断 - 接下来判断DB_TRX_ID >= low_limit_id
如果大于等于,则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那么对于当前事务不可见
如果小于,则进入下一步判断 -
判断DB_TRX_ID是否在活跃事务中,trx_list包含DB_TRX_ID
如果包含,则代表在Read View生成的时候,这个事务还是活跃状态,未commit的数据,当前事务也是看不到,如果不包含,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。
流程图如下:
总结:两种情况可见
- DB_TRX_ID < up_limit_id
- DB_TRX_ID不在trx_list范围内,且小于low_limit_id
拓展 :在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View。