引言
在分布式系统开发中,并发控制是一个永恒的话题。最近在代码评审时,我看到了这样一段代码:
// 复杂操作仍需 lock
public void ComplexOperation()
{
lock (_lockObj)
{
if (_count > 0 && _count < 100)
{
_count *= 2;
// 其他复杂业务逻辑...
}
}
}
这引发了一系列深入的讨论:如果锁里面执行的是用户下单操作,A用户下单过程中执行了1秒,B用户的请求会怎样?是直接失败还是等待?我们需要手动处理这种等待吗?
本文将围绕这个问题展开,深入探讨单机锁、细粒度锁、数据库事务和Redis分布式锁的优缺点及适用场景。
第一部分:单机锁的行为分析
锁的基本行为
当我们在C#中使用lock语句时,CLR会为我们管理一个等待队列:
public void PlaceOrder(Order order)
{
lock (_lockObj) // 所有用户下单都串行化!
{
// 复杂的下单逻辑,耗时1秒...
ProcessOrder(order);
UpdateInventory();
// 其他业务...
}
}
关键结论:B用户会等待A用户执行完,而不是直接失败。整个过程是自动的,不需要人工干预。
具体执行流程
- A用户进入
lock块,开始执行下单操作(耗时1秒) - B用户的线程到达
lock语句时,发现_lockObj已被锁定 - B线程会阻塞并等待,直到A线程释放锁
- A用户执行完毕,释放锁
- B线程获得锁,开始执行自己的下单操作
单机锁的严重问题
虽然锁机制自动处理了同步,但这种设计存在严重问题:
- 性能瓶颈:所有用户请求串行处理
- 响应时间变长:B用户需要等待A用户的1秒 + 自己的1秒 = 至少2秒
- 系统吞吐量下降:无法充分利用多核CPU优势
第二部分:解决方案对比
🥉 方案一:细粒度锁
private readonly Dictionary<string, object> _userLocks = new();
private readonly object _dictLock = new object();
public void PlaceOrder(string userId, Order order)
{
object userLock;
lock (_dictLock)
{
if (!_userLocks.TryGetValue(userId, out userLock))
{
userLock = new object();
_userLocks[userId] = userLock;
}
}
// 只锁同一用户的操作
lock (userLock)
{
ProcessOrder(order);
UpdateInventory();
}
}
优点:
- 性能较好:不同用户可以并发操作
- 实现相对简单
- 单机场景有效
缺点:
- 内存泄漏风险:需要定期清理不用的锁对象
- 分布式系统无效:多实例部署时无法同步
- 死锁风险:复杂操作可能产生死锁
🥈 方案二:Redis分布式锁
基于Redis的分布式锁解决了单机锁的局限性:
public async Task<bool> PlaceOrder(string userId, Order order)
{
var redisLock = new RedisDistributedLock(_redis, $"order_lock:{userId}");
if (await redisLock.AcquireAsync(TimeSpan.FromSeconds(30)))
{
try
{
// 跨多个应用实例也能保证同一用户串行操作
await ProcessOrderAsync(order);
return true;
}
finally
{
await redisLock.ReleaseAsync();
}
}
else
{
// 获取锁失败,可以返回特定错误
return false;
}
}
Redis分布式锁的优势
- 解决分布式问题:支持多实例部署
- 自动过期防止死锁:
// 即使进程崩溃,锁也会自动释放
var acquired = await _redis.StringSetAsync(
key, token, expireTime, When.NotExists);
- 可扩展架构:同一用户的请求在不同实例间也能正确同步
Redis分布式锁的挑战
- 性能开销:每次锁操作都需要网络IO(增加20ms+延迟)
- 复杂性增加:
// 需要处理各种边界情况
public class RedisDistributedLock
{
// 锁续期问题、时钟漂移、锁误释放等
public async Task StartLockRenewal() { /* ... */ }
public async Task<bool> ValidateLock() { /* ... */ }
}
- Redis可用性问题:Redis宕机时整个系统受影响
🥇 方案三:数据库事务(最推荐)
public async Task<bool> PlaceOrder(Order order)
{
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
// 检查库存等业务逻辑
var product = await _dbContext.Products
.FirstAsync(p => p.Id == order.ProductId);
if (product.Stock < order.Quantity)
return false;
// 更新库存
product.Stock -= order.Quantity;
await _dbContext.Orders.AddAsync(order);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
为什么数据库事务是最佳选择
- ACID保证:原子性、一致性、隔离性、持久性
- 数据库优化:成熟的锁机制和死锁检测
- 跨进程安全:多实例部署也能保证数据一致性
- 异常恢复:自动回滚机制
- 可扩展性:适合分布式系统
第三部分:综合对比与实战建议
方案对比表
| 特性 | 数据库事务 | Redis分布式锁 | 内存细粒度锁 | 全局锁 |
|---|---|---|---|---|
| 数据一致性 | ✅ 完美 | ⚠️ 有限 | ⚠️ 有限 | ✅ 好 |
| 分布式支持 | ✅ 是 | ✅ 是 | ❌ 否 | ❌ 否 |
| 性能 | ✅ 高 | ✅ 较高 | ✅ 较高 | ❌ 低 |
| 复杂度 | ✅ 低 | ⚠️ 中 | ⚠️ 中 | ✅ 低 |
| 内存管理 | ✅ 自动 | ✅ 自动 | ❌ 需手动清理 | ✅ 自动 |
| 异常处理 | ✅ 自动回滚 | ⚠️ 需手动处理 | ❌ 需手动处理 | ❌ 需手动处理 |
实战中的混合使用策略
在实际项目中,我们往往需要组合使用多种技术:
public class OrderService
{
public async Task<OrderResult> PlaceOrderAsync(Order order)
{
// 1. 快速内存检查(性能优化)拦截大部分无效请求
lock (GetUserLock(order.UserId))
{
if (_localCache.HasRecentOrder(order.UserId))
return OrderResult.Failed("操作过于频繁");
}
// 2. 先用Redis锁防重复提交(业务逻辑控制)
await using var redisLock = await _distributedLock
.AcquireAsync($"order:{order.UserId}", TimeSpan.FromSeconds(10));
if (redisLock == null)
return OrderResult.Failed("系统繁忙,请重试");
// 3. 核心业务用数据库事务保证数据一致性
using var dbTransaction = await _dbContext.Database.BeginTransactionAsync();
try
{
var product = await _dbContext.Products
.FirstAsync(p => p.Id == order.ProductId);
if (product.Stock < order.Quantity)
return OrderResult.Failed("库存不足");
product.Stock -= order.Quantity;
await _dbContext.Orders.AddAsync(order);
await _dbContext.SaveChangesAsync();
await dbTransaction.CommitAsync();
// 4. 更新缓存
_localCache.RecordOrder(order.UserId);
return OrderResult.Success();
}
catch
{
await dbTransaction.RollbackAsync();
throw;
}
}
}
场景化建议
使用数据库事务的场景:
- 核心数据修改:订单、库存、账户余额
- 需要强一致性保证的业务
- 已经部署了数据库,不想引入额外依赖
使用Redis分布式锁的场景:
- 分布式系统需要跨实例同步
- 业务逻辑控制(防重复提交、抢购等)
- 可以接受一定网络延迟的场景
使用内存细粒度锁的场景:
- 单机内存操作,不涉及数据库
- 缓存更新、内存计算、临时状态管理
- 性能要求极高的单机应用
第四部分:最佳实践总结
-
让专业的工具做专业的事:数据一致性交给数据库,业务逻辑并发控制用合适的锁策略
-
分层设计:
- 表现层:防重复提交(Token机制)
- 业务层:业务逻辑控制(Redis锁)
- 数据层:数据一致性(数据库事务)
-
降级策略:任何分布式组件都可能失败,要有降级方案
public async Task<bool> PlaceOrder(Order order)
{
try
{
var lock = await _redisLock.AcquireAsync();
// 正常流程
}
catch (RedisConnectionException)
{
// 降级策略:使用数据库悲观锁或直接拒绝请求
return await FallbackWithDatabaseLock(order);
}
}
- 监控与告警:对锁等待时间、获取失败率等关键指标进行监控
结语
并发控制是分布式系统设计的核心挑战之一。通过本文的分析,我们可以看到:
- 单机锁简单但局限性大
- 细粒度锁提升性能但复杂度增加
- Redis分布式锁解决分布式问题但引入网络依赖
- 数据库事务在数据一致性场景下仍然是最可靠的选择
在实际项目中,没有银弹。我们需要根据具体的业务需求、系统架构和性能要求,选择合适的并发控制策略,很多时候还需要组合使用多种技术。
核心原则:在保证数据正确性的前提下,尽可能提高系统的并发处理能力。
希望本文对你理解和设计分布式系统的并发控制有所帮助!