【连载3】MySQL 的 MVCC 机制剖析

目录

什么是 MVCC?

MVCC(Multi-Version Concurrency Control,多版本并发控制)是 MySQL 中 InnoDB 存储引擎实现隔离级别的基础机制,它通过保存数据的多个版本,实现了读写不冲突,从而提高了数据库的并发性能。

与传统的锁机制不同,MVCC 允许读操作不加锁,读操作不会阻塞写操作,写操作也不会阻塞读操作,这使得数据库在高并发场景下依然能保持良好的性能。

数据版本的管理

InnoDB 存储引擎为每行数据添加了两个隐藏列:

DB_TRX_ID:记录最后一次修改该数据的事务 ID

DB_ROLL_PTR:指向该数据的 undo log(回滚日志)记录

当事务修改数据时,InnoDB 不会直接覆盖旧数据,而是创建一个新的数据版本,并将旧版本的数据保留在 undo log 中。通过 undo log 链,我们可以追溯到数据的各个历史版本。

读操作流程

MVCC 实现了两种读操作:

**1.快照读(Snapshot Read):**读取的是数据的快照版本,不加锁,普通的 SELECT 语句就是快照读
2.当前读(Current Read): 读取的是数据的最新版本,需要加锁,如 SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE

快照读通过事务的 Read View(读视图)来确定可见的数据版本。Read View 包含了当前活跃事务的 ID 列表,通过比较数据版本的 DB_TRX_ID 与 Read View 中的事务 ID,来判断该版本是否可见。

写操作流程

当事务修改数据时,InnoDB 会:

  • 为该事务分配一个唯一的事务 ID(TRX_ID)
  • 创建数据的新版本,并将新版本的 DB_TRX_ID 设置为当前事务 ID
  • 将旧版本的数据写入 undo log,并更新新版本的 DB_ROLL_PTR 指向该 undo log 记录
  • 事务提交后,该版本成为最新版本

C# 操作 MySQL 示例代码

下面是一个 C# 操作 MySQL 的示例,展示了在并发场景下 MVCC 的效果:

csharp 复制代码
using MySqlConnector;
using System;
using System.Threading.Tasks;

class MvccExample
{
    private static string connectionString = "server=localhost;database=test;user=root;password=your_password;";
    private const int MaxRetryCount = 3;  // 最大重试次数
    private const int RetryDelayMs = 1000;  // 重试延迟时间

    static async Task Main(string[] args)
    {
        // 初始化测试数据
        await InitializeTestData();

        // 启动两个并发任务模拟多版本并发控制
        var task1 = Task.Run(Transaction1);
        var task2 = Task.Run(Transaction2WithRetry);

        await Task.WhenAll(task1, task2);

        // 查看最终结果
        await ShowFinalResult();
    }

    static async Task InitializeTestData()
    {
        using (var connection = new MySqlConnection(connectionString))
        {
            await connection.OpenAsync();
            
            // 创建测试表
            using (var command = new MySqlCommand(
                "CREATE TABLE IF NOT EXISTS products (" +
                "id INT PRIMARY KEY AUTO_INCREMENT, " +
                "name VARCHAR(50) NOT NULL, " +
                "price DECIMAL(10,2) NOT NULL)", connection))
            {
                await command.ExecuteNonQueryAsync();
            }

            // 清空表并插入测试数据
            using (var command = new MySqlCommand(
                "TRUNCATE TABLE products; " +
                "INSERT INTO products (name, price) VALUES ('测试商品', 100.00)", connection))
            {
                await command.ExecuteNonQueryAsync();
            }
        }
    }

    static async Task Transaction1()
    {
        using (var connection = new MySqlConnection(connectionString))
        {
            await connection.OpenAsync();
            using (var transaction = await connection.BeginTransactionAsync())
            {
                try
                {
                    Console.WriteLine("事务1: 开始");
                    
                    // 读取商品价格(快照读)
                    using (var command = new MySqlCommand(
                        "SELECT price FROM products WHERE id = 1", connection, transaction))
                    {
                        var price = (decimal)await command.ExecuteScalarAsync();
                        Console.WriteLine($"事务1: 读取到价格: {price}");
                    }

                    // 模拟处理时间,让另一个事务有机会执行
                    await Task.Delay(2000);

                    // 更新商品价格
                    using (var command = new MySqlCommand(
                        "UPDATE products SET price = price * 1.1 WHERE id = 1", connection, transaction))
                    {
                        await command.ExecuteNonQueryAsync();
                        Console.WriteLine("事务1: 价格提高10%");
                    }

                    // 再次读取价格(当前读)
                    using (var command = new MySqlCommand(
                        "SELECT price FROM products WHERE id = 1 FOR UPDATE", connection, transaction))
                    {
                        var price = (decimal)await command.ExecuteScalarAsync();
                        Console.WriteLine($"事务1: 更新后读取到价格: {price}");
                    }

                    await transaction.CommitAsync();
                    Console.WriteLine("事务1: 提交成功");
                }
                catch (Exception ex)
                {
                    await transaction.RollbackAsync();
                    Console.WriteLine($"事务1: 发生错误并回滚: {ex.Message}");
                }
            }
        }
    }

