• MVCC 机制的原理及实现

MVCC 机制的原理及实现

2025-05-27 01:00:03 栏目:宝塔面板 58 阅读

什么是 MVCC

MVCC(Multiversion Concurrency Control)翻译过来是多版本并发控制,和数据库锁一样,也是一种并发控制的解决方案。

在InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读,而非当前读。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式。

快照读

所谓快照读,就是读取的是快照数据,即快照生成的那一刻的数据,像我们常用的普通的SELECT语句在不加锁情况下就是快照读:

SELECT * FROM xx_table WHERE ...

注意:快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

当前读

当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的SELECT,或者对数据进行增删改都会进行当前读:

SELECT * FROM xx_table LOCK IN SHARE MODE; #共享锁
SELECT * FROM xx_table FOR UPDATE;         #排他锁
INSERT INTO xx_table values ...        #排他锁
DELETE FROM xx_table WHERE ...        #排他锁
UPDATE xx_table SET ...         #排他锁

解决什么问题

我们知道,在数据库中,对数据的操作主要有2种,分别是读和写,而在并发场景下,就可能出现以下三种情况:

  • 读-读并发:不存在任何问题,也不需要并发控制
  • 读-写并发:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写并发:有线程安全问题,可能会存在更新丢失问题

在没有写的情况下读-读并发是不会出现问题的,而写-写并发这种情况比较常用的就是通过加锁的方式实现。那么,读-写并发则可以通过MVCC的机制解决。

实现原理

Undo Log

undo log是Mysql中比较重要的事务日志之一,是一种用于回退的日志,在事务没提交之前,MySQL会先记录更新前的数据到undo log日志文件里面,当事务回滚时或者数据库崩溃时,可以利用undo log来进行回退。

  • insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的Undo Log Segment也会被系统回收
  • update或delete时产生的undo log,不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

一条记录在同一时刻可能有多个事务在执行,那么undo log会有一条记录的多个快照,那么在这一时刻发生SELECT要进行快照读的时候,要读哪个快照呢?

行记录的隐式字段

其实,数据库中的每行记录中,除了保存了我们自己定义的一些字段以外,还有一些重要的隐式字段的:

  • db_row_id:隐藏主键,如果我们没有给这个表创建主键,那么会以这个字段来创建聚簇索引
  • db_trx_id:对这条记录做了最新一次修改的事务的ID
  • db_roll_ptr:回滚指针,指向这条记录的上一个版本,其实他指向的就是Undo Log中的上一个版本的快照的地址

注意:以上字段只有在聚簇索引的行记录中才会有,而在普通二级索引中是没有这些值的。

每一次记录变更之前都会先存储一份快照到undo log中,那么这几个隐式字段也会跟着记录一起保存在undo log中,就这样,每一个快照中都有一个db_trx_id字段表示了对这个记录做了最新一次修改的事务的ID ,以及一个db_roll_ptr字段指向了上一个快照的地址。(db_trx_id和db_roll_ptr是重点,后面还会用到)

这样就形成了一个快照链表:

图片

有了undo log,又有了几个隐式字段,我们好像还是不知道具体应该读取哪个快照,那怎么办呢?

Read View

Read View 是InnoDB中一个至关重要的概念,是实现MVCC的基础,同时也是支持不同的事务隔离级别的基础,同时提高系统的并发能力和性能。

Read View主要来帮我们解决可见性的问题的, 即他会来告诉我们本次事务应该看到哪个快照,不应该看到哪个快照。

  • 在可重复读(Repeatable Read)级别下,快照(Read View)在事务开始后第一次查询时创建一次,并在整个事务期间保持不变。
  • 在读已提交(Read Committed)级别下,快照(Read View)会在每次查询时重新创建,以反映数据库中的最新提交更改。

在Read View中有几个重要的属性:

  • trx_ids,表示在生成Read View时当前系统中活跃的读写事务的事务id列表。
  • low_limit_id,应该分配给下一个事务的id值。
  • up_limit_id,未提交的事务中最小的事务ID。
  • creator_trx_id,创建这个Read View的事务ID。

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所在的旧记录就是当前事务能看见的最新老版本。

案例

假如一个ReadView的内容为:

trx_ids = [5,6,8)
low_limit_id = 8
up_limit_id = 5
creator_trx_id = 7

假设当前事务要读取某一个记录行,该记录行的db_trx_id(即最新修改该行的事务ID)为 trx_id,那么,就有以下几种情况了:

1、trx_id

2、trx_id>=low_limit_id,即大于8的事务,说明该事务在生成ReadView后才生成,所以该事务的结果就是不可见的。

3、up_limit_id

如果,事务ID在trx_ids列表中,如6,那么表示在当前事务开启时,这个事务还是活跃的,那么这个记录对于当前事务来说应该是不可见的。

