Rust 所有权模型与 分布式锁 的思考
先说结论:本地锁是语言问题,分布式锁是共识问题
前段时间把服务从单机改成多副本,原来一把 Mutex 能搞定的事情,突然就得请 Redis 出场了。我一开始还挺乐观,心想 Rust 的 MutexGuard 用起来这么舒服,分布式锁无非就是把 lock() 换成一次网络请求,再封装一个 guard,Drop 的时候释放,应该差不多吧。
结果发现差得还挺多。
Rust 的所有权模型管的是单进程内的资源生命周期,编译器能盯着你什么时候借、什么时候还、能不能跨线程。分布式锁管的是多个进程之间的租约和共识,网络会抖、进程会挂、GC 会停顿、时钟还不一定准。把这俩东西硬套在一起,不能说完全不行,但很容易写出一种“看起来像 RAII,实际上靠运气”的代码。
Rust 的 MutexGuard 让我误以为锁都能靠 Drop 收尾
本地锁的体验确实太顺了。写个 let guard = mutex.lock().unwrap();,后面该干嘛干嘛,作用域一结束,Drop 自动解锁。就算中间 panic 了,栈展开也会把 guard 带走,锁照样释放。编译器还会提醒你 std::sync::MutexGuard 不是 Send,别想着把它跨 await 持有,不然很容易把自己锁死。
这种确定性会让人产生一种错觉:锁这个东西,天生就应该跟一个 guard 对象绑定,拿到 guard 就是拿到锁,guard 没了锁就没了。
我一开始给分布式锁写的封装大概就是下面这个意思,伪代码:
struct DistLockGuard {
key: String,
token: String,
client: RedisClient,
}
impl Drop for DistLockGuard {
fn drop(&mut self) {
// 这里其实没法 await
let _ = self.client.unlock(&self.key, &self.token);
}
}
看起来挺像回事,问题是 Drop 是同步的,而释放锁通常是一次异步网络调用。你当然可以在 Drop 里 block_on,或者起一个后台任务去释放,但前者可能把 runtime 卡住,后者在进程退出、runtime 关闭的时候经常来不及跑。更别说网络超时了,Drop 里你等不等?等多久?失败了重不重试?
不出问题的话就没有问题了,出问题的时候,锁可能已经过期了,而你的释放请求还在路上。
给分布式锁套一层 Guard 之后,麻烦才开始
第一个坑是锁超时。本地锁你持有多久都行,除非你自己作死。分布式锁不行,你总得给锁设置一个过期时间,不然进程真挂了,这把锁就永久留在那里了。可一旦有了过期时间,业务执行时间超过租约怎么办?只能加看门狗续期。
续期这事儿听起来简单,实际写起来就是另一个状态机:后台任务定时续,续期失败要标记锁可能已经丢了,业务代码在关键写入前还得检查自己是不是还持有锁。你看,到了这一步,guard 已经不再是“我拿着锁”的证明了,它更像一个“我曾经拿到过锁,但现在不一定”的提醒。
第二个坑是释放失败。本地锁 Drop 一定执行,分布式锁释放可能因为网络分区失败。你这边以为释放了,Redis 那边租约还没到期,另一个节点也拿不到;或者更糟,另一个节点等租约到期后拿到了锁,而你这边因为网络恢复,又发了一个迟到的释放请求,把别人的锁给删了。所以释放的时候得比对 token,不能无脑 DEL。
第三个坑是“锁住了,但没完全锁住”。比如 A 拿到锁,业务跑到一半发生了长时间 GC 或者被调度走了,租约过期,B 拿到锁开始写,A 恢复后又往存储里写了一把。这时候光有互斥已经不够了,还得靠 fencing token:每次加锁生成一个单调递增的 token,存储端只接受 token 更大的写入,把旧租约的迟到请求挡掉。
所有权模型真正能借给分布式锁的,是“边界感”
我现在觉得,Rust 所有权模型对分布式锁最大的启发,不是“用 Drop 自动释放”,而是“把资源的获取和释放表达成一个明确的、不容易绕过去的边界”。
本地锁的边界是编译期确定的:guard 在哪个作用域里,生命周期到哪儿结束,编译器比你清楚。分布式锁的边界只能靠协议和代码约定来维护:锁的 key 是什么、租约多长、谁负责续期、释放失败怎么记录、业务写入要不要带 fencing token。这些都没法让编译器帮你检查,只能靠封装把该做的事情收拢到一个地方,别让业务代码随手 lock 一下又随手 unlock。
比如我现在更愿意把分布式锁写成显式的 acquire 和 release,中间的业务逻辑放在闭包里,或者至少让 guard 带一个 is_valid() 检查,而不是假装它跟 MutexGuard 一样可靠。释放也尽量显式做,Drop 只做兜底,而且兜底失败要打日志,不能静默吞掉。
所有权模型像是一个很强硬的房东,借出去的东西到点必须还,不还编译不过;分布式锁更像合租室友的口头约定,到点没回来,只能换锁,但换锁的时候原来那个人可能还在屋里翻东西。两者能互相借鉴,但真不能直接画等号。
最后一点碎碎念
Rust 写多了,确实会让人对“资源释放”这件事变得敏感,看到没有 guard 的锁就难受。但分布式系统里的锁,本来就不是一个纯语言层面能解决的问题。它更像是租约、超时、续期、token 和幂等凑出来的一套运行时协议。
所以现在的我,写本地锁就老老实实享受 MutexGuard 的确定性;写分布式锁就老老实实接受它的不确定性,该续期续期,该 fencing 就 fencing,别指望一个 Drop 就把所有问题都解决了。毕竟网络不会因为你的类型系统好看就少抖两下。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。