DistributedLock 实现:在 .NET 中构建可靠的分布式锁(Redis / SQL / 库实战)

为什么需要分布式锁

在分布式系统中,多实例并发访问共享资源(比如执行定时任务、写同一条记录、生成唯一序号等)时必须保证互斥,否则会出现竞态、重复处理或数据不一致。分布式锁提供跨进程/跨主机的互斥机制,确保在同一时间只有一个节点对关键资源执行操作。

常见实现方案概览

基于 Redis 的 Key+TTL(Redlock):通过在多个独立 Redis 实例上加锁并使用超时/租约保证,适合高性能场景。

基于数据库(例如 SQL Server 的 sp_getapplock / PostgreSQL advisory locks):利用数据库内建的锁机制实现可靠互斥,适合已有数据库且不想引入新依赖的场景。

使用成熟的 .NET 分布式锁库(如 DistributedLock、RedLock.net 等):用库封装复杂细节,生产中更省心。

设计要点(无论哪种实现都应注意)

锁的可重入性与租约(lease):多数分布式锁使用租约(TTL)来避免死锁,租约应略大于预期处理时间,或支持续租。

超时与失败回退(fallback):获取锁时设置合理等待超时,获取失败可选择重试或返回错误。

释放锁的安全性:释放锁时必须确保只有持有者能够释放(通常通过锁值/令牌校验),避免误释放。

时钟漂移与分布式一致性:分布式系统中时钟不一致会影响判断,Redlock 等算法有相关考虑;若使用单实例 Redis 要意识到其局限性。

幂等与补偿:即使锁出现失效或重入,业务应尽量保持幂等或提供补偿机制。

方案一:使用 DistributedLock(开源 .NET 库)——极简上手

DistributedLock 是常用的 .NET 库,封装了多种后端(SQL Server、Redis 等),提供 AcquireAsync() 风格的 API,使用起来像本地 lock。示例:

// 安装 NuGet: DistributedLock (或 DistributedLock.SqlServer / DistributedLock.Redis)
using DistributedLock; // depending on package

// 假设使用 SQL Server 后端
var connectionString = "Server=...;Database=...;User Id=...;Password=...;";
await using var provider = new SqlDistributedSynchronizationProvider(connectionString);

var resource = "my-app:process-order:order-123";
var timeout = TimeSpan.FromSeconds(10);

await using (var handle = await provider.AcquireAsync(resource, timeout))
{
    if (!handle.IsAcquired)
    {
        // 获取失败:可重试或返回
        return;
    }

    // 在此处安全地处理共享资源
    ProcessOrder("order-123");
}

优点:API 简洁,支持 reader-writer 等多种锁类型;对常见后端提供封装,减少自己实现细节的风险。

方案二:Redis + Redlock(高性能选项)

核心思想:在多个独立 Redis 实例上以相同 key 设置带 TTL 的锁,只有在多数实例上成功设置时视为加锁成功,从而降低单点故障影响。使用成熟实现(如 RedLock.net)能正确处理续租、时钟漂移与冲突。示例(使用 RedLock.net):

// 安装 NuGet: RedLock.net
using RedLockNet;
using RedLockNet.SERedis;
using StackExchange.Redis;

// 创建 Redis 连接和 RedLockFactory
var multiplexers = new[] {
    ConnectionMultiplexer.Connect("redis1:6379"),
    ConnectionMultiplexer.Connect("redis2:6379"),
    ConnectionMultiplexer.Connect("redis3:6379")
};

var redlockFactory = RedLockFactory.Create(multiplexers.Select(m => m.GetDatabase()).ToList());

string resource = "locks:process-order:order-123";
TimeSpan expiry = TimeSpan.FromSeconds(30);

using (var redLock = redlockFactory.CreateLock(resource, expiry))
{
    if (redLock.IsAcquired)
    {
        // 拿到锁,执行操作
        ProcessOrder("order-123");
    }
    else
    {
        // 锁获取失败,处理失败逻辑
    }
}

注意:Redlock 的实现与使用有一定争议(需要理解设计假设),但对于多数需要高吞吐、低延迟锁的场景是常见选择。

方案三:利用 SQL Server 的应用级锁(简单可靠)

如果你已经依赖 SQL Server,使用 sp_getapplock / sp_releaseapplock 是非常实用且可靠的方案,优点是事务边界内容易控制,不需要额外组件。示例(使用 Dapper 调用):

using (var conn = new SqlConnection(connString))
{
    await conn.OpenAsync();

    // 获取应用级锁(分布式互斥)
    var result = await conn.ExecuteScalarAsync<int>(
        "EXEC @returnCode = sp_getapplock @Resource = @resource, @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = @timeout",
        new { resource = "process-order:order-123", timeout = 10000, returnCode = 0 });

    // returnCode >= 0 表示获取成功
    if (result >= 0)
    {
        try
        {
            ProcessOrder("order-123");
        }
        finally
        {
            await conn.ExecuteAsync("EXEC sp_releaseapplock @Resource = @resource, @LockOwner = 'Session'", new { resource = "process-order:order-123" });
        }
    }
    else
    {
        // 获取失败
    }
}

优点:事务级别可靠、实现简单;缺点:性能可能不如 Redis,且依赖数据库可用性。

实现细节与示例策略(超时、续租与唯一标识)

1. 使用唯一锁值(token)

在写入锁 key 时写入随机 token(GUID),释放锁时只允许持有该 token 的客户端删除 key,避免误释放(常见 Lua 脚本校验与删除)。

-- Lua: 释放锁的原子操作
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

2. 续租机制

如果业务处理时间可能超过租约,设计续租机制(在持有锁时定期延长 TTL),并在续租失败时安全中止或降级处理。

3. 等待与重试策略

避免无限阻塞:把获取锁的等待时间和重试间隔参数化,使用指数回退或抖动避免羊群效应。

4. 幂等设计

确保被保护的业务操作是幂等的(或能被补偿),以应对锁意外失效或重复执行的情况。

常见坑与误区

  • 单实例 Redis + 简单 SET NX 可能导致不可恢复的竞争:单实例失效会导致不可用或错判。考虑使用 Redlock 或多副本策略。
  • 把分布式锁当成事务替代:锁解决并发,不等于事务的原子性、隔离性;复杂业务仍需数据库事务保证一致性。
  • 忽视释放安全:不校验 token 就删除 key,会导致非持有者误释放。
  • 租约设置不合理:租约太短会导致持有者在处理未完成时锁被释放;太长则降低容错能力。

如何选择适合你的方案

  • 需要超高性能、短租约场景(缓存、计数器):优先考虑 Redis + Redlock(但理解其假设)。
  • 已有稳定数据库、希望简单可靠:使用数据库内建锁(sp_getapplock、advisory locks)。
  • 想快速接入且不想自行实现细节:使用成熟库(DistributedLock、RedLock.net)可显著降低实现错误的概率。

结论与行动建议

  • 先评估场景需求:并发量、延迟、依赖组件(是否已有 Redis/数据库)、容错目标。
  • 优先使用成熟库:避免自行重写分布式锁算法带来的坑。
  • 做好超时、续租与释放安全:锁值校验、续租失败处理与幂等化是关键。
  • 测试故障场景:模拟网络分区、服务重启与时钟漂移,验证锁在异常情况下的表现。
评论