【连载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 机制还有任何疑问,也可以提出来,我们一起讨论学习!

相关推荐
235162 小时前
【MySQL】MVCC:从核心原理到幻读解决方案
java·数据库·后端·sql·mysql·缓存
zym大哥大2 小时前
高并发内存池
服务器·数据库·windows
. . . . .3 小时前
数据库迁移migration
数据库
shixian10304113 小时前
Django 学习日志
数据库·学习·sqlite
IT 小阿姨(数据库)4 小时前
PostgreSQL通过pg_basebackup物理备份搭建流复制备库(Streaming Replication Standby)
运维·服务器·数据库·sql·postgresql·centos
小蒜学长5 小时前
springboot基于javaweb的小零食销售系统的设计与实现(代码+数据库+LW)
java·开发语言·数据库·spring boot·后端
云边有个稻草人5 小时前
从内核调优到集群部署:基于Linux环境下KingbaseES数据库安装指南
linux·数据库·金仓数据库管理系统
EnCi Zheng5 小时前
JPA 连接 PostgreSQL 数据库完全指南
java·数据库·spring boot·后端·postgresql
Raymond运维5 小时前
MySQL包安装 -- RHEL系列(Yum资源库安装MySQL)
linux·数据库·mysql