redis用locktake/lockrelease实现分布式文件锁最轻量,需带唯一id、设过期时间、释放时校验id;zookeeper不适合作文件锁;数据库和文件系统锁跨服务器无效;常见翻车点是未处理抢锁失败、异常导致未释放、多线程共用db实例。

Redis 实现分布式文件锁的最小可行写法
直接用 StackExchange.Redis 的 LockTake + LockRelease 是最轻量、最可控的方式,别一上来就封装成“分布式锁服务”。核心是:锁必须带唯一租约 ID、必须设过期时间、释放时必须校验 ID 防止误删。
-
LockTake成功才代表抢到锁,失败要退避重试(比如指数退避),不能直接抛异常 - 锁 key 建议按业务粒度设计,例如
file:lock:/app/data/report_202410.csv,避免用泛化 key 如global_file_lock - 过期时间必须明显长于最大处理耗时(比如处理最多 30 秒,设 90 秒),否则锁自动失效会导致并发写入
- 释放锁必须用
LockRelease,且传入当初LockTake返回的lockId;自己用DEL或EVAL脚本删 key 极易出错
var db = redis.GetDatabase();
var lockKey = "file:lock:/data/config.json";
var lockId = Guid.NewGuid().ToString();
if (db.LockTake(lockKey, lockId, TimeSpan.FromSeconds(90)))
{
try
{
// 执行文件读写操作
}
finally
{
db.LockRelease(lockKey, lockId);
}
}
ZooKeeper 不适合做“文件锁”,但可以做协调节点
ZooKeeper 本身没有原生锁语义,Curator 的 InterProcessMutex 是模拟出来的,对文件场景反而太重。它真正适合的是服务发现、主节点选举这类强一致性协调,而不是高频、短时、大量文件路径的锁竞争。
- 每个文件锁都对应一个临时顺序节点(如
/locks/file_a_0000000001),ZK 节点创建开销大,QPS 上不去 - 网络抖动时会频繁触发 watch 回调和重连,容易出现锁状态不一致(比如客户端以为锁还在,其实 session 已丢)
- 如果你已经在用 ZooKeeper 做服务注册,可以复用它做“锁注册中心”——只存锁持有者 IP+PID+文件路径,由业务自己轮询或监听变更,不依赖 ZK 的锁原语
为什么不用数据库行锁或文件系统级锁
跨服务器场景下,SELECT ... FOR UPDATE 只在单库有效,分库后完全失效;而本地 FileStream 的 FileShare.None 根本不跨进程,更别说跨机器。
- SQL Server 的
sp_getapplock理论上能跨实例,但依赖链接字符串指向同一数据库,实际部署中往往做不到 - Windows 文件共享(SMB)的 byte-range 锁在高并发下极易死锁,且 Linux 客户端兼容性差
- 所有基于本地资源的方案,在容器或云环境(如 Kubernetes Pod 重启、挂载卷漂移)下都会丢失锁状态
Redis 锁的三个真实翻车点
不是锁不住,而是锁住了却没起作用——问题全出在边界逻辑上。
- 没处理
LockTake返回false的情况,代码继续往下走,等于没锁 - 文件操作里抛了未捕获异常,
finally没执行,LockRelease永远不被调用,锁一直挂着直到超时(可能几小时) - 多个线程共用同一个
IDatabase实例去调LockTake,而 StackExchange.Redis 的锁命令不是原子的——必须确保每次调用都走同一个 connection multiplexer 和 database 实例
跨服务器文件锁本质是“协商而非强制”,Redis 提供的是协调基础设施,真正的互斥靠的是你是否严格遵循租约模型。漏掉一次 LockRelease 或绕过一次 LockTake,整个锁机制就形同虚设。










