C#中如何用Redis和数据库实现可靠的分布式锁?

来源:网站运营作者:弥生美月头衔:网络博主
导读:本期聚焦于弥生美月创作的《C#中如何用Redis和数据库实现可靠的分布式锁?》,敬请观看详情。分布式锁的本质是在多个进程之间协调对共享资源的互斥访问,单机lock和Monitor无法跨越进程边界。在C#环境中,可以借助Redis的SETNX原子命令和Lua脚本实现带超时的锁,也可以利用关系数据库的行锁或应用程序锁构建锁表。两种方案在可靠性、性能、复杂度上存在明显差异,Redis方案延迟低、吞吐高,适合高并发场景;数据库方案强一致、实现简单,适合已有数据库且并发量可控的系统。本文结合C#代码演示加锁、续期、释放的完整流程,并分析误删锁、锁过期、主从切换等常见坑点与应对策略。

分布式锁解决的核心问题是:当多个服务实例或进程同时运行并访问同一份资源时,如何保证同一时刻只有一个执行单元能进入临界区。单机环境中常用的 lockMonitor 都依赖当前进程内的线程同步原语,一旦应用部署到多台服务器或容器中,这些机制就完全失效。C# 开发者需要借助独立于应用进程的外部存储来协调锁状态,其中 Redis 和关系型数据库是两种最主流的实现载体。

C#中如何用Redis和数据库实现可靠的分布式锁?

一、分布式锁的基本要求

无论采用哪种存储实现,一个合格的分布式锁都应该满足几个硬性条件。第一是互斥性,即任意时刻只有一个客户端能持有锁。第二是避免死锁,如果持有锁的进程崩溃,锁必须能自动过期或通过其他方式释放,不能永久阻塞其他请求。第三是锁标识,每个客户端在加锁时应写入唯一令牌,释放时校验令牌,防止把别的客户端持有的锁误删。第四是容错性,存储节点不能成为单点故障,否则锁服务不可用会拖垮业务。

除了这些基础要求,实际生产环境还会关注可重入、自动续期、高可用等问题。可重入锁允许同一个客户端在已经持有锁的情况下再次获取同一把锁,但这在分布式环境中需要额外记录持有者信息和重入次数。自动续期则用于处理业务执行时间超过锁过期时间的场景,避免锁提前失效导致并发进入。C# 中并没有内置的分布式锁原语,通常需要组合 Redis 命令或数据库事务来实现上述能力。

二、基于Redis的C#分布式锁实现

Redis 之所以适合做分布式锁,是因为它的单线程命令执行模型能够保证单个命令的原子性。早期方案使用 SETNX 设置锁,再通过 EXPIRE 单独设置过期时间,但这两个操作不是原子的,如果在两步之间进程崩溃,就会留下一个没有过期时间的锁。从 Redis 2.6.12 开始,官方推荐使用 SET key value NX PX milliseconds 一条命令完成加锁和设置过期时间。C# 可以通过 StackExchange.Redis 的 StringSet 方法配合 When.NotExists 参数实现。

下面的代码封装了一个简单的 Redis 锁,加锁时写入一个唯一的 token,过期时间由业务指定。

using StackExchange.Redis;

public class RedisLock
{
    private readonly ConnectionMultiplexer _redis;
    private readonly IDatabase _db;

    public RedisLock(string connectionString)
    {
        _redis = ConnectionMultiplexer.Connect(connectionString);
        _db = _redis.GetDatabase();
    }

    public bool TryAcquire(string key, string token, TimeSpan expiry)
    {
        return _db.StringSet(key, token, expiry, When.NotExists);
    }

    public void Release(string key, string token)
    {
        const string script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end";
        _db.ScriptEvaluate(script, new RedisKey[] { key }, new RedisValue[] { token });
    }
}

释放锁时不能直接执行 DEL,否则某个客户端业务超时后锁已经过期,它再删除可能会把其他客户端刚获取的锁删掉。正确做法是使用 Lua 脚本,先读取键的值并与自己的 token 比较,一致才删除。上面的 ScriptEvaluate 就是通过 Redis 的 Lua 脚本保证比较和删除的原子性。

在实际使用中,如果业务逻辑执行时间不确定,还需要考虑锁续期。可以启动一个后台定时器,在锁过期时间的前三分之一周期内刷新过期时间,但刷新时同样要校验 token 是否仍属于自己。续期失败通常意味着锁已经丢失,此时应该中断业务或进行回滚。另一个常见问题是 Redis 主从切换导致锁丢失:如果锁写到主节点,主节点宕机后还没同步到从节点,从节点升级后可能会让新的客户端获得同一把锁。对于一致性要求极高的场景,需要引入 RedLock 或使用带持久化策略的 Redis 集群。

三、基于关系数据库的C#分布式锁实现

数据库实现分布式锁的核心思想是利用数据库自身的事务和约束机制。比较简单的做法是创建一张锁表,锁名称作为主键或唯一键。加锁时向表中插入一条记录,只有插入成功的客户端才获得锁;释放锁时删除该记录。由于主键唯一约束能够阻止重复插入,因此保证互斥性。这种方案的优势是几乎不需要额外组件,已有 MySQL 或 SQL Server 的系统可以直接落地。

