找回密码
 会员注册
查看: 22|回复: 0

浅析MySQL之MVCC机制

[复制链接]

4

主题

0

回帖

13

积分

新手上路

积分
13
发表于 2024-10-13 10:26:58 | 显示全部楼层 |阅读模式
浅析MySQL之MVCC机制 383 一、前言在分析 MVCC 的原理之前,我们先回顾一下 MySQL 的一些内容以及关于 MVCC 的一些简单介绍。(注:下面没有特别说明默认 MySQL 的引擎为 InnoDB )1.1 数据库的并发场景数据库并发场景有三种,分别是:读-读:不存在线程安全问题,不需要并发控制。读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、不可重复读、幻读等问题。写-写:有线程安全问题,可能会存在更新丢失的问题,比如第一类更新丢失、第二类更新丢失。(第一类丢失更新:事务A回滚时,将已经提交的事务B的更新数据覆盖了;第二类丢失更新:事务A提交覆盖了事务B已经提交的数据,造成事务B所做的操作丢失)1.2 什么是 MVCCMVCC全称 Multi-Version Concurrency Control ,即多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中实现对数据库的并发访问,在编程语言中实现事务内存。多版本控制:指的是一种提高并发的技术,在最早的数据库系统中只有读-读之间可以并发,读-写、写-写之间都要阻塞。引入多版本并发控制之后,只有写-写之间相互阻塞,其他三种操作都可以并行,这样大幅的提高了 InnoDB 的并发度。在内部实现中,InnoDB 是通过 undo log 实现的,通过 undo log 可以找回数据的历史版本。找回的历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在 InnoDB 内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见行。一句话概述,MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,做到非阻塞并发读。1.3 当前读和快照读当前读select xxx lock in share mode; # 共享锁#排它锁select xxx for update;update xxx;delete xxx;insert xxx;像上面的这些操作就是一种当前读,因为它读取的是数据的最新版本,读取时还要保证其他事务不能修改当前记录,会对记录进行加锁。快照读不加锁的 select 就是快照读,即不加锁的非阻塞读。(快照读的前提是隔离级别不是 serializable,serializable 的隔离级别下快照读会退化成当前读) 之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于 MVCC 的,可以认为 MVCC 是行锁的一个变种,但是它在很多情况下避免了加锁操作,降低了开销。既然是基于多版本,所以快照读可能读到的不一定是数据的最新版本,而有可能是之前的历史版本。1.4 当前读和快照读与 MVCC 的关系准确的说,MVCC 指的是**"维护一个数据的多个版本,使得读写操作没有冲突"这么一个概念,仅仅是一个理想状态。而在 MySQL 中,实现这么一个 MVCC 理想概念,我们就需要 MySQL 提供具体的功能去实现,而快照读**就是 MySQL 为我们实现 MVCC 理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是一个悲观锁的具体功能实现,而要说的在细致一点,快照读本身也是一个抽象概念,在深入研究,MVCC 模型在 MySQL 中的具体实现则是由三个隐式字段、undo log 、Read View 等去完成的。1.5 MVCC 能解决什么问题MVCC 是一种解决读写冲突的无锁并发控制手段,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照,所以 MVCC 可以为数据库解决以下问题:在并发读数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写性能。可以解决脏读、不可重复读、幻读等事务隔离问题(不能解决更新丢失的问题)。所以说 MVCC 就是开发人员不满意只让数据库采用悲观锁(加锁)这样性能不佳的形式去解决读-写的问题而提出的解决方案,所以在数据库中,因为有了 MVCC ,所以我们可以形成两个组合:MVCC + 悲观锁MVCC解决读写冲突,悲观锁解决写-写冲突MVCC + 乐观锁MVCC解决读写冲突,乐观锁解决写-写冲突。二、MVCC实现原理2.1 隐式字段在一张表中,除了我们自定义的字段,实际上 MySQL 会隐式的定义一些字段。DB_TRX_ID6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务IDDB_ROLL_PTR7byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里)DB_ROW_ID6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引实际还有一个删除 flag 隐藏字段, 即记录被更新或删除并不代表真的删除,而是删除 flag 变了例如下面是 person 表的某条记录,如下图,DB_ROW_ID 是数据库为改行记录生产的唯一隐式主键,DB_TRX_ID 是当前操作该记录的事务ID,而 DB_ROLL_PTR 是一个回滚指针,用于配合 undo log 日志,指向上一个版本。2.2 undo log日志 undo log 类型insert undo log是指在 insert 操作中产生的 undo log。因为 insert 操作的记录,只对当前事务本身可见,对其他事务不可见(这是事务隔离性的要求),因此这种 undo log 可以在事务提交后直接删除。不需要进行 purge 操作update undo log是对 delete 和 update 操作产生的 undo log。该 undo log 可能需要提供给 MVCC 机制使用,因此不能在事务提交时就进行删除,提交时放入 undo log 链表,等待 purge 线程进行最后的删除。 purge 线程为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下老记录的 deleted_bit(即前面提到的删除 flag ),并不真正将过时的记录删除。为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录。为了不影响 MVCC 的正常工作,purge 线程自己也维护了一个 Read View(这个 Read View 相当于系统中最老活跃事务的 Read View ),如果某个记录的 deleted_bit 为true,并且 DB_TRX_ID 相对于 purge 线程的 Read View 可见,那么这条记录一定是可以被安全清除的。 对 MVCC 有帮助的实质是 update undo log ,undo log 存在于 rollback segment 中旧记录链,它的执行流程如下:一个事务插入 person 表插入了一条新记录,记录如下,name 为 Jerry, age 为24,隐式主键是1,事务ID和回滚指针我们假设为NULL。2.现在来了一个 事务1 对记录的 name 进行了修改,改为了 Tom,执行过程如下:在事务1修改该行数据时,数据会先对这行记录加排它锁。然后把该行数据拷贝到 undo log 中,作为旧记录,即在 undo log 中有当前行的拷贝副本。拷贝完毕后,修改该行的 name 为 tom,并且修改隐藏字段的事务ID为当前事务1的ID,我们默认从1开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,即表示我的上一个版本就是它。事务提交后,释放排它锁。3.又来了个事务2修改 person 表的同一个记录,将 age 修改为30岁,执行过程如下:在事务2修改该行数据时,数据库也先为该行加锁然后把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么最新的旧数据作为链表的表头,插在该行记录的 undo log 最前面修改该行 age 为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到 undo log 的副本记录事务提交,释放锁从上面我们可以看出,不同事务或者相同事务对同一记录的修改,会导致该记录的 undo log 称为一条记录版本线性表,即链表,undo log 的表头就是最新的旧记录,(当然就像之前说的该 undo log 的节点可能会被 purge 线程清除掉,像图中的第一条 insert undo log ,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)2.3 Read View 什么是 Read View ?Read View 是事务进行快照读操作时产生的读视图,在该事务执行快照读的那一刻,会生成数据库系统的当前的一个快照,记录并维护当前活跃事务的ID(当每个事务开启时,都会被分配一个 ID,这个 ID 是自增的,所以最新的事务,ID 值越大) 可见行判断所以我们知道 Read View 主要是用来做可见性判断的,即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。Read View 遵循一个可见性算法,主要是将要被修改的数据的最新记录的 DB_TRX_ID(即当前事务ID),与系统当前其他活跃事务的ID去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View 的属性做了某些对比,不符合可见性,那么就由 DB_ROLL_PTR 回滚指针去取出undo log中的 DB_TRX_ID 再比较,即遍历链表的 DB_TRX_ID(从链表头到尾,即从最近的一次修改查起),直到找到满足特定条件的 DB_TRX_ID,那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新的老版本。 判断条件是什么?我们可以将 Read View 简单的理解为三个全局属性trx_list 一个数值列表,用来维护 Read View 生成时刻此时系统正活跃的事务IDup_limit_id 记录 trx_list 中的最小的事务 IDlow_limit_id 在 Read View 生成时刻系统尚未分配的下一个事务 ID,即目前(不一定是 Read View 中)已经出现过的事务 ID 最大值+1 比较步骤首先比较 DB_TRX_ID
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 13:16 , Processed in 0.340707 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表