• MVCC 机制的原理及实现

MVCC 机制的原理及实现

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

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