mvcc标准机制
多版本并发控制(Multiversion concurrency control)。
当对数据进行写操作时,不允许对被操作数据进行读操作,这样会导致数据库并发性能低下。于是用mvcc在并发访问(读或写)数据库时,对正在事务内处理的数据做多版本的管理,以降低写操作的堵塞而引发读操作的并发问题。
mvcc在对插入、删除、更新、读取都有自己的标准机制。
标准插入流程
在我们自定义的表结构中,mysql会默认插入几个其他的字段,其中和mvcc相关的字段有DB_TRX_ID,DB_ROLL_PT,前者为数据行的版本号,后者为回滚版本号。
mysql的事务是全局的,每个事务都有一个全局的事务id,行版本号就是保存的当前事务的id号。例如执行事务:
begin; --事务id为1
insert into user(name,age) value ('zhangsan', 18);
insert into user(name,age) value ('lisi', 19);
commit;
假设此事务id为1,得到数据如下:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
1 | zhangsan | 18 | 1 | null |
2 | lisi | 19 | 1 | null |
这两条数据是在同一个id为1的事务中插入的,所以行版本号都为1,默认回滚版本号为null。
标准删除流程
例如删除id=2的记录:
begin; --事务id为22
delete from user where id=2;
commit;
此时在mvcc中并没有删除,而是将回滚版本号设置为当前事务的id号。行版本号不变。得到数据如下:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
1 | zhangsan | 18 | 1 | null |
2 | lisi | 19 | 1 | 22 |
标准修改流程
例如修改id=1的数据:
begin; --事务id为33
update user set age=19 where id=1;
commit;
此时是先将该记录复制为一条新的记录,行版本号设置为当前事务id,回滚版本号为null。然后在将原先的记录回滚版本号设置为当前事务id号,行版本号不变。
得到数据如下:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
1 | zhangsan | 18 | 1 | 33 |
2 | lisi | 19 | 1 | 22 |
2 | zhangsan | 19 | 33 | null |
标准查询流程
数据的查询规则:
1. 查找数据行版本号早于当前事务版本号的记录
确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改的。
2. 查找删除版本号要么为null,要么大于当前事务版本号的记录
确保取出来的记录在事务开启之前没有被删除。
例如查询:
begin; --事务id为44
select * from user;
commit;
根据规则,取出来的数据如下:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
2 | lisi | 19 | 1 | 22 |
2 | zhangsan | 19 | 33 | null |
验证查询规则
例如现在有两个事务如下:
事务1:
begin; --1
select * from user; --2
commit;
事务2:
begin; --3 事务id 33
update user set age=28 where id=1; --4
commit;
查询组合两套:
组合1:1-2-3-4-2
组合2:3-4-1-2
执行组合1
执行1-2后:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
1 | zhangsan | 18 | 1 | null |
2 | lisi | 19 | 1 | null |
执行3-4后:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
1 | zhangsan | 18 | 1 | 33 |
2 | lisi | 19 | 1 | null |
2 | zhangsan | 28 | 33 | null |
再执行2:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
2 | lisi | 19 | 1 | null |
2 | zhangsan | 28 | 33 | null |
取到的数据合理。
执行组合2
执行3-4后:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
1 | zhangsan | 18 | 1 | 33 |
2 | lisi | 19 | 1 | null |
2 | zhangsan | 28 | 33 | null |
再执行1-2后,假设此时事务id为55,获取到的数据为:
id | name | age | DB_TRX_ID | DB_ROLL_PT |
---|---|---|---|---|
2 | lisi | 19 | 1 | null |
2 | zhangsan | 28 | 33 | null |
此时获取到的数据包含3-4未提交的数据,这导致了脏读和不可重复读。
通过mvcc的标准机制导致了脏读和不可重复读,问题出在哪呢?
实际上innodb的mvcc并没有完全依据mvcc的标准流程,这种情况下并不是通过mvcc直接读取的,而是通过读取innodb 的undo log来获取。
即当发现被查询的结果集中有被上锁记录之后,这些被锁定的记录会转而去undo log中获取数据。
Undo Log
事务有时需要回滚操作,这时就需要undo。事务开始之前,首先将需操作的数据做备份,就是undo log。
undo log为了实现事务的原子性而出现的产物。如果事务处理过程中 出错或执行了ROLLBACK语句,Mysql可以利用undo log中的备份将数据逻辑恢复到事务开始之前的状态。
undo log可以用来实现多版本并发控制,undo log中的数据可作为数据旧版本快照供其他事务读取。
事务提交后并不能立即删除undo log,因为可能还有其他事务需要通过undo log来得到行记录之前的版本,因此事务提交时将undo log放入一个链表中,是否可以最终删除有purge线程来判断。
purge用于最终完成delete和update操作。这样设计是因为innodb支持mvcc,所以记录不能在事务提交时立即立即进行处理。这时其他事务可能正在引用这行,而是否可以删除通过purge来进行判断。若该记录已不被其他任何事务引用,那么就可以进行真正的delete操作。
Redo Log
redo log用来实现事务的持久性,innodb通过Force Log at Commit机制实现事务的持久性。即当事务提交时,必须先将该事务的所有日志写入到redo log中进行持久化,待事务的commit完成才算真正完成,这里的日志指重做日志,包括undo log和redo log两部分。
事务中操作的任何数据,将最新的数据备份到redo log,防止在发生故障时尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo
log进行重做,从而达到事务的持久化。
一旦事务成功提交且数据持久化落盘之后,此时Redo log中的对应事务数据记录就失去了意义,所以Redo log的写入是日志文件循环写入的。
快照读和当前读
快照读
普通的SELECT 就是快照读,innodb 快照读,数据的读取将由从undo log中读取。
当前读
当前读读取的数据是最新版本。通过锁机制来保证读取的数据无法通过其他事务进行修改
UPDATE 、DELETE 、INSERT 、SELECT … LOCK IN SHARE MODE 、SELECT … FOR UPDATE 都是当前读。