• MVCC 机制的原理及实现

MVCC 机制的原理及实现

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

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