CREATE TABLE distributed_lock (
    lock_name VARCHAR(128) PRIMARY KEY,
    owner_token VARCHAR(64) NOT NULL,
    expire_time DATETIME NOT NULL
);

加锁逻辑可以先用 INSERT 尝试插入,如果唯一约束冲突则说明锁已被占用。但是插入的锁记录如果没有过期时间,进程崩溃后记录会一直存在,造成死锁。解决方式是在表中增加 expire_time 字段,释放锁时先删除已过期的记录。更稳妥的办法是为锁记录增加定时清理任务,或者在代码中插入前先尝试删除过期数据。不过数据库锁的性能通常低于 Redis,因为每次加锁和释放都会产生磁盘 I/O 和事务日志。

SQL Server 还提供了更轻量的应用程序锁 sp_getapplock,它把锁状态维护在数据库会话或事务级别,不需要创建物理表。C# 中可以通过 SqlCommand 调用该存储过程。

using System.Data.SqlClient;

public class SqlServerLock
{
    public static bool TryAcquire(string connectionString, string resource)
    {
        using var conn = new SqlConnection(connectionString);
        conn.Open();
        using var cmd = new SqlCommand("sp_getapplock", conn)
        {
            CommandType = System.Data.CommandType.StoredProcedure
        };
        cmd.Parameters.AddWithValue("@Resource", resource);
        cmd.Parameters.AddWithValue("@LockMode", "Exclusive");
        cmd.Parameters.AddWithValue("@LockOwner", "Session");
        cmd.Parameters.AddWithValue("@LockTimeout", 5000);
        var result = cmd.ExecuteScalar();
        return Convert.ToInt32(result) == 0;
    }
}

上面的代码以会话为锁持有者,调用 sp_getapplock 后如果返回 0 表示成功获得锁。释放锁时可调用 sp_releaseapplock,参数与加锁时保持一致。使用数据库锁的优点是与业务事务容易结合,例如在同一会话中既获取资源锁又执行数据更新,可以保证资源访问和数据操作的一致性。缺点是锁的超时粒度受数据库参数控制,难以像 Redis 一样做到毫秒级精细控制,并且高并发下数据库连接数容易成为瓶颈。

四、Redis与数据库方案对比和选型建议

从性能角度看,Redis 基于内存操作,单次加锁延迟通常在亚毫秒级,适合高并发、低延迟的互联网业务。数据库方案需要经过 SQL 解析、事务日志和磁盘写入,单次操作耗时通常在几毫秒到几十毫秒,适合并发量不高但要求强一致或已有数据库资产的企业应用。下面的表格列出了几个关键维度的对比。

对比维度Redis 分布式锁数据库分布式锁
性能与延迟高,内存级读写较低,受磁盘和事务影响
一致性主从切换可能丢锁依赖事务,通常更强
实现复杂度中等,需处理续期和误删低,表或存储过程即可
运维成本需要维护 Redis 集群复用现有数据库
适用场景高并发、超时敏感并发可控、强一致优先

如果项目已经部署 Redis 并且没有严格的主从一致性要求,优先选择 Redis 锁,配合唯一令牌和 Lua 释放脚本能覆盖大部分业务场景。如果业务操作本身与数据库事务强相关,例如订单状态流转、库存扣减,使用数据库锁或直接依赖数据库行锁往往更自然,可以减少分布式协调的复杂度。

五、C#实现分布式锁的常见坑与优化建议

第一个坑是锁误删。无论是 Redis 还是数据库,释放锁时必须校验持有者身份。Redis 方案中应使用 Lua 脚本比较 token 后再删除;数据库方案中应按 owner_token 删除记录,而不是按锁名称无条件删除。

第二个坑是锁过期与业务超时。如果锁的过期时间设置得过短,而业务执行被 GC、网络抖动或慢查询拖住,锁可能在业务完成前失效,导致其他实例进入临界区。解决方案是将过期时间设置得尽量宽松,并为长时间任务实现续期线程。Redis 的 Redisson 等客户端已经提供了看门狗机制,C# 生态中虽然缺少完全等价的成熟组件,但可以通过定时刷新 token 自行实现。

第三个坑是错误使用锁粒度。锁的粒度应该尽量小,只锁定真正需要互斥的资源,例如对某个订单号加锁而不是对整个订单表加锁。锁粒度越小,并发吞吐越高,阻塞时间越短。C# 中可以将资源 ID 拼接为锁键,比如 lock:order:123456,避免所有请求争抢同一个全局锁。

最后还要关注分布式锁的监控和告警。统计锁的等待时间、持有时间、失败次数等指标,可以帮助发现锁竞争热点和异常长时间的临界区。如果频繁出现锁等待超时,应该考虑拆分粒度、优化业务逻辑或调整锁方案。

C#分布式锁Redis分布式锁数据库分布式锁修改时间:2026-08-28 15:00:19

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。