为什么要引入undolog
事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
程序员可以在事务执行过程中手动输入ROLLBACK语句结束当前的事务的执行。
事务ID的处理
一个事务可以是一个只读事务,或者是一个读写事务。
通过STARTTRANSACTIONREADONLY语句开启一个只读事务。只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对用户临时表做增、删、改操作。
我们可以通过STARTTRANSACTIONREADWRITE语句开启一个读写事务,或者使用BEGIN、STARTTRANSACTION语句开启的事务默认也算是读写事务。在读写事务中可以对表执行增删改查操作。
如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id
事务分配id的方式
对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务id,否则的话是不分配事务id的。
对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则的话也是不分配事务id的。
事务id生成机制
事务id本质上就是一个数字,它的分配策略和我们前边提到的对隐藏列row_id(当用户没有为表创建主键和UNIQUE键时InnoDB自动创建的列)的分配策略基本相同。
服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务id时,就会把该变量的值当作事务id分配给该事务,并且把该变量自增1。
每当这个变量的值为的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称之为MaxTrxID的属性处,这个属性占用8个字节的存储空间。
当系统下一次重新启动时,会将上边提到的MaxTrxID属性加载到内存中,将该值加上之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于MaxTrxID属性值)。
这样就可以保证整个系统中分配的事务id值是一个递增的数字。先被分配id的事务得到的是较小的事务id,后被分配id的事务得到的是较大的事务id。
trx_id隐藏列
聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_id、roll_pointer的隐藏列,如果用户没有在表中定义主键以及UNIQUE键,还会自动添加一个名为row_id的隐藏列。
trx_id列就是某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id
roll_pointer:本质上就是一个指向记录对应的undo日志的一个指针。比方说我们向表里插入了2条记录,每条记录都有与其对应的一条undo日志。记录被存储到了类型为FIL_PAGE_INDEX的页面中(就是我们前边一直所说的数据页),undo日志被存放到了类型为FIL_PAGE_UNDO_LOG的页面中。roll_pointer本质就是一个指针,指向记录对应的undo日志。
undo日志的格式
为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志。
一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、...、第n号undo日志等,这个编号也被称之为undoNO。
我们前边说明表空间的时候说过,表空间其实是由许许多多的页面构成的,页面默认大小为16KB。这些页面有不同的类型,其中有一种称之为FIL_PAGE_UNDO_LOG类型的页面是专门用来存储undo日志的。也就是说Undopage跟储存的数据和索引的页等是类似的。
FIL_PAGE_UNDO_LOG页面可以从系统表空间中分配,也可以从一种专门存放undo日志的表空间,也就是所谓的undotablespace中分配。先来看看不同操作都会产生什么样子的undo日志。
当我们向某个表中插入一条记录时,实际上需要向聚簇索引和所有的二级索引都插入一条记录。不过记录undo日志时,我们只需要考虑向聚簇索引插入记录时的情况就好了,因为其实聚簇索引记录和二级索引记录是一一对应的,我们在回滚插入操作时,只需要知道这条记录的主键信息,然后根据主键信息做对应的删除操作,做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉。
INSERT操作对应的undo日志
当我们向表中插入一条记录时最终导致的结果就是这条记录被放到了一个数据页中。如果希望回滚这个插入操作,那么把这条记录删除就好了,也就是说在写对应的undo日志时,主要是把这条记录的主键信息记上。InnoDB的设计了一个类型为TRX_UNDO_INSERT_REC的undo日志。
DELETE操作对应的undo日志
在页上有一个垃圾列表:记录删除后的数据,占用的存储空间可以被重新利用。
我们操作的删除是将将记录的delete_mask标识位设置为1,这个阶段称之为deletemark。
当该删除语句所在的事务提交之后,会有专门的线程后来真正的把记录删除掉(从正常记录链表中移除,并且加入到垃圾链表中)。
UPDATE操作对应的undo日志
不更新主键的情况:更新前后的列占用的存储空间一样大,直接更新。占用空间不一致,先删除掉旧记录,再插入新记录。
更新主键的情况:将旧记录进行deletemark操作,创建一条新记录。
崩溃后的恢复为什么不用binlog?
核心点:binlog记录结果。redolog记录过程
这两者使用方式不一样
binlog会记录表所有更改操作,包括更新删除数据,更改表结构等等,主要用于人工恢复数据.
redolog对于我们是不可见的,它是InnoDB用于保证crash-safe能力的,也就是在事务提交后MySQL崩溃的话,可以保证事务的持久性,即事务提交后其更改是永久性的。
一句话概括:binlog是用作人工恢复数据,redolog是MySQL自己使用,用于保证在数据库崩溃时的事务持久性。
redolog是InnoDB引擎特有的,binlog是MySQL的Server层实现的,所有引擎都可以使用。
redolog是物理日志,记录的是“在某个数据页上做了什么修改”,恢复的速度更快;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这的c字段加1”;
redolog是“循环写”的日志文件,redolog只会记录未刷盘的日志,已经刷入磁盘的数据都会从redolog这个有限大小的日志文件里删除。binlog是追加日志,保存的是全量的日志。
最重要的是,当数据库crash后,想要恢复未刷盘但已经写入redolog和binlog的数据到内存时,binlog是无法恢复的。虽然binlog拥有全量的日志,但没有一个标志让innoDB判断哪些数据已经入表(写入磁盘),哪些数据还没有。
同时写Redo和Binlog怎么保持一致?
核心点:使用了两阶段事务
当事务提交时InnoDB存储引擎进行prepare操作。
MySQL上层会将数据库、数据表和数据表中的数据的更新操作写入BinLog文件。
InnoDB存储引擎将事务日志写入RedoLog文件中。
Redo日志和Undo日志的关系
数据库崩溃重启后,需要先从redolog中把未落盘的脏页数据恢复回来,重新写入磁盘,保证用户的数据不丢失。当然,在崩溃恢复中还需要把未提交的事务进行回滚操作。由于回滚操作需要undolog日志支持,undolog日志的完整性和可靠性需要redolog日志来保证,所以数据库崩溃需要先做redolog数据恢复,然后做undolog回滚。
在事务执行过程中,除了记录redo一些记录,还会记录undolog日志。Undolog记录了数据每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undolog进行回滚操作。
因为redolog是物理日志,记录的是数据库页的物理修改操作。所以undolog(可以看成数据库的数据)的写入也会伴随着redolog的产生,这是因为undolog也需要持久化的保护。
事务进行过程中,每次sql语句执行,都会记录undolog和redolog,然后更新数据形成脏页。事务执行COMMIT操作时,会将本事务相关的所有redolog进行落盘,只有所有的redolog落盘成功,才算COMMIT成功。然后内存中的undolog和脏页按照同样的规则进行落盘。如果此时发生崩溃,则只使用redolog恢复数据。