分布式锁定
1)为什么(以及何时)需要分布式锁定
分布式锁定是一种机制,可确保群集多个节点之间临界部分的互操作性。典型任务:- 背景任务/sheduler的领导力(领导力)。
- 将单一执行者限制为共享资源(移动文件、迁移模式、独家支付步骤)。
- 如果无法实现等效性/排序,则顺序处理单元(wallet/order)。
- 如果可以进行等效的upsert,CAS(匹配和设置)或按键排队(按键排列)。
- 如果资源允许交换操作(CRDT,计数器)。
- 如果问题由单个存储中的事务解决。
2)威胁模型和属性
故障和复杂性:- 网络:延迟、分离(分区)、数据包丢失。
- 过程:GC暂停,停止世界,锁定后崩溃。
- 时间:时钟漂移和位移打破TTL方法。
- 重新拥有:网络后的"僵尸"过程可能认为它仍然拥有城堡。
- 安全:不超过一个有效所有者(安全)。
- 活着:城堡在主人失灵(liveness)时被释放。
- 正义:没有饥饿。
- 时钟独立性:正确性独立于时钟(或由时钟调整来补偿)。
3)主要型号
3.1 Lease(租赁锁)
TTL发出了锁。业主有义务将其延长至到期(心跳/保持活力)。
优点:在裂缝中自负。
风险:如果所有者"卷曲"并继续工作,但失去了延期,则可能会出现双重所有权。
3.2 Fencing token(护栏令牌)
每次成功捕获时,都会发出单调增长的数字。资源用户(DB,队列,文件存储)检查令牌并拒绝使用旧编号的操作。
这在TTL/租赁和网络拆分中至关重要-可以防止"旧"所有者。
3.3 Quorum锁(CP系统)
使用分布式共识(Raft/Paxos;etcd/ZooKeeper/Consul),记录与共识日志相关联→在大多数节点上没有分裂中断。
另外:强大的安全保障。
减:对法定人数的敏感性(如果失去法定人数,其生存能力将减弱)。
3.4个AP锁(内存/缓存+复制)
例如,Redis群集。高可用性和速度,但没有严格的网络分离安全保证。需要在合成器侧面进行弯曲。
4)平台和模式
4.1 etcd/ZooKeeper/Consul(建议使用强锁)
短暂节点(ZK)或会话/leases(etcd):关键在会话现场时存在。
会期保持性;法定人数的损失→会议到期→锁被释放。
等候队列的序节点(ZK'EPHEMERAL_SEQUENTIAL) →公平性。
go cli, _:= clientv3. New(...)
lease, _:= cli. Grant(ctx, 10) // 10s lease sess, _:= concurrency. NewSession(cli, concurrency. WithLease(lease. ID))
m:= concurrency. NewMutex(sess, "/locks/orders/42")
if err:= m. Lock(ctx); err!= nil { / handle / }
defer m. Unlock(ctx)
4.2 Redis(整洁)
经典是"SET key value NX PX ttl"。
问题:- 复制/传送器可以允许同时拥有者。
- 多个实例的雷德洛克(Redlock)降低了风险,但无法消除;在网络不可靠的环境中有争议。
将Redis应用为快速协调层更安全,但始终在目标资源中补充加密令牌。
示例(Lua-unlock):lua
-- release only if value matches if redis. call("GET", KEYS[1]) == ARGV[1] then return redis. call("DEL", KEYS[1])
else return 0 end
4.3 DB锁
PostgreSQL咨询锁:Postgres群集(过程/会话)中的锁。
好吧,当所有关键部分都处于一个DB中时。
SQL:
sql
SELECT pg_try_advisory_lock(42); -- take
SELECT pg_advisory_unlock(42); -- let go
4.4个文件/云"锁"
S3/GCS+具有"If-Match"(ETag)条件的lock对象元数据→本质上是CAS。
适合备用/迁移。
5)安全锁设计
5.1所有者身份
存储"owner_id"(节点#process #pid#start_time)+unlock时用于对账的随机令牌。
重复解锁不应拆除别人的锁。
5.2 TTL和延期
TTL <T_fail_detect(故障检测时间)和≥ p99操作关键部分×库存。
扩展-定期(例如,每个"TTL/3"),具有截止日期。
5.3 Fencing token on shinka
改变外部资源的部分必须传递"fencing_token"。
Sink(DB/缓存/存储)存储"last_token"并拒绝较小的:sql
UPDATE wallet
SET balance = balance +:delta, last_token =:token
WHERE id =:id AND:token > last_token;
5.4队列等待和公平
在ZK-"EPHEMERAL_SEQUENTIAL"和观察者中:客户正在等待释放最近的前任。
在etcd中,是具有修订版/发行版的钥匙;根据"mod_revision"排序。
5.5分裂大脑的行为
CP方法:没有法定人数,就无法锁定-停机时间比打破安全性更好。
AP方法:允许在分离的岛屿上取得进展→需要进行调整。
6)领导力(领导力)
在etcd/ZK中-"领导者"是独家的epemer键;其余的订阅更改。
领导者写心跳;损失-连任。
所有领导者操作都伴随着fencing token(时代/修订号)。
7)错误及其处理
客户拿起城堡,但在→规范之前没有人会受到伤害。TTL/会议将腾空。
城堡在工作的中间到期:- 看门狗是强制性的:如果延期失败,则中断关键部分并回滚/补偿。
- 没有"下车":没有锁,关键部分就无法继续。
一个漫长的停顿(GC/stop-the-world)→扩展没有发生,另一个接管了城堡。工作流必须检测所有权的损失(保持通道)并中断。
8)Dedlocks,优先事项和倒置
在分布式世界中,Dedlocks很少见(锁通常是一个锁定),但是如果锁有几个锁定,则坚持单个取回顺序(锁定命令)。
优先级倒置:低优先级所有者在高优先级等待时保留资源。解决方案:TTL限制,提前(如果业务允许),共享资源。
禁食:使用等待队列(ZK子顺序节点)实现公平。
9)可观察性
度量标准:- `lock_acquire_total{status=ok|timeout|error}`
- `lock_hold_seconds{p50,p95,p99}`
- "fencing_token_valu"(单调)
- `lease_renew_fail_total`
- 'split_brain_prevented_total'(由于缺乏法定人数而拒绝了多少次尝试)
- `preemptions_total`, `wait_queue_len`
- `lock_name`, `owner_id`, `token`, `ttl`, `attempt`, `wait_time_ms`, `path` (для ZK), `mod_revision` (etcd).
- 带有结果的"acquire → critical section → release"。
- "lease_renew_fail_total"的增长。
- `lock_hold_seconds{p99}` > SLO.
- "Orfen"锁(没有心跳)。
- 等待队列膨胀。
10)实例
10.1安全的Redis锁定与fencing(伪)
1.将令牌计数器存储在可靠的堆栈中(例如Postgres/etcd)。
2.成功的"SET NX PX"会读取/嵌入令牌,并且所有资源更改都将通过DB/服务中的令牌验证来完成。
python acquire token = db. next_token ("locks/orders/42") # monotone ok = redis. set("locks:orders:42", owner, nx=True, px=ttl_ms)
if not ok:
raise Busy()
critical op guarded by token db. exec("UPDATE orders SET... WHERE id=:id AND:token > last_token",...)
release (compare owner)
10.2 etcd Mutex + watchdog (Go)
go ctx, cancel:= context. WithCancel(context. Background())
sess, _:= concurrency. NewSession(cli, concurrency. WithTTL(10))
m:= concurrency. NewMutex(sess, "/locks/job/cleanup")
if err:= m. Lock(ctx); err!= nil { /... / }
// Watchdog go func() {
<-sess. Done ()//loss of session/quorum cancel ()//stop working
}()
doCritical (ctx )//must respond to ctx. Done()
_ = m. Unlock(context. Background())
_ = sess. Close()
10.3 ZK领导(Java, Curator)
java
LeaderSelector selector = new LeaderSelector(client, "/leaders/cron", listener);
selector. autoRequeue();
selector. start(); // listener. enterLeadership() с try-finally и heartbeat
10.4 Postgres advisory lock with dedline (SQL+app)
sql
SELECT pg_try_advisory_lock(128765); -- attempt without blocking
-- if false --> return via backoff + jitter
11)花花公子(Game Days)
失去法定人数:禁用1-2个etcd节点→尝试取锁不应通过。
GC暂停/停止世界:人为地延迟所有者的流动,→检查看门狗正在中断工作。
Split-brain:模彷城堡所有者和看门狗之间的网络鸿沟→新主人获得更高的折腾,旧的-被合成器拒绝。
Clock skew/Drift:从所有者那里移走手表(对于Redis/lease) →确保通过令牌/检查确保正确性。
发布前崩溃:TTL/会话释放 →锁的过程下降。
12)反模式
干净的TTL锁,当访问外部资源时,不带扣。
依靠本地时间来正确无误(没有HLC/fencing)。
通过一个Redis向导在带有操纵器的环境中分发锁,而无需确认副本。
无限关键部分(TTL"世纪")。
在没有"owner_id"/token对账的情况下拆除"外星人"锁。
缺少backoff+Jitter →尝试的"风暴"。
一站式全球锁-一袋冲突;在钥匙上缝合更好。
13)实施支票
- 定义了资源的类型以及是否可以通过CAS/队列/幂等来实现。
- 选择了一种机制:etcd/ZK/Consul for CP;Redis/缓存-仅带调试。
- 实现:"owner_id",TTL+扩展,看门狗,正确解锁。
- 外部资源会检查fencing token(单调性)。
- 有领导和失败的策略。
- 设置指标、异常值、令牌和修订版本。
- 在acquire上提供backoff+jitter和taymout。
- 举行比赛日:法定人数,分裂大脑,GC暂停,时钟跳跃。
- 关于采取多个锁的顺序的文件(如果需要)。
- 退化计划(brownout):在无法进入城堡的情况下该怎么办。
14) FAQ
Q: Redis锁定是否足够"SET NX PX"?
A:只有当资源检查了fencing token时。否则,在网络共享中,两个所有者是可能的。
Q: 默认情况下选择什么?
答:对于严格的保证是etcd/ZooKeeper/Consul (CP)。对于一个DB内的轻量级任务-advisory locks Postgres。Redis只是一个骗局。
问:什么是TTL上市?
答:"TTL ≥ p99的关键部分持续时间× 2",并且足够短,可以快速清理"僵尸"。扩展-每个"TTL/3"。
问:如何避免饥饿?
A:顺序等待队列(ZK序列)或公平算法;尝试限制和公平规划。
Q: 是否需要时间同步?
答:要正确无误,请使用fencing。对于可操作的可预测性-是的(NTP/PTP),但不要依赖锁逻辑中的墙时钟。
15)结果
强大的分布式锁定是在具有lease+keepalive的法定支架(etcd/ZK/Consul)上构建的,并且必须通过在可变资源级别上的锁定令牌来补充。任何没有围栏的TTL/Redis方法都是破裂的风险。首先考虑低成本和偶然性,在没有低成本的情况下使用锁定,测量,测试故障模式-并且"关键部分"仅在意义上而不是事件数量上仍然至关重要。