文章目录
- 背景
- MVCC定义
- 快照读和当前读
- 当前读
- 快照读
- MVCC实现原理
- 隐式字段
- undo log
- 版本链
- 1.插入一条记录
- 2.修改记录
- 3.修改记录
- Read View读视图
- 属性:
- Read View可见性算法
- 隔离级别
- 长事务
- 为什么要避免长事务
背景
并发事务可能产生的问题:
- 读+读,并发读不会有问题
- 读+写,并发读写可能会发生脏读、不可重复读、幻读
- 写+写,并发修改同一行数据,可能产生数据丢失(会滚丢失、覆盖丢失)等问题
MVCC定义
MVCC(Mutil Version Concurrency Control)多版本并发控制,是一种并发访问的机制(非具体实现),广泛应用于数据库管理系统,比如Mysql、Oracle、Postgresql等,实现对数据库的并发访问。本质就是一行数据具有多个不同版本的记录。
Mysql的InnoDB引擎实现了MVCC机制,用来处理读写冲突,做到非阻塞并发读,提升并发效率。
快照读和当前读
当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
- select lock in share mode(共享锁) 读读不冲突、读写、写写冲突
- select for update ; update, insert ,delete(排他锁) 读读、读写、写写冲突
快照读
顾名思义,就是读取undo log中的某一版本的快照,读到的数据可能不是最新的,但是可以不加锁就可以读到数据
- 读读不冲突、读写不冲突
- 写写冲突
MVCC实现原理
MVCC的目的就是多版本并发控制,在数据库的实现,就是为了解决读写冲突,它的实现原理主要依赖记录中的3个隐式字段,undolog,Read View来实现的。
隐式字段
每行记录除了我们自定义的字段之外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段
- BD_TRX_ID: 6byte,最近修改(修改/插入)的事务ID:记录创建该记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR: 7byte,回滚指针,指向这条记录的上一个版本(存储在rollback segment里)
- DB_ROW_ID: 6byte,隐含自增ID,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
undo log
在修改数据的时候,会向 redo log 中记录修改的页内容(为了在数据库宕机重启后恢复对数据库的操作),也会向 undo log记录数据原来的快照(用于回滚事务)。undo log有两个作用,除了用于回滚事务,还用于实现MVCC
- insert log:代表事务在 insert 新记录时产生的 undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
- update undo log:事务在进行 update 或 delete 时产生的 undo log ; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除
版本链
1.插入一条记录
2.修改记录
- 加锁
- copy到undo log,作为旧记录(当前行的copy副本)
- 修改age,trx_id(从1开始递增);回滚指针指向副本
- 提交事务,释放锁
3.修改记录
- 加锁
- copy到undo log,作为旧记录(当前行的copy副本),已经有undo log,此副本作为链表表头插入的undo log的头节点
- 修改age,trx_id(从1开始递增);回滚指针指向副本
- 提交事务,释放锁
Read View读视图
Read View是在对数据进行快照读时,会产生的一个”一致性读视图“。
属性:
- m_ids:活跃事务id列表,当前系统中所有活跃的(也就是没提交的)事务的事务id列表。
- min_trx_id:m_ids 中最小的事务id。
- max_trx_id:生成 ReadView 时,系统应该分配给下一个事务的id(注意不是 m_ids 中最大的事务id),也就是m_ids 中的最大事务id + 1 。
- creator_trx_id:生成该 ReadView 的事务的事务id。
这些属性组成了当前事务的一致性视图(Read View),而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。
Read View可见性算法
把数据的最新记录中的 DB_TRX_ID取出来,与Read View对比,如果不符合可见性,那就通过undo log 取下一个版本对比,直到找到满足可见性的版本数据。
- 当【版本链中记录的 trx_id 等于当前事务id(trx_id = creator_trx_id)】时,说明版本链中的这个版本是当前事务修改的,所以该快照记录对当前事务可见。
- 当【版本链中记录的 trx_id 小于活跃事务的最小id(trx_id < min_trx_id)】时,说明版本链中的这条记录已经提交了,所以该快照记录对当前事务可见。
- 当【版本链中记录的 trx_id 大于下一个要分配的事务id(trx_id > max_trx_id)】时,该快照记录对当前事务不可见。
- 当【版本链中记录的 trx_id 大于等于最小活跃事务id】且【版本链中记录的trx_id小于下一个要分配的事务id】(min_trx_id<= trx_id < max_trx_id)时,
- 如果trx_id m_ids中,说明生成 ReadView 时,修改记录的事务还没提交,所以该快照记录对当前事务不可见;
- 如果trx_id不在m_ids中,说明生成该版本的事务已经提交,对当前事务可见
隔离级别
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 串行化(SERIALIZABLE)
mvcc只在读已提交和可重复读两种隔离级别生效
- 读已提交
- 事务开启后,每次select都会生成一个Read View,可以读到别的事务已经提交的数据
- 可重复读
- 事务开启后,只在第一次select时生成Read View,之后的select都基于此视图做可见性判断。
长事务
为什么要避免长事务
- 长事务可能存在很老的Read View,如下图的事务1和2,这些视图很可能访问任何数据,在这个事务提交前,它可能用到的回滚记录都不能清理,需要保留,占用大量存在空间
- 长事务占用锁资源,长时间不释放锁,可能拖垮整个库