如果,事务id不在trx_ids列表中,如7,那么表示的是在当前事务开启之前,其他事务对数据进行修改并提交了,所以,这条记录对当前事务就应该是可见的。

当然这里有个例外情况,那就是这个trx_id=creator_trx_id,那么就肯定是可见的

总结一下就是,一个事务能看到的是在他开始之前就已经提交的事务的结果,而未提交的结果都是不可见的。

当数据的事务ID不符合Read View规则时候,那就需要从undo log里面获取数据的历史快照,然后数据快照的事务ID再来和Read View进行可见性比较,如果找到一条快照,则返回,找不到则返回空。

总结

在InnoDB中MVCC就是通过Read View + Undo Log来实现的,undo log中保存了历史快照,而Read View用来判断具体哪一个快照是可见的。

本文地址:https://www.yitenyun.com/240.html

搜索文章

Tags

数据库 API FastAPI Calcite 电商系统 MySQL 数据同步 ACK Web 应用 异步数据库 双主架构 循环复制 Deepseek 宝塔面板 Linux宝塔 Docker 生命周期 序列 核心机制 JumpServer JumpServer安装 堡垒机安装 Linux安装JumpServer esxi esxi6 root密码不对 无法登录 web无法登录 Windows Windows server net3.5 .NET 安装出错 宝塔面板打不开 宝塔面板无法访问 SSL 堡垒机 跳板机 HTTPS 查看硬件 Linux查看硬件 Linux查看CPU Linux查看内存 无法访问宝塔面板 Windows宝塔 Mysql重置密码 HTTPS加密 连接控制 机制 ES 协同 Oracle 处理机制 Serverless 无服务器 语言 OB 单机版 Spring SQL 动态查询 scp Linux的scp怎么用 scp上传 scp下载 scp命令 技术 存储 运维 缓存方案 缓存架构 缓存穿透 索引 架构 InnoDB 分页查询 Rsync 日志文件 MIXED 3 响应模型 修改DNS Centos7如何修改DNS RocketMQ 长轮询 配置 监控 HexHub Redis 电商 系统 Linux 安全 服务器 聚簇 非聚簇 MySQL 9.3 异步化 防火墙 黑客 sftp 服务器 参数 数据 主库 group by 自定义序列化 查询 数据库锁 SQLark PostgreSQL SQLite-Web SQLite 数据库管理工具 开源 存储引擎 管理口 业务 R edis 线程 线上 库存 预扣 Doris SeaTunnel MVCC 人工智能 向量数据库 推荐系统 共享锁 加密 场景 流量 工具 PG DBA Ftp redo log 重做日志 ​Redis 机器学习 推荐模型 数据备份 信息化 智能运维 网络架构 网络配置 AI 助手 高可用 Canal B+Tree ID 字段 优化 万能公式 • 索引 • 数据库 RDB AOF Redis 8.0 GreatSQL 连接数 Postgres OTel Iceberg Python 缓存 核心架构 订阅机制 同城 双活 网络故障 INSERT COMPACT Hash 字段 自动重启 Web 云原生 微软 SQL Server AI功能 prometheus Alert 不宕机 IT运维 大模型 SVM Embedding 引擎 性能 分库 分表 高效统计 今天这篇文章就跟大家 虚拟服务器 虚拟机 内存 向量库 Milvus OAuth2 Token Entity 开发 Netstat Linux 服务器 端口 单点故障 sqlmock ZODB 容器化 DBMS 管理系统 MongoDB 容器 LRU 崖山 新版本 Undo Log 数据集成工具 窗口 函数 mini-redis INCR指令 JOIN 悲观锁 乐观锁 openHalo SpringAI 分布式 集中式 磁盘架构 QPS 高并发 排行榜 排序 Redka 数据脱敏 加密算法 Redisson 锁芯 PostGIS Testcloud 云端自动化 分页 数据结构 EasyExcel MySQL8 Recursive 聚簇索引 非聚簇索引 数据类型 模型 启动故障 大表 业务场景 StarRocks 数据仓库 意向锁 记录锁 分布式架构 分布式锁​ Pottery 原子性 AIOPS R2DBC IT Caffeine CP MCP 开放协议 SSH 网络 InfluxDB RAG HelixDB 1 工具链 事务 Java 发件箱模式 池化技术 连接池 Web 接口 dbt 数据转换工具 部署 数据分类 filelock 对象 速度 服务器中毒 事务隔离 字典 传统数据库 向量化 优化器 读写 Go 数据库迁移 仪表盘 Order 单线程 双引擎 频繁 Codis 分页方案 排版 数据页 线程安全 LLM Crash 代码 List 类型 事务同步 UUIDv7 主键 订单 Pump 日志 Ansible