【MySQL数据库】InnoDB存储引擎:事务原理redolog、undolog与版本控制MVCC
上文我们提到,事务有ACID四个特性,分别是:
原子性、一致性、隔离性、持久性。
那么为什么只有InnoDB支持这些特性呢?既然是“特”性,那么就会有一些不同于其它引擎的机制来保证这些特性。本文就主要学习一下事务的原理。
事务原理
事务持久性的秘密:redo log
redo log是保证持久性的核心机制。

InnoDB的内存架构分为内存结构和磁盘结构。
传送门:InnoDB存储引擎:逻辑存储结构、内存架构、磁盘架构_innodb引擎的数据结构
当一个事务执行insert、update、delete等非查询操作然后commit提交事务后,会到BufferPool中看看是否缓冲池中有操作的表的缓存,如果没有则会开启后台线程从磁盘中将表数据加载到缓冲池的某个缓冲区中(如果原本缓冲池就有这个表的缓冲区则省略该步骤),然后根据sql语句将BufferPool中的表数据修改,开启后台线程,将新的表数据刷新到磁盘中。
但是,当缓冲区修改完后,就会给用户提示执行成功,然后后台刷新到磁盘的时候发生了异常,MySQL服务宕掉了,那么这些数据就不能成功刷到磁盘中,就会导致数据不一致。
为了解决这个问题,在内存中加入了redo log buffer,磁盘架构中对应有redo log file;当修改完BufferPool后,向redo log buffer中也写一条记录,然后直接将redo log buffer的内容刷新到磁盘的日志文件中,然后再提示用户提交事务成功。最后后台线程将缓冲区数据刷新到磁盘中,如果这时候出错了,就按照磁盘中的redo log file 再次刷新。
redo log是基于双文件,循环写的,当日志空间满时,日志会被新的记录覆盖。
现在可能有一个问题:那我一提交就写日志,保证日志记录刷到磁盘中;为什么不直接将缓冲区脏页刷新到磁盘,然后再提示用户成功呢,为什么多此一举,非要经过redo log绕一圈呢?
回答:
因为在磁盘中访问一个表存储的某行记录,是随机访问,效率低。
而日志只需要追加一条日志记录就可以了,是顺序访问,效率高。
而这种方式也是一个技术,叫做:WAL(Write-Ahead Logging),即先写日志。
redo log的核心目的是:在事务提交时,先保证日志持久化(redo log刷盘),再返回用户成功,从而解决崩溃时数据不一致的问题。
事务原子性的秘密:undo log
undo log是保证原子性的核心机制。
回滚日志(undo log):用于记录数据被修改前的信息,作用包含两个【rollback、MVCC】
undolog和redolog不一样,redolog是物理日志,在磁盘文件中确确实实存在,而undolog是逻辑日志。
逻辑日志:简单理解为记录的就是sql语句
物理日志:记录的是数据页的变更,存储在磁盘文件中
对于undolog,作为逻辑日志,可以这么理解:
当delete数据时,undolog就会记录一条对应的insert记录,反之亦然
当update数据时,undolog就会记录一条相反的update记录
当rollback回滚时,就可以从undolog中的逻辑记录读到相应的内容并进行回滚
但实际上不是这样,而是在操作后,将这一行的旧数据记录下来,再将新的放上去,最后将这些历史数据穿成链表,当回滚时,就会回到当时的那个版本的数据行去。【也就是undo log版本链】
undo log版本链具体怎么工作的,在MVCC中会有说明。
undo log存储:采取段的方式进行管理和记录,存放在rollback segment回滚段中,内部包含1024个undo log segment。
undo log销毁:undo log在事务执行时产生,事务提交后,并不会立即删除undo log,因为这些日志可能还会用于MVCC,要进行检查一番,如果不用于MVCC才删除。
事务一致性的原理:由redo log和undo log共同保证。
MVCC:多版本并发控制机制
MVCC:Multi-Version Concurrency Control。它通过版本管理,来实现事务的隔离性,允许读写操作同时进行,提高数据库的并发性能和响应能力。
并发控制分类
当前读:读取的是记录的最新版本,读取时还要保证其它并发事务不能修改当前记录,会对读取的记录进行加锁,对于大多数操作: select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排他锁)都是一种当前读。
通过next-key lock(记录锁+间隙锁)的方式解决了幻读,因为当执行select ... for update语句时,会加上next-key lock,如果有其它事务在next-key lock锁范围内插入一条记录,那么这条记录就会被阻塞无法插入,所以就很好的避免了幻读的问题。【传送:锁机制】
快照读:简单的select(不加锁)就是快照读,快照读读取到的是记录数据的可见版本,有可能是历史数据,由于不加锁,所以是非阻塞读。
通过MVCC方式解决幻读,因为 “可重复读” 的隔离等级下,事务执行过程中看到的数据,一致跟这个事务启动时看到的数据是一致的;即使中途其它事务向这个表插入了一条数据,在该事务中也select不出来其它事务插入的数据,所以很好的避免了幻读问题。
{
ReadCommitted(RC):每次select生成一个快照读
RepeatableRead(RR):开启事务后第一个select产生
Serializable:快照读会退化为当前读,无锁并发退化为有锁并发,并发效率降低
}
MVCC的具体实现,依赖于三个内容:
三个隐式字段
每个数据表在创建时,会有三个隐藏的字段。
$ibd2sdi store_by_withoutkey.ibd//省略"columns": [{"name": "sname",//省略},{"name": "sgender",//省略},{"name": "DB_ROW_ID",//省略},{"name": "DB_TRX_ID",//省略},{"name": "DB_ROLL_PTR",//省略}],
我们可以看到除了建表设计的两个字段,额外生成了三个字段:
db_row_id, db_trx_id, db_db_roll_ptr;
首先介绍第一个:db_row_id(行ID)
这个隐藏字段不是所有的表都会有的,主要是取决于设计表结构时,MySQL是否能找到一个显式的字段作为主键,如果找不到,才会生成这么一个隐藏字段作为主键。因为InnoDB必定会有一个聚簇索引组成的B+树存在,那也就是一个表必须有一个主键。上面我们展示的是,没有建主键索引时表的字段组成,如果我们建一个带索引的表,再次使用工具ibd2sdi查看,则会看到,文件中压根就没有这个字段了。【根据命令自行测试吧】
接着介绍第二个:db_trx_id(事务id)
这个隐藏字段是每个表都会有的,哪个事务修改的这条行数据,那么事务id就是这个事务的id,(InnoDB中,事务id是自增的,下一次的事务id一定比之前的事务id的值要大)
最后介绍第三个:db_roll_ptr(回滚指针)↘↓↓↓↙
undo log版本链
我们可以看到,表中的记录中这个db_roll_ptr指针存储的是修改前的记录在undolog中的地址,而修改前的记录中的db_roll_ptr存储的是再之前的记录,依次往前推,最后,形成了一个undo log版本链。当某个事务查询时,走快照读,则会根据一定的规则在这个链表中找到自己应该查询到的数据。而记录中的数据是最新的数据,当前读读到的都是这个版本。
ReadView-读视图
具体要返回版本链中哪个数据,并不由undo log决定,而是由ReadView决定的:
ReadView是快照读SQL执行时MVCC提取数据的依据,记录并维护了系统当前活跃的事务(未提交的)id。ReadView包含四个核心字段:
m_ids:当前活跃的事务ID的集合
min_trx_id:最小活跃事务ID
max_trx_id:预分配事务ID,当前最大事务ID+1(因为事务ID是自增的)
creator_trx_id:ReadView创建者的事务ID
版本链数据访问规则:
tips: trx_id:当前undolog记录对应的当前事务ID
law1. if(trx_id == creator_trx_id)可以访问该版本
【如果if成立,就说明数据是当前事务更改的,自然可以访问】
law2. if(trx_id < min_trx_id)可以访问该版本
【如果成立,就说明数据已经提交了】
law3. if(trx_id >= max_trx_id)不能访问该版本
【如果成立,说明该事务是在ReadView生成后开启的】
law4. if(min_trx_id<= trx_id < max_trx_id)如果trx_id不在m_ids中就可以访问该版本
【如果成立,已提交】
事务隔离性的原理:由MVCC和锁机制共同保证。
【传送:锁机制】
!感谢阅读!