【连载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 的效果:
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 机制还有任何疑问,也能够提出来,大家一起讨论学习!
 
                    
                     
                    
                 
                    
                 
                
            
         
 
         浙公网安备 33010602011771号
浙公网安备 33010602011771号