引言

在分布式系统开发中,并发控制是一个永恒的话题。最近在代码评审时,我看到了这样一段代码:

// 复杂操作仍需 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用户执行完,而不是直接失败。整个过程是自动的,不需要人工干预。

具体执行流程

  1. A用户进入lock块,开始执行下单操作(耗时1秒)
  2. B用户的线程到达lock语句时,发现_lockObj已被锁定
  3. B线程会阻塞并等待,直到A线程释放锁
  4. A用户执行完毕,释放锁
  5. 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分布式锁的优势

  1. 解决分布式问题:支持多实例部署
  2. 自动过期防止死锁
// 即使进程崩溃,锁也会自动释放
var acquired = await _redis.StringSetAsync(
    key, token, expireTime, When.NotExists);
  1. 可扩展架构:同一用户的请求在不同实例间也能正确同步

Redis分布式锁的挑战

  1. 性能开销:每次锁操作都需要网络IO(增加20ms+延迟)
  2. 复杂性增加
// 需要处理各种边界情况
public class RedisDistributedLock
{
    // 锁续期问题、时钟漂移、锁误释放等
    public async Task StartLockRenewal() { /* ... */ }
    public async Task<bool> ValidateLock() { /* ... */ }
}
  1. 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;
    }
}

为什么数据库事务是最佳选择

  1. ACID保证:原子性、一致性、隔离性、持久性
  2. 数据库优化:成熟的锁机制和死锁检测
  3. 跨进程安全:多实例部署也能保证数据一致性
  4. 异常恢复:自动回滚机制
  5. 可扩展性:适合分布式系统

第三部分:综合对比与实战建议

方案对比表

特性 数据库事务 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分布式锁的场景:

  • 分布式系统需要跨实例同步
  • 业务逻辑控制(防重复提交、抢购等)
  • 可以接受一定网络延迟的场景

使用内存细粒度锁的场景:

  • 单机内存操作,不涉及数据库
  • 缓存更新、内存计算、临时状态管理
  • 性能要求极高的单机应用

第四部分:最佳实践总结

  1. 让专业的工具做专业的事:数据一致性交给数据库,业务逻辑并发控制用合适的锁策略

  2. 分层设计

    • 表现层:防重复提交(Token机制)
    • 业务层:业务逻辑控制(Redis锁)
    • 数据层:数据一致性(数据库事务)
  3. 降级策略:任何分布式组件都可能失败,要有降级方案

public async Task<bool> PlaceOrder(Order order)
{
    try
    {
        var lock = await _redisLock.AcquireAsync();
        // 正常流程
    }
    catch (RedisConnectionException)
    {
        // 降级策略:使用数据库悲观锁或直接拒绝请求
        return await FallbackWithDatabaseLock(order);
    }
}
  1. 监控与告警:对锁等待时间、获取失败率等关键指标进行监控

结语

并发控制是分布式系统设计的核心挑战之一。通过本文的分析,我们可以看到:

  • 单机锁简单但局限性大
  • 细粒度锁提升性能但复杂度增加
  • Redis分布式锁解决分布式问题但引入网络依赖
  • 数据库事务在数据一致性场景下仍然是最可靠的选择

在实际项目中,没有银弹。我们需要根据具体的业务需求、系统架构和性能要求,选择合适的并发控制策略,很多时候还需要组合使用多种技术。

核心原则:在保证数据正确性的前提下,尽可能提高系统的并发处理能力。

希望本文对你理解和设计分布式系统的并发控制有所帮助!