    static async Task Transaction2WithRetry()
    {
        int retryCount = 0;
        while (retryCount < MaxRetryCount)
        {
            try
            {
                await Transaction2Implementation();
                return;
            }
            catch (MySqlException ex) when (ex.Number == 1205)  // 锁等待超时错误码
            {
                retryCount++;
                if (retryCount >= MaxRetryCount)
                {
                    Console.WriteLine($"事务2: 已达到最大重试次数({MaxRetryCount}),操作失败");
                    return;
                }
                Console.WriteLine($"事务2: 锁等待超时,将在{RetryDelayMs}ms后重试(第{retryCount}次)");
                await Task.Delay(RetryDelayMs);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"事务2: 发生错误: {ex.Message}");
                return;
            }
        }
    }

    static async Task Transaction2Implementation()
    {
        using (var connection = new MySqlConnection(connectionString))
        {
            await connection.OpenAsync();
            // 设置事务超时时间为10秒
            using (var transaction = await connection.BeginTransactionAsync(
                System.Data.IsolationLevel.RepeatableRead))
            {
                try
                {
                    Console.WriteLine("事务2: 开始");
                    await Task.Delay(500);

                    // 读取商品价格
                    using (var command = new MySqlCommand(
                        "SELECT price FROM products WHERE id = 1", connection, transaction))
                    {
                        var price = (decimal)await command.ExecuteScalarAsync();
                        Console.WriteLine($"事务2: 读取到价格: {price}");
                    }

                    // 更新商品价格
                    using (var command = new MySqlCommand(
                        "UPDATE products SET price = price * 1.2 WHERE id = 1", connection, transaction))
                    {
                        await command.ExecuteNonQueryAsync();
                        Console.WriteLine("事务2: 价格提高20%");
                    }

                    await transaction.CommitAsync();
                    Console.WriteLine("事务2: 提交成功");
                }
                catch (Exception ex)
                {
                    await transaction.RollbackAsync();
                    throw;  // 抛出异常让重试机制处理
                }
            }
        }
    }

    static async Task ShowFinalResult()
    {
        using (var connection = new MySqlConnection(connectionString))
        {
            await connection.OpenAsync();
            using (var command = new MySqlCommand(
                "SELECT price FROM products WHERE id = 1", connection))
            {
                var price = (decimal)await command.ExecuteScalarAsync();
                Console.WriteLine($"最终价格: {price}");
            }
        }
    }
}

MVCC 常见的坑

1.对快照读的误解

很多开发者认为在同一个事务中,多次执行相同的 SELECT 语句会得到相同的结果,但实际上这只在 REPEATABLE READ 隔离级别下成立。如果使用 READ COMMITTED 隔离级别,每次查询都会获取新的快照,可能看到其他事务已提交的修改。

2.undo log 膨胀问题

MVCC 需要保存数据的多个版本,这会导致 undo log 不断增长。如果存在长事务,会阻止 undo log 的回收,可能导致磁盘空间耗尽。

3.幻读问题

在 REPEATABLE READ 隔离级别下,MVCC 可以解决不可重复读问题,但无法完全解决幻读问题。需要使用间隙锁(Gap Lock)来防止幻读。

4.对事务隔离级别的误用

很多开发者不了解不同隔离级别下 MVCC 的行为差异,错误地选择了隔离级别。例如,在需要严格一致性的场景下使用了 READ COMMITTED 级别。

5.忽略当前读的锁机制

使用 SELECT ... FOR UPDATE 等当前读操作时,会加行锁,如果不注意可能导致死锁或长时间阻塞。

互动

MVCC 是 MySQL 中非常重要但也比较复杂的机制,理解它对于编写高效、正确的数据库操作代码至关重要。

你在使用 MySQL 时遇到过哪些与 MVCC 相关的问题?是如何解决的?欢迎在评论区分享你的经验和见解。如果对 MVCC 机制还有任何疑问,也可以提出来,我们一起讨论学习!

相关推荐
a***11351 分钟前
使用Django Rest Framework构建API
数据库·django·sqlite
KaiwuDB24 分钟前
KWDB 3.0.0 正式发布!年度重磅升级,重塑 AIoT 产业智能数据基座
数据库
凯子坚持 c35 分钟前
openGauss向量数据库技术演进与AI应用生态全景
数据库·人工智能
z***56561 小时前
【玩转全栈】----Django模板语法、请求与响应
数据库·python·django
韩立学长1 小时前
【开题答辩实录分享】以《智慧农业信息化服务平台小程序》为例进行答辩实录分享
数据库·spring boot·小程序
java_logo1 小时前
Kubernetes Dashboard Docker 容器化部署指南
运维·mysql·docker·云原生·容器·kubernetes·php
一 乐2 小时前
农产品电商|基于SprinBoot+vue的农产品电商系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·spring boot
TDengine (老段)2 小时前
TDengine IDMP 赋能新能源:光伏电站智能运维实践
大数据·运维·数据库·物联网·时序数据库·tdengine·涛思数据
Chan162 小时前
热点数据自动缓存方案:基于京东 Hotkey 实践
java·数据库·redis·mysql·spring·java-ee·intellij-idea
Leisure -_-2 小时前
Oracle Database 26ai Free 安装讲解
数据库·oracle