锁的代价
前面一节讲到过死锁,以及事务型数据库中需要经常地检测死锁的问题,提出的对应解决方案就是使用行级锁,将需要锁定的行锁定起来。那么,这里就不得不说到锁的代价了,其实锁定一行的代价还是很高的,因为它对其它事务来说就是在短期内不可用的了,这对数据库的吞吐量来说是极大的损害!
那么有没有什么方法能有效地减少锁的的创建呢?这就是我们接下来要介绍的MVCC啦。MVCC其实是行级锁的一个变种,但是很多情况下它都避免了锁的创建操作,所以整个数据库的用在锁的开销很低。实现的方式主要是非阻塞的读操作和锁定必须行的写操作。
MVCC多版本并发控制
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。这句话怎么理解呢?如果妳看过前面的几篇文章,就会明白事务的一致性特性,也就是说不管什么时候,每个事务看到的数据都是一致的。根据事务的开始时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的,但是对于这个事务本身来说,数据必须是要是一致的。
版本控制 Version Control
这里以MySQL的默认存储引擎InnoDB为例,说一说MVCC是如何实现的?InnoDB中,MVCC是通过在每一行记录后面保存两个隐藏的列来实现的。分别是创建时间gmt_create
和过期时间(或删除时间)gmt_expire
。这里存储并不是真实的时间值,而是系统的版本号,这个版本号是会依次累加的。比如说事务N开始的时候系统的版本号为666,那么这个事务的版本号就会从666开始,也就是说gmt_create
会存储为666。主要是到了后面对数据进行操作的时候可以和记录的版本号作对比,看数据是否已经被修改了。
MVCC的CRUD具体操作
为了具体地给大家解释MVCC下数据库的CRUD四种操作是如何执行的,我们以MySQL在REPEATABLE READ(可重复度)的隔离级别下,看看MVCC的具体操作。
SELECT
InnoDB会根据以下的两个条件来检查每条记录:
- InnoDB只查找版本早于当前事务版本的数据行(也就是说,其它大于当前系统版本号的数据行都是在本事务开始后被操作过的),这样可以确保事务读取的行都是事务自己插入或者修改过的,或者是事务开始前就已经存在的。
- 行的删除版本要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行,在事务开始之前未被删除。
只有符合上述两个条件的记录,才能被返回作为查询的结果。
INSERT
InnoDB为新插入的每一行数据保存当前系统版本号作为数据行版本号。
DELETE
InnoDB为删除的每一行数据保存当前系统版本号作为当前的数据行的删除标识。
UPDATE
InnoDB插入一行新的数据,保存当前的系统版本号作为这一行的版本号,同时保存当前的版本号到原来的那行数据作为它的删除版本号。
总结
保存这两个额外的版本号其实对于大多数读操作来说,都是不需要加锁操作的。这样设计我们前面也说过了,有利于减少系统因为加锁带来的巨大开销,这样性能也会更好!但是不足之处就是每一条记录都需要额外的空间,需要做更多的一致性检查工作。
MVCC只会在REPEATABLE READ和READ COMMITTED两个隔离级别下工作,为什么呢?因为其他两个隔离级别READ UNCOMMITTED和SERIALIZABLE和MVCC是不兼容的,READ UNCOMMITTED只会读取最新的数据行记录,不是当前的事务版本号之前的数据行哦!SERIABLIZABLE则是会对所有的数据行都会加锁,WTF!多么耗空间啊!大家在实际的生产环境下没有特别严格的需要千万不要随便使用最后一种事务隔离级别SERIALIZABLE,它可是性能杀手!