<p>StackExchange.Redis 是 C# 生产环境最稳妥的分布式锁实现。必须用原子 SET key value EX seconds NX 加锁,lockValue 须全局唯一;释放锁必须用 Lua 脚本校验 value 后删除;需实现自动续期,推荐使用 RedLockNet 库。</p>

StackExchange.Redis 是 C# 生产环境最常用、最稳妥的分布式锁实现路径。别用 lock 或 Monitor,它们只在单进程内有效;也别手写 GET + DEL 释放逻辑——这是导致锁误删的头号原因。
加锁必须用原子 SET 命令,不能拆成 SETNX + EXPIRE
Redis 的 SET key value EX seconds NX 是唯一推荐的加锁方式。它把「设置值」「设置过期时间」「仅当 key 不存在时才设」三件事合为一个原子操作。
常见错误写法:db.StringSet(key, value, when: When.NotExists) 紧接着调 db.KeyExpire(key, TimeSpan.FromSeconds(30)) —— 这两步之间若进程崩溃或网络中断,key 就会变成永不过期的“死锁”。
正确做法(使用 LockTake):
bool acquired = await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromSeconds(30));
其中 lockValue 必须是全局唯一标识,比如 Guid.NewGuid().ToString(),绝不能用固定字符串或线程 ID。
释放锁必须用 Lua 脚本校验 value 一致性
直接 db.LockRelease(lockKey, lockValue) 看似方便,但底层仍是先 GET 再 DEL,存在竞态窗口:A 拿到锁后执行超时,锁自动过期;B 成功加锁;A 恢复并执行 DEL,结果删掉了 B 的锁。
必须用 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[] { lockValue });注意:不要自己拼接 SQL 式的 delete from db_lock where lock_str='xxx',数据库锁在高并发下性能差、易死锁,且没有天然的原子 compare-and-delete 语义。
锁续期(Keep Alive)不是可选项,而是超时场景下的刚需
业务逻辑执行时间不确定时,30 秒锁过期可能远不够。但又不能盲目设成几小时——万一节点宕机,锁长期不释放,整个系统就卡死。
解决方案是加一个后台心跳任务,在锁剩余生命周期 1/3 时尝试续期:
- 用
db.KeyTimeToLive(lockKey)获取剩余 TTL - 若小于 10 秒,且当前仍持有该锁(需再次校验 value),则调
db.KeyExpire(lockKey, TimeSpan.FromSeconds(30)) - 建议用
RedLockNet库,它内置了自动续期和多 Redis 实例容错能力,比裸用StackExchange.Redis更省心
手动实现续期时,务必避免在 finally 块里无条件续期——如果加锁失败,续期会污染其他节点的锁。
ZooKeeper 和数据库锁只适合特定场景,别为了“多样性”而引入复杂度
ZooKeeper 锁依赖临时顺序节点 + Watcher,适合强一致、低频、长连接场景(如配置中心选主),但运维成本高、.NET 客户端生态弱(Curator 无官方 .NET 版),普通业务没必要上。
数据库锁(如 insert into db_lock)看似简单,实际有严重隐患:
-
INSERT失败靠异常捕获,但不同数据库异常类型不统一(SQL Server 是SqlException,MySQL 是MySQLException) - 没事务隔离级别控制时,可能出现幻读导致多个实例同时认为自己抢到了锁
- 锁表本身成了性能瓶颈,尤其在分库分表后无法跨库协调
除非你已有一套稳定、低延迟、高可用的 Redis 集群,否则别轻易换方案。Redis 单节点在多数业务中已足够可靠;真要防止单点故障,优先考虑 Redis Sentinel 或 Cluster,而不是切 ZooKeeper。
真正容易被忽略的是锁粒度与业务语义的对齐。比如“初始化任务”不该锁 "init" 这种泛化 key,而应是 "init:config_v2.3.1";“扣减库存”不能只锁商品 ID,得带上租户或渠道维度,否则跨租户请求会互相阻塞。锁 key 不是越短越好,而是要精确表达“谁和谁不能并发”。


















