MVCC(Multi-Version Concurrency Control多版本并发控制)。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。
简单理解:并发访问数据库时(读和写),对正在事务内处理的数据做多版本的管理,以达到用来 避免写操作的拥塞,从而提升读操作的并发问题(基于undo log快照读来解决)。
在数据库创建表时,比如有id
、name
、age
三个字段,但是InnoDB的内部实现中为每一行数据增加了三个隐藏列用于实现MVCC。
列名 | 长度(字节) | 作用 |
---|---|---|
DB_TRX_ID | 6 | 插入或更新行的最后一个事务的事务标识符(版本号)。(删除视为更新,将其标记为已删除) |
DB_ROLL_PTR | 7 | 写入回滚段的撤消日志事务标识符(版本号)(若行已更新,则撤消日志记录包含在更新行之前重建行内容所需的信息) |
DB_ROW_ID | 6 | 行标识(自增id,也就是当我们建表没有指定主键列时,mysql会自动生成DB_ROW_ID用作主键) |
所以,我们创建表的真正结构,其实是下面这个样子的:
id | name | age | DB_ROW_ID | DB_TRX_ID | DB_ROLL_PTR |
---|---|---|---|---|---|
InnoDB引擎,通过为每一行记录添加两个额外的隐藏的值( DB_TRX_ID、DB_ROLL_PTR )来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。并不会存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。下文中,我们就只拿这两个字段+事务版本号来做介绍了。
MVCC在MySQL执行插入操作时,InnoDB会为这个新的数据行,DB_TRX_ID
字段记录当前事务版本号执行流程图如下图所示:
MVCC在MySQL执行删除操作时,InnoDB会为要删除的这个行,DB_ROLL_PTR
字段记录当前删除(回滚)的事务版本号。执行流程图如下所示:
MVCC 在 MySQL 执行修改操作时,InnoDB 会写一个这行数据的新拷贝,这个拷贝的版本号为当前的事务版本号(DB_TRX_ID
字段);同时它会将这个事务版本号写到旧行的删除(回滚)版本号中( DB_ROLL_PTR
字段)。执行流程图如下所示:
MVCC在MySQL执行查询操作时,InnoDB查询数据有两条规则:
数据行 事务版本号<=当前事务ID
的数据行;删除(回滚)版本号为NULL
或者删除(回滚)版本号大于当前事务ID
的数据行。有两个事务1、2 对数据进行操作。①②③④为执行步骤序号
(1)情形一
按照步骤 ①②③④②执行操作。流程图如下所示:
(2) 情形二
按照步骤 ③④①②执行操作。流程图如下所示:
(3) 情形分析
我们发现,两个事务A、B 在两种情形下执行相同的操作,但是查询却出现了不同的结果。那么这问题出在了哪里?我们继续向下分析。其实这问题并不是出在 MVCC 上,针对这种情况,InnoDB 引擎又是怎么解决这一问题的呢??这就引出了:undo Log、redo Log 这两个概念。
在介绍Undo之前,遇到两个问题:(1)加X锁写操作阻塞时,那么数据库在高并发时肯定不行的;(2)使用MVCC控制版本时,又发现一个重复读导致的数据不一致问题。此时:InnoDB引擎就是通过引入undo log,通过读快照的方式来解决这两个问题的。
undo log 的出现,既可以主要解决数据原子性问题的同时,又可以配合 MVCC 间接解决高并发下的读操作阻塞问题。
undo log是指在事务开始之前、操作数据之前,首先将需要操作的数据备份到一个地方的过程。
undo log的出现是为了实现事务的原子性而出现的产物,事务在处理的过程中,如果出现了错误或用户执行了rollback。MySQL可以利用undo log中的备份,将数据恢复到事务开始之前的状态。
在InnoDB引擎中,undo log和MVCC一起来实现MySQL数据的多版本控制。在事务提交之前,undo保存了未提交事务之前的版本到undo log,undo log中的数据可以作为数据旧版本快照来供其它并发事务进行快照读。
基于 undo log,可以将 undo log 中的数据作为旧版本快照的方式,来供其他并发事务进行快照读。(这就是 MVCC 配合 undo log 来解决读操作的并发问题)
Ⅰ.快照读 (通过MVCC + undo log 解决幻读问题)
SQL 读取的数据时快照版本,也就是历史版本。**普通的 Select 就是基于快照读的方式 。**InnoDB 快照读,数据的读取将由 cache(原本数据) + undo(事务修改过的数据)两部分组成。
Ⅱ.当前读 (通过Next-key lock 解决幻读问题)
SQL 读取的数据时最新版本。通过 锁机制 来保证读取的数据无法通过其他事务进行修改。常用的 update、delete、insert、select xxx LOCK IN SHARE MODE(共享锁)、select xxx FOR UPDATE(排他锁) 都是当前读。
了解了 undo log 之后,我们 基于 undo log + MVCC
再来按步骤 ③④①② 分析执行结果,图示如下:
执行流程:
如果当前事务在执行过程中,MySQL服务崩溃了,重启时则会从undo log日志中恢复;如果如果断电了,重启后还会根据undo日志进行恢复。此操作具有幂等性,重复多少次都没有问题。
redo log是指事务中操作的任何数据,将最新的数据备份到另一个地方的过程。redo log是为了实现事务的持久性而出现的产物。redo log的持久性,并不是随着事务的提交才写入,而是在事务的执行的过程中,便开始写入到redo buffer中,最后以redo log文件方法flush到磁盘中。
redo log的出现,就是放在在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。
引入 redo log 机制,InnoDB引擎认为数据只要写入到 redo log 成功,就认为当前事务已经完成,而不是要求你将数据全部 flush 到磁盘(*.IBD 文件)才算事务完成。如果每次修改都 flush 数据到磁盘,那显然是很消耗时间的;但是如果你将日志写入到 redo log中,根据 redo log 落盘策略对数据进行保存,显然效率会高很多(即:将耗时的操作放到后台去操作)。
如果此时 MySQL 服务异常、断电等情况,在重启时 MySQL 便会读取 redo log 中的数据来进行恢复。此时你或许会有这样一个疑惑:redo buffer 中的数据还没有来得及写入到 redo log 这部分的数据,不会丢吗?此处就涉及到了 redo buffer 数据 落盘到 redo log 的策略问题。
bin log二进制日志:
作用:用于复制,在主从复制中,从库利用主库上的binlog日志进程重播,从而实现主从同步。
用于数据库的基于时间点的还原。
undo log回滚日志:
作用:保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读;
redo log重做日志:
作用:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。