一、当数据库变成"钉子户":锁的烦恼现场

最近团队里刚入职的小王遇到了件怪事:每当促销活动时,订单系统的库存扣减总会随机失败。通过日志发现大量这样的错误提示:"Lock wait timeout exceeded"(锁等待超时)。这就像超市收银台突然被某个顾客长期霸占,后面排队的顾客只能干着急。

我们使用的技术栈是:

  • C# 8.0
  • MySqlConnector 2.3.8
  • MySQL 8.0.26
  • Dapper 2.0.123

以下是一个典型的异常场景代码示例:

// 问题示例:库存扣减操作
public async Task DeductStock(int productId, int quantity)
{
    using var connection = new MySqlConnection(_connectionString);
    await connection.OpenAsync();
    
    using var transaction = await connection.BeginTransactionAsync();
    try
    {
        // 显式锁定要更新的行(排他锁)
        var stock = await connection.QueryFirstOrDefaultAsync<Stock>(
            "SELECT * FROM product_stock WHERE product_id = @productId FOR UPDATE",
            new { productId }, transaction);

        if (stock.Quantity >= quantity)
        {
            await connection.ExecuteAsync(
                "UPDATE product_stock SET quantity = quantity - @quantity WHERE product_id = @productId",
                new { quantity, productId }, transaction);
        }
        
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

这段看似合理的代码在高并发场景下却成了性能瓶颈。FOR UPDATE子句像给数据行上了把密码锁,当多个请求同时到达时,后来的请求只能排队等候,最终引发超时。

二、破锁工具箱:九种应对策略

2.1 快照读妙用(技术栈:MySQL InnoDB)

// 解决方案1:使用读已提交隔离级别
public async Task UpdateWithSnapshotRead()
{
    using var connection = new MySqlConnection(_connectionString);
    await connection.OpenAsync();
    
    // 设置隔离级别为读已提交
    await connection.ExecuteAsync("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
    
    using var transaction = await connection.BeginTransactionAsync();
    try
    {
        // 不再使用FOR UPDATE,依赖MVCC机制
        var stock = await connection.QueryFirstAsync<Stock>(
            "SELECT quantity FROM product_stock WHERE product_id = @productId",
            new { productId }, transaction);

        if (stock.Quantity >= quantity)
        {
            await connection.ExecuteAsync(
                "UPDATE product_stock SET quantity = quantity - @quantity WHERE product_id = @productId",
                new { quantity, productId }, transaction);
        }
        
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

优点:利用MVCC机制避免读锁
缺点:需要确保所有操作在事务内完成
适用场景:读多写少的库存系统

2.2 乐观锁巧解(技术栈:版本号控制)

// 解决方案2:版本号乐观锁
public async Task OptimisticLockUpdate(int productId, int quantity)
{
    using var connection = new MySqlConnection(_connectionString);
    await connection.OpenAsync();
    
    var retryCount = 0;
    while (retryCount < 3)
    {
        var stock = await connection.QueryFirstAsync<Stock>(
            "SELECT quantity, version FROM product_stock WHERE product_id = @productId",
            new { productId });

        if (stock.Quantity < quantity) throw new InsufficientStockException();
        
        var affectedRows = await connection.ExecuteAsync(
            @"UPDATE product_stock 
              SET quantity = quantity - @quantity, version = version + 1 
              WHERE product_id = @productId AND version = @version",
            new { quantity, productId, version = stock.Version });
        
        if (affectedRows > 0) return;
        retryCount++;
        await Task.Delay(50 * retryCount);
    }
    throw new ConcurrentUpdateException();
}

优势:避免显式锁带来的阻塞
注意:需要设计重试机制和版本号字段

三、死锁迷局的破译指南

当两个事务互相等待对方持有的锁时,就会发生死锁。通过以下代码可以捕获并处理死锁:

public async Task SafeTransactionOperation()
{
    const int maxRetries = 3;
    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        using var connection = new MySqlConnection(_connectionString);
        await connection.OpenAsync();
        
        using var transaction = await connection.BeginTransactionAsync();
        try
        {
            // 业务操作...
            await transaction.CommitAsync();
            return;
        }
        catch (MySqlException ex) when (ex.ErrorCode == MySqlErrorCode.LockDeadlock)
        {
            if (attempt == maxRetries) throw;
            await Task.Delay(100 * (int)Math.Pow(2, attempt));
        }
    }
}

四、锁监控的三种法宝

4.1 实时锁查询

-- 在MySQL客户端执行
SHOW ENGINE INNODB STATUS;

重点关注LATEST DETECTED DEADLOCK

4.2 C#诊断工具

public async Task MonitorLockInfo()
{
    using var connection = new MySqlConnection(_connectionString);
    var lockInfo = await connection.QueryAsync(
        @"SELECT 
            r.trx_id waiting_trx_id,
            r.trx_query waiting_query,
            b.trx_id blocking_trx_id,
            b.trx_query blocking_query
          FROM information_schema.innodb_lock_waits w
          INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
          INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id");
    
    // 将锁信息输出到日志系统
    _logger.LogInformation("当前锁等待信息:{LockInfo}", lockInfo);
}

五、锁优化的三重境界

  1. 索引优化:确保WHERE条件字段都有合适索引
  2. 访问顺序:统一多个表的操作顺序
  3. 超时设置:合理配置innodb_lock_wait_timeout

六、技术选型的平衡艺术

行级锁 vs 表级锁

  • 行级锁适合高并发更新
  • 表级锁适合批量操作

悲观锁 vs 乐观锁

  • 悲观锁适合写多读少
  • 乐观锁适合读多写少

七、血的教训:五个必知禁忌

  1. 避免在事务中执行远程调用
  2. 不要忘记设置合理的超时时间
  3. 警惕NOLOCK提示的副作用
  4. 慎用长时间运行的事务
  5. 禁止在循环中执行更新操作

八、未来之路:分布式锁的思考

随着系统规模扩大,可以结合Redis实现分布式锁:

// 伪代码示例
public async Task DistributedLockOperation()
{
    var redisLock = _redisClient.CreateLock("stock_lock", TimeSpan.FromSeconds(10));
    if (await redisLock.AcquireAsync())
    {
        try
        {
            // 执行关键操作
        }
        finally
        {
            await redisLock.ReleaseAsync();
        }
    }
}

九、破锁专家的终极感悟

处理数据库锁就像管理交通拥堵,需要(1)提前规划索引路线(2)设置合理的信号灯(事务隔离级别)(3)建立应急通道(死锁处理机制)。通过本文的九种武器,相信你已经掌握了破解MySQL锁问题的核心方法。记住,最好的锁策略是让锁尽可能少地出现,而不是出现后如何解决。

最后送大家一句口诀:"索引到位锁自消,事务简短性能高,监控预警不可少,乐观策略是绝招"。愿各位的数据库永远畅通无阻!