可重复读的实现
事务的隔离级别从弱到强以此是:未提交读,已提交读,可重复读,串行化。mysql innodb引擎默认的是RR可重复读。那么它是怎么实现的呢?
MVCC
数据库中的每条记录都是多版本的。这个多版本是通过undo log来实现的。数据库存储了记录的当前值,通过apply undo log,使得记录回退。如图:
某条记录的当前值是事务id102对应的数据版本,通过向前apply undo log,拿到该记录的100版本,直到拿到我们需要的版本。
注意,上图中,undo log中事务id的顺序不一定是递增的顺序的。原因是有可能后开启的事务先修改该条记录。而先开启的事务后修改。
事务的可见范围
RR级别下,事务的一致性视图是在事务开启的时候生成的。我们将全部的事务分成三类:
- 当前已开启但未提交的事务
- 当前未开启的事务 可能会在本事务开启之后开启,这些事务的ID都会大于当前事务的ID
- 已经提交的事务
那么对于当前事务来讲,只有已经提交的事务产生的记录修改是可见的。我们只需要根据记录的当前版本和undo log找到第一个可见记录版本就可以了。
事务的当前读
前边讲到的事务的读在RR下都是只读可见的记录版本。但是对于事务中的更新操作,则是采用的当前读的策略。举个例子:
有A、B两个事务。有一条记录(name, age)-> (xiaoming, 10)
- A事务先开启,执行 update user set age=age+1 where name='xiaoming'; //此时age变成11.
- B事务开启,执行select age from user where name='xiaoming'; //由于RR,看到的是10.
- A事务提交 //释放X锁
- B事务再次执行select age from user where name='xiaoming'; //由于RR,看到的是还是10.
- B事务执行 update user set age=age+1 where name='xiaoming'; //采用当前读,将age变成12
- B事务再次执行select age from user where name='xiaoming'; //看到的是12.
对于第6步来讲,该条记录的最新版本是B事务(5操作导致),因此是能够看到最新结果的,即12.
值得注意的是,如果将该条记录沿undo log向前退一步,会到达A事务产生的版本,而该版本对B来讲是不可见的。也就是说,某条记录的某一版本可见,不代表该版本之前的版本也是可见的。但是这都不重要,因为我们只需要最新的可见的版本。
事务的锁释放
事务中,锁的获取是在执行相应sql时获取的,但是锁的释放却不是该sql执行完就立即释放的,而是等到事务结束才释放。
因此如果一个事务要获取多把锁,我们一般把竞争比较激烈的,有可能导致锁等待的锁放在最后获取,这样能减少锁占用的时间。
长事务的弊端
事务开启到事务结束的时间过长,我们称为长事务。长事务有如下缺点:
- 长时间占有锁资源不释放,阻塞其他的事务处理;
- 影响undo log的清理。
前边我们提到事务隔离是基于undolog来实现的。undolog会在不被使用时删除。而长事务会导致全部的undolog迟迟不能清理,浪费空间。
死锁与死锁检测
如果两个事务AB分别要去获取对方已经获取到的锁,就会导致死锁。有可能是不同表的锁,也有可能是同一张表的不同记录的行锁。
一旦发生了死锁,就需要处理。常见的处理方式有如下:
- innodb_lock_wait_timeout 设置最大的获取锁的时间,一旦超过这个时间,就认为获取锁失败,回滚事务并释放锁。
- innodb_deadlock_detect 死锁检测 一旦事务在等待获取某把锁,就发起死锁检测,如果检测到死锁,就讲其中一条事务进行回滚处理,释放锁,使得其他事务执行下去。
优缺点:
lock_wait_time 时间设置的过大,会导致死锁的事务长时间持有锁;过小又会误影响到正常的事务
死锁检测:每个事务一旦要等待获取锁,就要进行死锁检测,比较消耗CPU资源。