news 2026/9/7 9:11:09

etcd contrib/lock Fencing Token 示例:复现租约过期导致的分布式锁失效与版本验证方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
etcd contrib/lock Fencing Token 示例:复现租约过期导致的分布式锁失效与版本验证方案

etcd contrib/lock Fencing Token 示例:复现租约过期导致的分布式锁失效与版本验证方案

【免费下载链接】etcdDistributed reliable key-value store for the most critical data of a distributed system项目地址: https://gitcode.com/GitHub_Trending/et/etcd

etcd 仓库在 contrib/lock 提供了一个可直接运行的示例程序,完整复现了“基于租约(lease)的分布式锁无法真正保证互斥”这一经典问题,并演示了如何通过 Fencing Token(版本号/围栏令牌)技术让共享存储拒绝持有过期租约的客户端写入。阅读本文后,你将掌握:如何用etcdctl lease revoke手工制造“假过期”、concurrency包中 Session/Mutex 的租约机制如何工作,以及如何在共享存储侧用版本号校验杜绝旧持锁者的非法访问。

1. 问题背景:为什么租约锁不能提供真正的互斥

一般来说,基于租约的锁服务无法为进程提供互斥性。原因是这类租约机制依赖锁服务与客户端双方的物理时钟:语言运行时的 Stop-the-world GC 停顿、网络延迟、宿主机时钟漂移等诸多因素,都可能导致一个仍然存活的客户端进程被授予的租约在服务端被判定为已过期。结果是:进程 A 以为自己还持有锁,而进程 B 此时可以拿到同一把锁——两个“持锁者”同时操作共享资源。

etcd 官方的 why lock and lease 相关说明 中也指出,这类问题可以用version number validation(版本号验证)fencing tokens(围栏令牌)技术解决:由共享资源侧(图中的存储)对每个请求携带的令牌做校验,拒绝版本落后于当前版本的写入请求。

contrib/lock目录正是把上述思路做成了可执行的演示:

  • storage:一个极简的内存 KV 存储,通过 HTTP + 自定义 JSON 协议对外提供服务;
  • client:模拟“Client 1 / Client 2”两个客户端进程,在 etcd 锁的协调下向storage写入键值,并在请求中携带自己的“版本号”。

目录结构与两个程序如下:

文件角色说明
contrib/lock/client/client.go客户端连接 etcd、通过concurrency包加锁,持租约版本调用 storage 写入
contrib/lock/storage/storage.go共享存储监听:8080,内存 map 存储,写操作前做版本比对

2. 示例的通信协议:storage 的 JSON 接口

storage进程是一个 HTTP 服务(http.ListenAndServe(":8080"),见 storage.go 第 104-111 行),所有请求以 JSON 形式 POST 到根路径。clientstorage共用同一组协议结构体:

type request struct { Op string `json:"op"` // "read" 或 "write" Key string `json:"key"` // 键 Val string `json:"val"` // 值 Version int64 `json:"version"` // 客户端携带的 fencing 版本(etcd 租约 ID) } type response struct { Val string `json:"val"` Version int64 `json:"version"` Err string `json:"err"` }

存储内部为每个键保存{val, version}两个字段(storage.go 第 26-31 行)。核心校验逻辑在handler的 write 分支中(storage.go 第 79-91 行):

} else if strings.Compare(req.Op, "write") == 0 { if val, ok := data[req.Key]; ok { if req.Version != val.version { // 版本不匹配:拒绝写入,返回 fencing 错误 writeResponse(response{"", -1, fmt.Sprintf( "given version (%x) is different from the existing version (%x)", req.Version, val.version)}, w) } else { // 版本一致:更新值与版本 data[req.Key].val = req.Val data[req.Key].version = req.Version ... } } else { // 键不存在:直接写入 data[req.Key] = &value{req.Val, req.Version} ... } }

这就是 Fencing Token 的落点:共享存储只信任“版本号 >= 当前记录”的写者。旧租约持有者再来写时,其版本与存储中的最新版本不同,请求被直接拒绝。

3. 构建与运行(完整复现步骤)

以下内容完整继承自 contrib/lock/README.md,并补充了源码层面的说明。

3.1 构建

clientstorage分别在其目录下执行go build

$ cd contrib/lock/client && go build $ cd contrib/lock/storage && go build

需要注意的一点:contrib/lock目录下没有独立的go.mod,而 client.go 依赖go.etcd.io/etcd/client/v3go.etcd.io/etcd/client/v3/concurrency。从源码结构看,在模块解析上需要本地环境能导出这两个包(例如通过 workspace 或 replace 指到本仓库的client/模块),否则直接构建可能找不到依赖。

3.2 启动 etcd(锁服务)

在 etcd 源码根目录下构建并启动 etcd,它在演示中承担“锁服务”的角色:

$ make # 构建 etcd $ bin/etcd # 启动 etcd

示例客户端默认连接127.0.0.1:2379等标准端口(client.go 第 99-104 行),因此用默认配置的bin/etcd即可。

3.3 启动 storage

storage目录运行:

$ ./storage

3.4 启动 Client 1 并制造“假过期”

$ ./client 1

client 1先创建 etcd 客户端与 session,然后阻塞等待人工操作,输出形如:

client 1 starts created etcd client and session acquired lock, version: 694d82254d5fa305 please manually revoke the lease using 'etcdctl lease revoke 694d82254d5fa305' or wait for it to expire, then start executing client 2 and hit any key...

此时可以用etcdctl验证租约确实被创建:

$ bin/etcdctl lease list found 1 leases 694d82254d5fa305

然后手动撤销租约——这等价于模拟 Client 1 进程发生了长时间 STW 停顿、其 KeepAlive 心跳中断导致服务端判定租约过期:

$ bin/etcdctl lease revoke 694d82254d5fa305 lease 694d82254d5fa305 revoked

注意:由于示例把 session TTL 设置为 1 秒(见 4.1 节),如果什么都不做,租约几秒后也会自然过期,效果相同。

3.5 启动 Client 2 写入成功

$ ./client 2 client 2 starts created etcd client and session acquired lock, version: 694d82254e18770a this is client 2, continuing

如果一切顺利,./client 2会很快结束,并成功把键写入storage。此时 storage 中key0记录的版本是694d82254e18770a

3.6 恢复 Client 1,观察 fencing 生效

回到./client 1的终端,按任意键恢复该进程。它会带着旧租约版本号再次尝试写入,输出如下:

resuming client 1 expected fail to write to storage with old lease version: error: given version (694d82254d5fa305) is different from the existing version (694d82254e18770a)

这正是 Fencing 的价值:Client 1 进程还“活着”,但它对共享存储的写入已被版本校验拒绝,从而避免了基于租约锁无法防止的“旧持锁者越权写入”。client程序对此做了明确的分支处理——mode 1 收到该错误属于预期行为(打印expected fail),而 mode 2 收到任何写错误则视为异常直接退出(client.go 第 136-144 行)。

4. 源码纵深:etcd 锁与租约的实现

4.1 client 侧:Session 与 Locker 的协作

client.go 第 99-125 行 展示了整个加锁流程:

client, err := clientv3.New(clientv3.Config{ Endpoints: []string{"http://127.0.0.1:2379", ...}, }) // 先做连接检查,否则 newSession 会无限挂起 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err = client.MemberList(ctx) session, err := concurrency.NewSession(client, concurrency.WithTTL(1)) locker := concurrency.NewLocker(session, "/lock") locker.Lock() defer locker.Unlock() version := session.Lease() // 租约 ID 即 fencing 版本号 log.Printf("acquired lock, version: %x", version)

几个关键点:

  • 连接预检:代码注释明确指出先调MemberList探测连通性,否则NewSession会在连不上 etcd 时无限挂起;
  • TTL=1 秒concurrency.WithTTL(1)让租约极短,便于演示“自然过期”;若不指定,session.go 中defaultSessionTTL为 60 秒;
  • 版本号就是租约 IDsession.Lease()返回的 lease ID(如694d82254d5fa305)被直接作为 fencing 版本随写请求下发给 storage。

4.2 Session 为什么能保证“活着就持有锁”

session.go 第 41-79 行 中,NewSession先用client.Grant申请一个租约,再启动client.KeepAlive并起一个 goroutine 消费 keepalive 通道:只要客户端进程存活且网络正常,租约会被持续续期;进程崩溃、连接断开或长时间停顿(GC STW)时,心跳中断,租约在服务端过期。Session.Done()通道在租约孤儿化/过期时关闭,供上层感知失效。

4.3 Mutex 如何基于租约实现排队加锁

NewLocker(session, "/lock")底层是 mutex.go 中的Mutex。其tryAcquire(mutex.go 第 111-132 行)用一次 Txn完成入队:

m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease()) // 队列键 = 前缀 + 租约ID cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0) put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease())) // 键绑定到租约 getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...) // 取最早入队者 resp, err := client.Txn(ctx).If(cmp).Then(put, getOwner).Else(get, getOwner).Commit()

要点是:

  1. 每个等待者的键为pfx + 租约ID,并以租约 ID 作为键名的一部分天然实现了按获得锁先后排队(lease ID 单调递增);
  2. 该键绑定了 session 的租约——进程死亡、租约过期时 etcd 自动删除该键,即自动“让位”给下一个等待者;
  3. Lock若发现最早入队者不是自己,则通过 key.go 的 waitDeletes 监听前面等待者的键被删除(即前一位持锁者释放或租约过期),再复核自己的键仍存在后取锁;期间若 session 已失效则返回ErrSessionExpired

这也解释了演示中的时序:Client 1 的租约被 revoke 后,其队列键/lock694d82254d5fa305被 etcd 自动删除,Client 2 随即成为最早等待者并取锁成功——整个过程无需任何显式的锁释放操作。

4.4 端到端流程小结

把上述拼起来,示例的完整因果链是:

  1. Client 1 以 TTL=1 的 session 加锁,获得租约...fa305(版本号);
  2. etcdctl lease revoke撤销租约 → etcd 自动删除 Client 1 的锁队列键(模拟“假过期”);
  3. Client 2 加锁成功,租约...770a,携版本694d82254e18770akey0到 storage,成功;
  4. Client 1 恢复运行,携旧版本694d82254d5fa305key0,被 storage 的版本比对拒绝(见 3.6 节错误输出)。

没有第 4 步的版本校验,Client 1 就能把 storage 的数据覆盖回旧值——这就是“租约锁本身无法互斥”的具体后果。

5. 适用前提与延伸

  • 运行前提:单节点 etcd 运行在默认端口2379(客户端默认连接127.0.0.1:2379/22379/32379),storage 独占8080端口;示例未启用 TLS 与认证。
  • 版本号的取舍:示例直接以 etcd 租约 ID 作为 fencing 版本,其单调性由 etcd 保证。实际系统中也可以用数据库行级版本号、存储自身的高水位 ID 等,原则是存储侧能拒绝“回退”的版本
  • 延伸阅读(本仓库内)
    • client/v3/concurrency 完整实现了基于 session 的MutexLockerElection,可替代本示例中的NewLocker用法;
    • tests/robustness 提供了在故障注入场景下验证分布式正确性的测试框架,思路与本示例的“制造过期”一脉相承;
    • etcdctl lease list/revoke等命令见 etcdctl。

contrib/lock用最少的代码把“分布式锁为什么不够、Fencing Token 如何兜底”这条分布式系统的核心论证链跑成了可观察的实验,配合上述源码路径阅读,可以作为理解 etcd 租约与并发原语的绝佳切入点。

【免费下载链接】etcdDistributed reliable key-value store for the most critical data of a distributed system项目地址: https://gitcode.com/GitHub_Trending/et/etcd

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 9:09:28

SpringBoot与微信小程序构建精密温室监控:从数据链路到物联网应用

做毕业设计选“精密温室监控小程序(SpringBoot微信小程序)”这个方向,很多人的第一反应是:这不就是做一个能在手机上查温度和湿度的小系统吗?真等动手写代码,才会发现它和图书管理、商城这类纯信息管理系统…

作者头像 李华
网站建设 2026/9/7 9:09:03

MT7663 USB WiFi驱动交叉编译实战:从makefile到固件部署

简介:面向嵌入式 Linux 平台开发者的 MT7663 USB 转 Wi-Fi 驱动源码包,已经在海思3531硬件平台上完成交叉编译验证,可直接生成大小约 3MB 的内核驱动模块,适合正在移植无线网卡驱动或者调试 Wi-Fi 相关功能的开发人员直接使用。该…

作者头像 李华
网站建设 2026/9/7 9:06:24

多目标跟踪实战指南:从SORT到ByteTrack的算法选型与调参心法

简介:面向计算机视觉入门与进阶学习者的多目标跟踪代码资源,采用VIBE前景检测与卡尔曼滤波组合方案,适合理解视频监控、自动驾驶中动态目标的检测与持续跟踪流程。压缩包共16个文件,以C编写,包含7个头文件和7个源文件&…

作者头像 李华
网站建设 2026/9/7 9:06:15

技术博客选题指南:从生活情感回归代码实践

这个标题属于生活情感类话题,无法与 CSDN 技术教程的定位匹配,也没有可落地的技术栈、代码、配置或排查场景。为了保证内容真实可靠,我不能编造故事或强行套用技术框架来写这篇“项目”,否则就成了虚构内容,对读者的实…

作者头像 李华
网站建设 2026/9/7 9:05:59

负荷模型仿真与参数辨识:电力系统稳定分析的基石

简介:这份资源面向电力系统仿真与负荷建模方向的工程师、研究人员及电气专业学生,聚焦三阶感应电机负荷模型的动态特性复现。模型基于x-y-0坐标系统构建,利用PSASP仿真获取实测数据,再通过粒子群算法完成参数辨识,并以…

作者头像 李华
网站建设 2026/9/7 9:05:48

baseflight-master详解:老飞控固件的Git分支、源码结构与编译烧录指南

简介:baseflight-master是一套面向无人机飞控系统二次开发的开源源码工程,主要服务从事飞控算法研究、嵌入式开发或希望自定义飞行特性的开发者。工程基于C语言编写,可在Keil中直接编辑与编译,内部按初始化代码、传感器融合、PID控…

作者头像 李华