1. 再谈隔离级别
我们知道事务有四个隔离级别,可能存在三种并发问题:
在MySQL中,默认的隔离级别是可重复读,可以解决脏读和不可重复读的问题,如果仅从定义的角度来看,它并不能解决幻读问题。如果我们想要解决幻读问题,就需要采用串行化的方式,也就是将隔离级别提升到最高,但这样一来就会大幅降低数据库的事务并发能力。
MCC可以不采用锁机制,而是通过乐观锁的方式来解决不可重复读和幻读问题!它可以在大多数情况下替代行级锁,降低系统的开销。
2. 隐藏字段,Undo Log版本链
回顾一下undo日志的版本链,对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中列就相当于一个指针,可以通过它来找到该记录修改前的信息。
假设插入了记录的事务id为8,那么此刻该条记录的示意图:
- insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)
能不能在两个事务中交叉更新同一条记录呢?不能!这不就是一个事务修改了另一个未提交事务修改过的数据,脏写。
InnoDB使用锁来保证不会有脏写情况的发生,也就是在第一个事务更新了某条记录后,就会给这条记录加锁,另一个事务再次更新时就需要等待第一个事务提交了,把锁释放之后才可以继续更新。
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表:
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录的最新的值。
每个版本中还包含生成该版本时对应的事务id。
当一个事务begin开始时,如果未做增删改等操作(查询select操作是可以允许的),那么系统就不会为这个事务分配事务id值,则其事务id值默认为0。直到事务中进行了增删改等操作,系统才会为事务分配自增的id。