在分布式系统中,分布式锁其实并不是完全安全的,特别是支持GC的中间件或网络延迟,都会使共享资源不在安全,而fencing token就是避免这一现象的并发控制机制。
文章目录
- 前言
- 🧠一、分布式锁的局限性
- [💡二、fencing token](#💡二、fencing token)
-
- [2.1 fencing token的诞生](#2.1 fencing token的诞生)
- [2.2 fencing token的工作机制](#2.2 fencing token的工作机制)
- [2.3 fencing token的设计理念](#2.3 fencing token的设计理念)
- [🤔三、fencing token的实现](#🤔三、fencing token的实现)
- 总结
前言
在分布式系统中,多个节点同时访问共享资源时,协调成了至关重要的问题。分布式锁作为解决这一问题的关键工具,却存在一个致命的缺陷:它可能在某些情况下失效,导致多个客户端同时操作共享资源,从而造成数据损坏。
fencing token(防护令牌)机制由分布式系统专家Martin Kleppmann提出,用以增强分布式锁的安全性。
🧠一、分布式锁的局限性
在讨论fencing token之前,必须先理解分布式锁存在的问题。分布式锁通常基于Redis、ZooKeeper等系统实现,通过互斥机制确保同一时间只有一个客户端可以访问共享资源。
然而在实际环境中,网络延迟、进程暂停(如GC停顿) 等情况难以避免。考虑以下场景:
- Client 1获取锁后开始操作共享资源。
- 由于GC停顿或网络延迟,Client 1的锁过期。
- Client 2获取锁并开始操作共享资源。
- Client 1从停顿中恢复,继续操作共享资源。
- 此时两个客户端同时操作共享资源,数据可能被损坏。
这种情况即使在使用了看似可靠的分布式锁方案(如Redis Red Lock)时也可能发生。
💡二、fencing token
2.1 fencing token的诞生
Martin Kleppmann在《How to do distributed locking》一文中详细分析了分布式锁的局限性,并提出了fencing token方案。
fencing token的核心思想很简单:在获取锁的同时,获取一个单调递增的token (令牌),每次访问共享资源时都必须出示这个token。
共享资源端会记录处理过的最大的token值,如果接收到一个更小的token,请求就会被拒绝。这样就建立了一种序列机制,确保了操作的顺序性
2.2 fencing token的工作机制
fencing token机制涉及三个核心组件:
- 锁服务 (Lock Service):负责分配锁和fencing token的服务。它需要保证token的单调递增性,即使在多个节点上也不能重复。
- 客户端 (Client):访问共享资源的应用程序。在访问资源前必须从锁服务获取锁和fencing token。
- 资源服务 (Storage) 提供实际服务的资源存储层。需要验证token的有效性。
下面给出fencing token的工作流程:
- 步骤 1:申请资源时获取令牌
客户端需要操作共享资源前,先向协调者(如分布式锁服务、数据库、ZooKeeper 等)申请资源,并同时获取一个 Fencing Token。 - 步骤 2:执行操作时携带令牌
客户端执行资源操作(如写入数据库、修改缓存)时,必须将令牌作为操作的一部分传入(通常通过 SQL 条件、API 参数等方式)。 - 步骤 3:资源端验证令牌有效性
共享资源在执行操作前,会先验证客户端携带的令牌是否为当前针对该资源的 "最新令牌"。
2.3 fencing token的设计理念
fencing token的设计建立在几个核心原则之上:
令牌的单调递增性(如基于数据库自增 ID、Redis 的 INCR 命令等):
fencing token必须是单调递增的,这是机制能够工作的数学基础。这种设计保证了时间的偏序关系,即使物理时间可能不同步,操作序列也能保持正确。
服务端验证:
资源服务端必须维护已处理的最大token值 ,并拒绝所有携带小于等于该值的token的请求。这一验证过程是保证安全性的关键。
最小权限原则:
fencing token只解决授权问题,不涉及身份认证。这种单一职责设计使得系统更加简洁。
尽管fencing token概念上很简单,但在实际系统中实现却面临几个挑战:
- token的持久化与一致性
资源服务需要持久化存储当前最大的token值,以防止系统重启后数据丢失。在分布式资源服务中,这一状态需要在多个副本间同步,保证一致性。 - 性能考量
token验证引入了额外的开销。在设计时需要权衡安全性与性能的关系,可能需要在关键操作上才使用fencing机制。 - 与现有系统的集成
许多现有系统并不原生支持fencing token机制。集成时可能需要修改客户端或服务端代码,这可能会很复杂。
到这里相信很多人都有疑问,这不就是乐观锁吗?
是的,两者的设计思想极其相似,两者都用于并发控制,但设计思路和适用场景不同,对比如下:
维度 | Fencing Token | 乐观锁(如版本号) |
---|---|---|
核心逻辑 | 基于 "递增令牌的唯一性",验证是否为最新令牌 | 基于 "版本号比对",验证资源是否被修改过 |
适用场景 | 分布式系统(跨节点、跨服务的资源竞争) | 单机数据库(同一节点内的行级并发控制) |
令牌生成依赖 | 需独立协调者(如锁服务、Redis) | 依赖数据库自身的版本字段(如 version) |
解决的核心问题 | 防止 "僵尸客户端" 的无效操作 | 防止 "并发写入覆盖"(如多人同时编辑文档) |
性能 | 验证逻辑简单(整数比较),性能极高 | 需比对版本字段,性能略逊于令牌验证 |
🤔三、fencing token的实现
下面是一个表格汇总了 fencing token 在一些常见系统/库中的实现概况:
系统/库名称 | 实现形式 | 关键特性 | 适用场景 |
---|---|---|---|
Redisson | FencedLock | 基于 fencing token 理论,提供单调递增的 token。 | Redis 为基础的分布式环境 |
Apache ZooKeeper | 序列号 (zxid) 或自定义令牌 | 利用其强一致性和顺序节点特性生成单调递增的标识,可自行实现 token 验证。 | 需要强一致性和高可靠性的分布式系统 |
etcd | 修订号 (Revision) 或租约ID | 提供全局单调递增的修订号,可用作 fencing token。 | Kubernetes、需要分布式锁的场景 |
⚙️ 实践中的考量
尽管 fencing token 显著提升了分布式锁的安全性,但在实际应用中仍需注意以下几点:
- fencing token 主要解决的是"持有过期锁的客户端误操作"的问题,但它并不能保证分布式锁在所有场景下的百分百安全,也无法解决锁服务本身不可用(Liveness 属性)的问题。分布式系统的复杂性决定了没有完美的方案。
- Token 状态的持久化与同步:在集群化部署的资源服务中,如何保证所有节点看到的"当前最大 token"是一致的,或者如何将 token 状态在不同节点间高效同步,是一个需要仔细设计的问题,否则可能引入新的一致性漏洞。
- 性能权衡:生成全局单调递增的 token 以及每次操作都进行验证,必然会引入额外的开销。需要在安全性和性能之间做出权衡。对于性能极其敏感但可以容忍极小概率锁失效的场景,可能会选择不同的策略。
fencing token 是构建可靠分布式系统的一个重要工具。如果你正在设计一个对数据一致性要求非常高的分布式系统(例如涉及金融交易、关键配置更新),那么强烈建议选择或实现一个提供了 fencing token 机制的分布式锁,并确保你的资源服务能够正确地进行 token 验证。
总结
Fencing Token 是分布式系统中解决 "并发冲突" 和 "僵尸客户端" 问题的轻量级且高效的机制。其核心优势在于:
- 通过 "递增令牌" 确保操作的唯一性和有效性;
- 验证逻辑简单,无状态,易于扩展;
- 可与分布式锁、数据库等组件无缝结合,提升系统的一致性和可靠性。
在设计分布式资源竞争场景时,Fencing Token 通常是比单纯 "超时" 或 "乐观锁" 更优的选择。