一、当数据库变成"钉子户":锁的烦恼现场
最近团队里刚入职的小王遇到了件怪事:每当促销活动时,订单系统的库存扣减总会随机失败。通过日志发现大量这样的错误提示:"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);
}
五、锁优化的三重境界
- 索引优化:确保WHERE条件字段都有合适索引
- 访问顺序:统一多个表的操作顺序
- 超时设置:合理配置
innodb_lock_wait_timeout
六、技术选型的平衡艺术
行级锁 vs 表级锁:
- 行级锁适合高并发更新
- 表级锁适合批量操作
悲观锁 vs 乐观锁:
- 悲观锁适合写多读少
- 乐观锁适合读多写少
七、血的教训:五个必知禁忌
- 避免在事务中执行远程调用
- 不要忘记设置合理的超时时间
- 警惕NOLOCK提示的副作用
- 慎用长时间运行的事务
- 禁止在循环中执行更新操作
八、未来之路:分布式锁的思考
随着系统规模扩大,可以结合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锁问题的核心方法。记住,最好的锁策略是让锁尽可能少地出现,而不是出现后如何解决。
最后送大家一句口诀:"索引到位锁自消,事务简短性能高,监控预警不可少,乐观策略是绝招"。愿各位的数据库永远畅通无阻!