undo log 记录的是sql语句执行更新前的数据,这里的更新是泛指,除了select其它都算更新。在读已提交和可重复读的隔离级别下,会记录事务中某条数据的修改版本链,用来支持MVCC,详细参考MVCC章节。
redo log的设计是为了提高性能,如果没有redo log,数据库更新一条数据的过程大致如下:
bin log主要用来记录sql操作,简单形象的理解就是记录了每条sql语句,是追加写,性能较高。如果你想把你的数据库恢复到某一秒,可以使用bin log来恢复。bin log是Mysql提供的,并不是某个特定的数据库引擎提供,因此具有通用性,不管选用哪个数据库引擎,只要打开了bin log开关,就会记录bin log。
一个事务大致过程如下:
上面步骤中,3,4,5步被称为两阶段提交,目的就是使redo log和bin log数据一致。现在来讨论下在上述5个步骤中,其中一个步骤在执行时宕机,undo log,redo log,bin log是怎么协作来保证数据的一致性的。
Mysql的每一行最后是有个3个或者4个隐藏字段的
CREATE TABLE `testtable` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `bus1` int(11) DEFAULT NULL COMMENT '业务字段1', `bus2` varchar(20) DEFAULT NULL COMMENT '业务字段2', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 insert into testtable values(1,11,'第一条数据'); insert into testtable values (3,22,'第二条数据'); insert into testtable values (4,33,'第三条数据'); insert into testtable values (10,44,'第四条数据');
假设表名为testtable有以下数据(假设插入两条数据的事务tx_id分别是10和20)
id | bus1 | bus2 | tx_id | lst_point | del_flag |
---|---|---|---|---|---|
1 | 11 | 我是第一条数据 | 10 | null | false |
2 | 22 | 我是第二条数据 | 20 | null | false |
注意:insert操作时undo log不会生成任何数据
然后对这两条数据进行一些操作
-- 事务A 分配的事务id为100 start transaction; update testtable set bus2='我是更新后的第一条数据' where id = 1; update testtable set bus2='我是第二次更新后的第一条数据' where id = 1; commit
-- 事务A 分配的事务id为101 start transaction; update testtable set bus2='我是第三次更新后的第一条数据' where id = 1; commit
那么id=1对应的数据的版本链大概是这样的(序号这一列是我加上去的,方便一些解释)
序号 | id | bus1 | bus2 | tx_id | lst_point | del_flag | 所在地 |
---|---|---|---|---|---|---|---|
1 | 1 | 11 | 我是第三次更新后的第一条数据 | 101 | 指向序号2 | false | 数据表 |
2 | 1 | 11 | 我是第二次更新后的第一条数据 | 100 | 指向序号3 | false | undo log |
3 | 1 | 11 | 我是第二次更新后的第一条数据 | 100 | null | false | undo log |
可以看到3次对id=1的数据进行update操作,形成了一个版本链
MVCC全称是多版本并发控制,通过对某一条记录的多个版本共存达到不加锁的目的。由于读已提交隔离级别是直接读取当前行的最新数据,串行化是事务依次进行,这两个隔离级别不使用MVCC,主要是不存在竞争。当事务遇到第一个select时会生成当前数据库的快照(read view),生成方式不是拷贝一份数据库数据出来而是将当前活跃的id放入一个数组中,只要分析好活跃事务的操作,其它的可以不考虑,因为只有活跃的事务会更改数据库。在可重复读隔离级别下,第一个select生成快照后,后面所有的select都使用这个快照,在读已提交隔离级别中,每次遇到select都会生成一次新的快照,目的就是已经提交的数据也能被查看到。
先给出可见性判定逻辑,当select发生生成快照时,自己的事务id记为self_tx_id(非官方名称),正在活跃事务的id最小值记为min_tx_id(非官方名称),此时事务最大的一个id记为max_tx_id(看清楚,是事务最大的id,不是活跃事务的最大id,事务的最大id是活跃事务和已提交事务中最大的id),活跃事务的数组记为activie_tx_ids(非官方名称);那么:
序号 | 事务100 | 事务101 | 事务102 | 事务103 |
---|---|---|---|---|
1 | start transaction; | |||
2 | update testtable set bus2 = ‘我是事务100’ where id =3; | |||
3 | start transaction; | |||
4 | update testtable set bus2 = ‘我是事务101’ where id = 1; | |||
5 | commit; | |||
6 | start transaction; | |||
7 | update testtable set bus2 = ‘我是事务102’ where id = 1; | |||
8 | select * from testtable where id = 1; | |||
9 | start transaction; | |||
10 | update testtable set bus2=‘我是事务103’ where id = 1; | |||
11 | commit; | |||
12 | commit; | |||
13 | select * from testtable where id = 1; | |||
14 | select * from testtable where id = 3; | |||
15 | commit; |
分析事务100 第一个select版本链大致如下:
序号 | id | bus1 | bus2 | tx_id | lst_point | del_flag |
---|---|---|---|---|---|---|
1 | 1 | 11 | 我是事务101 | 101 | null | false |
2 | 1 | 22 | 我是事务102 | 102 | 指向序号1 | false |
可以知道:
当前事务id——self_tx_id是100
活跃事务的最小id——min_tx_id是100
事务的最大id——max_tx_id是102
活跃事务id数组——active_tx_ids是100,102
首先id=1这条记录最近一次更新是事务102更新的,因此current_tx_id是102,然后判断102在min_tx_id和max_tx_id之间,又在活跃事务id数组中,因此对于事务100来说,不可见,然后回溯到上一版本可以得知current_tx_id是101,101在min_tx_id和max_tx_id之间但不在活跃事务id数组中,所以可见。
事务100执行select之前的语句:
事务101执行所有语句:
事务102执行update:
此时事务100执行第一个select:
可以看到读取到的是事务101更新后的数据,没有读取到事务102更新的数据。
再分析事务100的第二个select,此时事务102进行了commit,事务103也执行完成。此时事务100使用的快照还是第一次select产生的快照,但是id=1这条记录的最后更新事务id是103,此时103比做快照时最大事务id还要大,事务100是无法看到这条记录的,进行版本回溯,回溯到最后修改事务id为102的版本中,发现102在活跃事务列表中,依旧是无法看到,那么继续回溯就到了事务id是100的版本中,此时可见,所以本次select读到的和上面的数据是一致的,我们通过数据库验证下:
事务103执行结果(注意这个结果是事务102 commit后才提交成功的,因为事务102也更新了id=1的数据,会把这条数据锁住,commit后释放锁,事务103才能提交成功,但不妨碍undo log版本链生成)
事务102执行结果:
此时执行第二次select的结果:
看到的依旧是事务101更新后的结果。
然后事务100执行第三次select:
这个这里就不详细分析了,就是当前事务中的select读取了当前事务已经修改的数据,会读取到当前事务更新后的数据。
至此MVCC分析完毕。上面的分析 都是建立在可重复读隔离级别下的,读已提交隔离级别中,每次遇到select都重新生成快照,分析方式与上面一致,这里就不再赘述了。