2024-03-03 23:18:33
MVCC原理详解
一、什么是MVCC
多版本控制(MVCC,Multi-Version Concurrency Control)是一种提高数据库并发性能的技术。在引入MVCC之前,最早的数据库系统在读读、读写、写读、写写操作中,除了读读可以并发外,其他操作都需要阻塞。而MVCC的引入,使得只有写写操作之间需要相互阻塞,读读、读写、写读操作都可以并行,从而大幅度提高了数据库的并发度。
MVCC通过在数据库中保存数据的多个版本,使得每个事务可以读取到符合其版本要求的数据。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
MVCC只在已提交读(Read Committed)和可重复读(Repeatable Read)两个隔离级别下工作,其他两个隔离级别(未提交读和串行化)与MVCC是不兼容的。因为未提交读总是读取最新的数据行,而不是读取符合当前事务版本的数据行;而串行化则会对读的所有数据多加锁。
二、MVCC相关的一些概念
事务版本号
事务每次开启时,都会从数据库获得一个自增长的事务ID,这个ID可以用来判断事务的执行先后顺序,即事务版本号。每当begin的时候,首先要做的就是从数据库获得一个自增长的事务ID,它也就是当前事务的事务ID。
隐藏字段
对于InnoDB存储引擎,每一行记录都有两个隐藏列:trx_id和roll_pointer。trx_id表示最后一次修改该记录的事务ID;roll_pointer是一个指针,指向该记录的上一个版本,从而形成一个版本链。如果数据表中存在主键或者非NULL的UNIQUE键,则不会创建row_id,否则InnoDB会自动生成单调递增的隐藏主键row_id。
undo log
undo log可以理解成回滚日志,它存储的是老版本数据。在表记录修改之前,会先把原始数据拷贝到undo log里,如果事务回滚,即可以通过undo log来还原数据。或者如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。undo log在insert/update/delete(本质也是做更新,只是更新一个特殊的删除位字段)操作时都会产生。
InnoDB中的undo log分为两类:insert undo log和update undo log。insert undo log在事务回滚时需要,并且在事务提交后就可以立即丢弃;而update undo log不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录时,对应的回滚日志才会被删除。
版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer)连成一个链表,这个链表就称为版本链。
快照读和当前读
快照读:读取的是记录数据的可见版本(可能是旧版本),不加锁。普通的select语句都是快照读。
当前读:读取的是记录数据的最新版本,显式加锁的都是当前读,如select ... for update和select ... lock in share mode。
ReadView
ReadView是事务在进行快照读的时候生成的记录快照,它保存了当前事务开启时所有活跃的事务列表(即不应该让这个事务看到的其他事务ID列表)。ReadView通过几个重要属性来保证可见性判断:
trx_ids:当前系统中那些活跃(未提交)的读写事务ID列表(不包括当前事务自己和已提交的事务)。
low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id为low_limit_id。
creator_trx_id:表示生成该ReadView的事务的事务ID。
访问某条记录时,通过ReadView的可见性规则来判断该记录是否可见:
如果被访问版本的事务ID等于creator_trx_id,那么表示当前事务访问的是自己修改过的记录,该版本对当前事务可见。
如果被访问版本的事务ID小于up_limit_id,那么表示生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的事务ID大于low_limit_id值,那么表示生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的事务ID在up_limit_id和low_limit_id之间,那就需要判断版本的事务ID是不是在trx_ids列表中。如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
三、MVCC实现原理分析
如何查询一条记录
获取事务自己的事务ID(trx_id),这个是在事务开启时获取的。
获取ReadView,这个是在select时才会生成的。
在数据库表中查询数据,并根据ReadView中的事务版本号进行比较。
如果不符合ReadView的可见性规则,就需要通过undo log中的历史快照找到符合规则的数据。
InnoDB通过ReadView+Undo Log实现MVCC。Undo Log保存了历史快照,ReadView的可见性规则帮助判断当前版本的数据是否可见。
MVCC是如何实现读已提交和可重复读的呢?
读已提交和可重复读唯一的区别在于:在读已提交(RC)隔离级别下,每个select都会创建最新的ReadView;而在可重复读(RR)隔离级别下,则是当事务中的第一个select请求才创建ReadView。
四、MVCC能否解决幻读问题?
MVCC能解决不可重复读问题,但是不能解决幻读问题,不论是快照读和当前读都不能解决。在可重复读(RR)级别下,解决幻读靠的是锁机制,而不是MVCC机制。
幻读是指在同一个事务中,两次查询得到的记录集合不一致的情况。例如,在一个事务中,第一次查询某个条件没有记录,然后另一个事务插入了一条符合该条件的记录并提交,接着在同一个事务中再次查询该条件,却得到了之前不存在的记录。
MVCC虽然可以保证在同一个事务中多次查询得到的结果是一致的(即解决了不可重复读问题),但是它不能保证在两次查询之间没有其他事务插入新的记录(即不能解决幻读问题)。要解决幻读问题,需要使用锁机制来确保在两次查询之间没有其他事务对表进行插入操作。


综上所述,MVCC是一种提高数据库并发性能的重要技术,它通过保存数据的多个版本和提供可见性规则来实现并发控制。但是,MVCC并不能解决幻读问题,要解决这个问题需要使用锁机制。