mysql undo log研究
undo log基础
大家都知道,数据库的四个隔离级别。有一个情况大家也熟悉:即RC和RR两种隔离级别下的不同可见性,即不可重复读问题。
不可重复读的含义是事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据做了更新并提交,导致事务A多次读取时数据不一致
在RC隔离级别下,伪代码
session1
start transaction;
session2
start transaction;
session1先读取一次,是1200
session2加了300,之后commit
session1再读取一次,是1500
如果session1基于1200进行了操作,就可能造成数据紊乱的结果
而在RR隔离级别下,结果
会发现session1读取的结果一致都是第一次start transaction之前数据的值,在整个session过程中不变,比如说都是1200
而在RR隔离级别下,如果我就在这个基础上做修改,会存在问题吗?
session2 1500
session1 read 仍是1200,但其执行
UPDATE account_innodb SET balance = balance - 100 WHERE id = 1;
commit;
再查询,结果是1400,是正确的,而不是我们之前预想的1100
这个不可重复读的问题,或者说是RC、RR下innodb的快照读/非阻塞读是如何实现的呢?
答案就是由undo log来提供支持的。
undo log用来实现事务的一致性,即事务ACID中的C–consistence,支持回滚和MVCC多版本控制
undo log是记录到undo page中的,默认存放在 ibdata1中,即系统表空间中。表空间内部由多个segment段对象(逻辑概念)组成,每个段由extend区(逻辑概念)组成,每个区由页(物理概念)组成,在每个页中保存数据。
undo log的工作方式
-
背景知识1: mysql数据行里的DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID字段
- DB_TRX_ID字段标识最近一次对本行记录做修改,insert或update的事务id,至于说delete操作,在innodb看来,也不过是一次update操作,有一个deleted的隐藏列
- DB_ROLL_PTR (rollback pointer),即回滚指针,指写入回滚段rollback segment的undo日志记录,如果一行记录被更新,则undo log record包含重建该行记录被更新之前内容所必须的信息
- DB_ROW_ID(当innodb引擎没有任何索引时,会自动创建的隐藏的主键列)包含一个随着新行插入而单调递增的行ID,当由innodb自动产生聚集索引时,聚集索引会包括行ID的值,否则这个行ID不会出现在任何索引中
-
背景知识2: undo日志
当我们对记录做了变更操作时,就会产生undo日志。
undo日志中存储的是老版数据,当一个旧的事务要去读取数据时,为了能够读取到老版本的数据,需要顺着undo列找到满足其可见性的记录。
undo log主要分为两种:insert undo log和update undo log。
其中,insert undo log表示的是事务对insert新纪录产生的undo log,只在事务回滚时需要,并且可以在事务提交时就可以丢弃,其不是讲解的重点。
重点是update undo log,事务对记录进行update或者delete操作时会产生update undo log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
undo log日志的工作方式简化的演示,这里只是显示了事务对行记录的更新过程(innodb在内部做了非常多的工作)
上图表示我们对DB_ROW_ID=1的行做了update,这一行被事务A做了修改,将原来field2里面的值由12更新为了32。
其修改的过程是这样的:
-
首先用排他锁锁定该行
-
然后把该行修改前的值copy一份到undo log里面
-
之后修改当前行的值,填写事务id到DB_TRX_ID,使用DB_ROLL_PTR回滚指针指向undo log中修改前的行
在这之后,如果数据库中还有别的事务再用快照读来读取该日志记录,那么对应的undo log还没有被清除,此时某个事务又对同一行记录做了修改,将其fields3的值由13修改为了45
这样又会多了一条undo log记录,数据的多个版本就是这样实现的
以上就是undo log的大概样子,它按照修改的时间顺序从今到远,通过DB_ROLL_PTR给连接起来了
RC、RR级别下的innodb的快照读/非阻塞读如何实现
首先是read view的概念,主要是用来做可见性判断的,即当我们去执行快照读select的时候,会针对我们查询的数据创建出一个read view,来决定当前事务能看到的是哪个版本的数据。有可能是当前最新版本的数据,也有可能是undo log中某个版本的数据。
read view遵循一个可见性算法,主要是将要修改数据的DB_TRX_ID取出来,与系统其它活跃事务ID做对比,如果大于或者等于这些ID的话,就通过DB_ROLL_PTR指针去取出undo log上一层的DB_TRX_ID,直到小于这些活跃事务ID为止,这样就保证了我们当前取到的数据版本是当前可见的最稳定的版本。
mysql中的源码
可以看到其有m_low_limit_id和m_up_limit_id
每当我们start transaction的时候,事务id都会递增,也就是说越新开启的事务,其id就越大。
我们主要就是通过这两个值,去和我们的DB_TRX_ID做对比,进而决定让他是不是去回溯到我们的undo log,去取出适应该版本的一个数据的版本来。
总结:正是因为生成时机的不同,造成了RC、RR两种隔离级别下的不同可见性。
在repeatable read级别下,session在start transaction后的第一条快照读,会创建一个快照,即read view,将当前系统中活跃的其他事务记录起来,此后再调用快照读的时候,还是使用的同一个read view。而在read committed级别下,事务中每条select语句,每次调用快照读的时候,都会创建一个新的快照,这就是为什么之前,我们在RC级别下,能用快照读看到别的事务已经提交到的对表记录的增删改。而在RR级别下,如果首次使用快照读,是在别的事务对数据库记录进行增删改并提交之前的,此后即便别的事务对记录进行了增删改并提交,还是读不到数据的变动的原因。对RR来说,首次事务select的时机是相当重要的。
所以,在RC下可以看到两次select的结果不同,而在RR下,都是读取同一个快照,所以每次select的结果相同
由于undo log的支持,使得innodb在RC和RR级别下支持非阻塞读,而读取数据时的非阻塞就是所谓的MVCC。而innodb的非阻塞读机制就实现了仿造版的MVCC。
MVCC就是读不加锁,读写不冲突,在读多写少的OLTP中,极大增加了系统的并发功能。为什么这里只实现了伪MVCC功能呢?并没有实现核心的多版本共存,undo log中的内容只是串行化的结果,记录了多个事务的过程,不属于多版本共存。
undo log物理存储
undo log是记录到undo page中的,默认存放在 ibdata1中,即系统表空间中。表空间内部由多个segment段对象(逻辑概念)组成,每个段由extend区(逻辑概念)组成,每个区由页(物理概念)组成,在每个页中保存数据。
rollback segment (回滚段)
-
MySQL 5.5前只有1个rollback segment
-
MySQL 5.5+ 有128个rollback segment
-
不保存任何undo log
-
仅保存undo log segment的位置
-
含有1024个undo slot
MySQL5.5中只有一个Rollback Segment,即只有1024个undo log segment,那就表示最多只有能有1024个并发事务(线程)去执行undo
如果用不到undo ,其实是可以超过1024 个线程的。哪些线程会用到undo呢?事务下的增删改会用到,在秒杀场景下是不是会有问题的?
在MySQL5.6中支持128 * 1024 个并发执行undo的线程
undo log segment(undo日志段)
实际存储undo log的对象
由undo page组成
每个undo page可以保存多个事务的undo log
最重要的是undo log中存储了哪些内容
undo log header
undo log records – undo log记录分为两种, insert 的undo和update 的undo
-
insert undo log record – 记录insert
-
update undo log record – 记录update和delete
undo log 是逻辑记录
,记录了每一行修改的值(前后项)。
undo log清理–purge线程
真正的删除记录
删除undo log
举例:表tb1 中有记录pk=1,2,3;此时delete from tb1 where pk=1;
-
将pk=1的记录标记为删除(delete-mark,info bits),数据库中pk=1的记录此时还是存在的,空间并没有被释放,该操作为同步操作(SQL执行完,也就标记完成了)。
-
purge,该部分为后台线程(purge线程)异步操作,会真正的删除该记录,且空间被释放。purge线程是系统自动的,无法人工控制。
标记为已删除的原因:
-
该事务可能需要回滚,先作保留。
-
当事务1 去删除pk=1且没有提交时, 事务2 应该要能看到pk=1的记录(事务的隔离性)。
问题1:我们既然有了undo日志,为什么还要delete-mark,然后purge呢?
在这里,我们要区分几种情况了
-
过滤条件是聚集索引
delete – 将该记录标记为delete-mark 。
update – 将该记录先物理delete(聚簇索引里主键相同的行最多只能有1个),然后insert或者可以原地更新[in place update](即使删除了,也可以通过undo进行还原)。
-
过滤条件是二级索引
delete – 将该记录标记为delete-mark 。
update – 将该记录标记为delete-mark (索引列是columns + pk,即使是唯一索引更新也是和原来的不一样),然后insert 。
问题2:为什么没有insert
- insert操作是不需要异步去purge,因为insert的记录之前是不存在的
- 不存在记录(未提交)是没有别的事务能引用到的,所以insert以后,对应的undo可以直接删除,而不需要等待异步purge
redo log与undo log区别
- redo log用来保证事务的原子性和持久性,undo log用来保证事务的一致性
- redo log和undo log都可以看做是一种恢复操作,redo恢复提交事务修改的页操作,而undo回滚行记录到某个特定版本,因此两者记录的内容不同。而且redo是物理逻辑日志,根据页进行记录(物理),记录的是页的变化(逻辑),而undo log是逻辑日志,根据每行记录进行记录
推荐阅读
-
mysql undo log研究
-
MySQL5.1主从同步出现Relay log read failure错误解决方法
-
深入研究mysql中的varchar和limit(容易被忽略的知识)
-
mysql清除log-bin日志的方法
-
MySQL5.1主从同步出现Relay log read failure错误解决方法
-
/var/log/pacct文件导致MySQL启动失败的案例分享
-
深入研究mysql中的varchar和limit(容易被忽略的知识)
-
mysql清除log-bin日志的方法
-
mysql正确安全清空在线慢查询日志slow log的流程分享
-
开启bin-log日志mysql报错的解决方法