一、为什么需要Redis分布式锁?

上周我们团队遇到了一个诡异的BUG:用户抢购优惠券时,出现了同一张券被重复领取的情况。检查代码发现,虽然用了数据库事务,但在高并发场景下多个请求同时通过验证。这时候老张掏出手机:"用Redis做个分布式锁吧!"

典型应用场景

  1. 电商秒杀系统中库存扣减
  2. 文件上传时的重复提交拦截
  3. 定时任务调度防多节点重复执行
  4. 支付系统的订单状态变更

就像医院候诊室的叫号系统,分布式锁就是那个确保"每次只进一人"的电子显示屏,防止多个服务实例同时操作关键资源。

二、技术选型:为什么选择StackExchange.Redis?

在.NET生态中,常见的Redis客户端有:

客户端库 维护状态 性能 功能完整性
StackExchange 活跃 完整
ServiceStack 停滞 部分缺失
CSRedis 活跃 轻量

我们选择StackExchange.Redis的三大理由:

  1. 官方推荐,微软Azure认证
  2. 支持异步编程模型
  3. 完善的连接池管理和重试机制

三、手把手实现Redis锁

示例1:基础锁实现

// 创建Redis连接(单例模式最佳)
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost");
IDatabase db = redis.GetDatabase();

// 获取锁
public async Task<bool> AcquireLock(string lockKey, string clientId, TimeSpan expiry)
{
    // 使用SET命令的NX和EX参数保证原子性操作
    return await db.StringSetAsync(
        lockKey,
        clientId,
        expiry,
        When.NotExists,
        CommandFlags.DemandMaster);
}

// 释放锁
public async Task ReleaseLock(string lockKey, string clientId)
{
    // Lua脚本保证原子性验证和删除
    string script = 
        @"if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
          else
            return 0
          end";
    
    await db.ScriptEvaluateAsync(
        script,
        new RedisKey[] { lockKey },
        new RedisValue[] { clientId });
}

代码要点解析:

  1. When.NotExists确保只有不存在时才设置值
  2. 客户端ID使用Guid保证全局唯一
  3. Lua脚本解决"先判断后删除"的原子性问题

示例2:带自动续期的锁

public class AutoRenewalLock : IDisposable
{
    private readonly IDatabase _db;
    private readonly string _lockKey;
    private readonly string _clientId;
    private readonly TimeSpan _expiry;
    private Timer _renewTimer;

    public AutoRenewalLock(IDatabase db, string lockKey, TimeSpan expiry)
    {
        _db = db;
        _lockKey = lockKey;
        _clientId = Guid.NewGuid().ToString();
        _expiry = expiry;
        
        // 启动续期定时器
        _renewTimer = new Timer(
            callback: async _ => await RenewLock(),
            state: null,
            dueTime: (int)_expiry.TotalMilliseconds / 2,
            period: (int)_expiry.TotalMilliseconds / 2);
    }

    private async Task RenewLock()
    {
        await _db.StringSetAsync(
            _lockKey,
            _clientId,
            _expiry,
            When.Exists);
    }

    public void Dispose()
    {
        _renewTimer?.Dispose();
        ReleaseLock().Wait();
    }

    private async Task ReleaseLock()
    {
        var script = "..."; // 同基础锁的Lua脚本
        await _db.ScriptEvaluateAsync(script, new[] { _lockKey }, new[] { _clientId });
    }
}

四、避坑指南:常见问题与解决方案

1. 锁超时难题

某电商平台曾因锁超时设置不当导致库存超卖:

  • ❌ 错误做法:固定30秒超时
  • ✅ 正确方案:动态计算(业务耗时*3 + 网络缓冲)

推荐公式:

超时时间 = 平均业务耗时 × 3 + 网络延迟缓冲(建议200ms~500ms)

2. 锁误删防护

通过三层防护确保安全:

  1. 客户端唯一标识(UUID)
  2. Lua脚本原子验证
  3. 锁版本号机制(类似CAS)

五、技术方案对比分析

Redis锁 vs 数据库锁

维度 Redis锁 数据库锁
性能 10万+ QPS 1000 QPS
实现复杂度 中等 简单
可靠性 依赖Redis可用性 依赖数据库事务
适用场景 高频短操作 低频长事务

RedLock算法进阶

当需要更高可靠性时,可以采用RedLock算法:

  1. 向5个Redis实例顺序获取锁
  2. 半数以上成功视为获取成功
  3. 总耗时需小于锁有效期
// RedLock.NET示例
var redlockFactory = RedLockFactory.Create(
    new List<RedLockMultiplexer> { connection });
using var redLock = await redlockFactory.CreateLockAsync(
    "resourceKey",
    TimeSpan.FromSeconds(30));

六、最佳实践路线

实施步骤建议

  1. 评估业务场景的并发强度
  2. 确定可接受的异常边界
  3. 选择基础锁或RedLock方案
  4. 建立监控指标(锁等待时间、冲突率)
  5. 设计降级方案(如本地锁+Redis锁组合)

性能优化技巧

  1. 连接池大小设置:建议最大连接数=并发线程数×1.5
  2. 序列化优化:优先使用MessagePack代替JSON
  3. 批量操作:使用IBatch接口提升吞吐量

七、总结与展望

经过多个项目的实战检验,合理使用Redis分布式锁可以解决90%的并发问题。但需要注意:

  • 锁粒度要尽可能小(如按用户ID分段)
  • 必须设置合理的超时时间
  • 配合熔断机制防止Redis故障扩散

未来趋势方面,随着.NET 6的普及,可以尝试将锁逻辑与Channel结合,实现更高效的事件驱动架构。但核心思想不会变——就像交通信号灯,分布式锁的本质是资源访问的协调者。