版本链其实就是CURD的历史记录,回滚的本质也是用版本链中的最近一条历史记录覆盖当前记录。版本链针对的是每个表中的记录,只要表中有任意一条记录被修改,版本链中就会新增一条历史记录。
目录
1、为什么需要版本链?
一方面允许回滚。如果用户想撤销之前的操作,有了版本链就可以用之前的历史记录来覆盖当前历史记录,从而达到回溯的效果。
另一方面是控制事务的隔离性。先到来的事务无法看到后来的事务作出的修改,不同事务看到的可能是不同版本,修改的也是不同的版本,所以两个事务执行的操作并不会互相影响,这就达到了隔离两个事务的目的。
2、有关版本链的前提知识
(1) ReadView
每当有一个事务到来的时候,Mysql 就会创建一个ReadView(读快照),相当于相机拍下了那一时刻的事务状况,ReadView记录了在当前事务之前有哪些事务处在活跃状态,有哪些事务的历史记录允许被看到等等。
//一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
m_ids;
// 活跃事务中事务ID的最小值
up_limit_id;
// 未来事务临界点(下一个事务到来时应该分配的ID,之后到来的事务都属于未来事务,历史记录不可被看到)
low_limit_id;
// 创建该ReadView的事务ID(快照形成点)
creator_trx_id
(2) 四个隐藏字段
每个表在被创建的时候,都会存在四个隐藏字段。我们对表中的每一条记录作出的任何操作,都会被保存下来(包括谁操作了该条记录、历史记录放在哪、该条记录是否被删除等)
- DB_TRX_ID:最近修改 / 插入该条记录的ID
- DB_ROLL_PTR:回滚指针,指向该条记录的上一个版本(一般在undo log)
- DB_ROW_ID:隐含的自增ID。若数据表无主键, InnoDB 会自动以 DB_ROW_ID 产生一 个聚簇索引
- flag:用于标识该记录是否被删除
(3) undo log(回滚日志)
其实就是保存日志数据,也就是历史记录。每次执行sql操作都会有历史记录产生,产生的结果将作为历史记录保存到undo日志文件中。针对不同的操作,隐藏字段的填充内容也会不同,产生的日志也会有些许不同:
- update、delete:可以正常生成版本链
- insert:因为没有历史记录,所以实际上会插入一条空的历史记录(此时回滚指针为NULL)
- select:不会对数据作出修改,维护多个版本没有意义,不会生成版本链(历史记录)
2、单个事务的版本链
假设有一个10号事务到来,此时会形成一个“读快照”ReadView,记录下该事务到来时,有哪些事务是处于活跃状态、下一个事务到来时,应该分配的事务ID是多少等。
10号事务执行任何sql操作时都会产生历史记录,产生的历史记录保存到 undo log 中,每条记录中的隐藏字段 DB_ROLL_PTR 都会指向上一条历史记录。即便是历史记录也会指向自己的上一条记录。
3、多个事务的版本链
Mysql 中存在多个事务的时候,难免存在多个事务要修改同一行记录的情况,这个时候最好的办法就是加锁。就算有多个事务到来也必然存在先后顺序,此时Mysql会依次给他们创建快照(ReadView)。
假设有一个10号事务要修改记录,这个时候就会给该事务要修改的行加锁(我们称为“行锁”),修改之前将记录拷贝到undo log,这样就有了历史记录;修改完毕以后,释放 “ 行锁 ” 。
然后又有一个11号事务,也是要对同一行修改,首先也是加行锁,其次将之前的记录拷贝到undo log,最后再修改并提交。由此我们可以知道,每个事务可以看到的历史记录不一样,11号事务在10号事务之后,可以看到10号事务提交的记录;但是10号事务无法看到11号事务提交的记录。
4、RR 与 RC 的本质区别
从上述事务版本链可以大致了解到,事务是如何实现回滚、产生版本链以及保证彼此之间不会受到影响的。但是不同隔离级别下的事务,允许看到的版本链存在差异,比如读提交(RC)级别下,可以看到其他事务的提交结果;但是可重复读(RR)级别下,无法看到其他事务的执行结果直至当前事务提交。
- RR级别:只有事务到来的时候,才会形成一次ReadView,之后就不再更新,此后使用的都是同一个ReadView,看到的其他事务的版本链也是固定的。
- RC级别:每次快照读(查看历史版本)都会生成一个快照 和 ReadView,这就是每次查询出来的记录都是最新的原因。