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

相关推荐
ZeroNews内网穿透1 分钟前
RStudio Server 结合 ZeroNews,实现远程访问管理
运维·服务器·网络·数据库·网络协议·安全·web安全
李白你好5 分钟前
Redis 漏洞图形化利用工具
数据库·redis·缓存
JosieBook12 分钟前
【数据库】IoTDB数据库与时序大模型深度融合,开启数据智能分析新范式
数据库·iotdb
betazhou16 分钟前
docker容器单机创建3个节点的MySQLMGR集群
运维·mysql·docker·容器·集群·mgr
Logic10123 分钟前
《Mysql数据库应用》 第2版 郭文明 实验4 视图和索引的构建与使用核心操作与思路解析
数据库·sql·mysql·学习笔记·计算机网络技术·形考作业·国家开放大学
码农12138号23 分钟前
网络安全-SQL注入
数据库·web安全·sql注入
破刺不会编程25 分钟前
ubuntu环境下mysql的安装
数据库·mysql
合方圆~小文26 分钟前
变焦摄像头画面时间戳同步方案
数据结构·数据库
QQ129584550429 分钟前
SSAS-检查字段里的不可见字符
数据库·数据仓库·数据分析
dblens 数据库管理和开发工具32 分钟前
DBLens for MySQL | MySQL 数据库管理和开发工具
数据库·mysql·mysql数据库管理和开发工具