分布式系统中的故障不可避免:网络分区、超时和间歇性连接问题都可能导致故障。当这些故障发生时,可能会导致延迟、事务不完整或数据状态不一致,最终影响用户体验和系统可靠性。当系统出现故障时,客户端通常会重试请求,以确保操作能够成功完成。
然而,如果没有适当的处理机制,重试可能会导致意外后果,例如重复事务、数据损坏或状态不一致。在系统或API中实现幂等性,可以确保在这些故障之后的重试能够可靠地处理,从而维护系统的完整性和一致性。
什么是幂等性?
幂等性是一种操作特性,它保证无论执行多少次,结果都相同。换句话说,多次执行该操作不会改变首次成功执行后的结果。
举个例子:假设客户端通过调用服务来更新一笔交易,服务会更新其数据库以将交易标记为已完成。然而,在服务向客户端返回成功响应时,发生了临时的网络故障,导致客户端无法收到成功消息。因此,客户端不知道请求是否已成功处理,于是再次重试请求。
如果没有幂等性,第二次请求可能会再次更新交易,从而导致意外的副作用。而当实现了幂等性时,系统会识别第二次调用为重复请求,并确保数据库保持不变,防止重复更新。
何时需要系统具备幂等性?
是否需要幂等性取决于具体的应用场景,主要适用于写操作。
读操作无需进行幂等性检查,因为它们本身就具有幂等性。由于读操作不会改变系统状态,只要底层数据保持不变,多次执行读操作的结果将始终相同。
并非所有写操作都需要幂等性。判断是否需要幂等性的一个关键原则是,评估操作是否基于输入进行计算并改变系统状态。
例如,一个简单地将输入直接存储到数据库中的写操作无需幂等性,因为使用相同的输入重复调用只会覆盖数据,而不会导致不一致(例如,将一个数字设置为X)。然而,一个修改现有状态的写操作(例如,将一个数字增加X)则需要幂等性,以确保一致性。
实现幂等性的机制
在分布式系统中,实现幂等性有多种方法。以下是一些常用的技术:
1. 唯一键
实现幂等性的最常见方法是为每个请求分配唯一标识符。这种方法可以确保检测并适当处理重复请求。
请求处理的一般流程:
- 从请求中提取唯一标识符。
- 检查是否已经处理过具有相同标识符的请求。
- 如果没有(首次请求):处理请求,将结果连同唯一标识符一起持久化,并返回结果。
- 如果有(重复请求):检索之前存储的结果并返回,可选择性地表明请求已被处理。
如果请求中未提供唯一标识符,系统可以通过计算所有输入参数的哈希值并将其用作标识符来生成一个。但API文档必须明确说明这一方法,以确保客户端不在请求中包含可变字段,例如当前时间戳。否则,重试时可能会生成不同的标识符,从而失去幂等性的意义。
2. 乐观锁
乐观锁是一种在数据库和分布式系统中用于处理对同一数据的并发更新的并发控制机制,同时尽量减少冲突。它也可以用来实现幂等性,采用“先读后写”的方式。
请求处理的一般流程:
- 客户端从服务器读取一个值及其版本号,假设为x。
- 客户端发送更新请求,包含之前读取的版本号(x)。
- 服务器检查客户端请求中的版本号是否与数据库中的当前版本匹配。
- 如果匹配:服务器处理请求,并将版本号递增(x → x+1)。
- 如果不匹配:请求被拒绝。版本号不匹配意味着数据已被更新,可能是由于重复请求或其他系统对数据的修改。
- 如果请求被拒绝,客户端必须获取最新的值及其更新后的版本号,并根据新的版本和值决定是否重试。
这种方法确保重复或过时的请求不会覆盖最新数据,从而保持一致性并防止意外修改。
实现幂等性的挑战
虽然幂等性至关重要,但其实现可能会引入复杂性,包括:
- 状态管理:维护唯一键或版本号会增加系统的开销,并需要高效的存储和检索机制。
- 处理副作用:某些操作会触发副作用(例如发送邮件、触发通知)。确保这些操作不会重复发生需要额外的保障措施。
- 分布式系统中的数据一致性:如果多个服务参与一个操作,那么在它们之间协调幂等性会变得更具挑战性。
结论
理想的幂等性系统是不需要幂等性的系统——例如无状态操作,其输出始终直接由输入决定。然而,在现实应用中,并非所有系统都能做到无状态。例如,银行系统在进行存款或取款等更新操作之前,必须先读取当前账户余额。在这种情况下,实现幂等性对于维护一致的系统状态、防止意外副作用以及通过避免重复交易或数据损坏来确保无缝的用户体验至关重要。
通过使用唯一键和乐观锁等技术,开发人员可以在权衡利弊的情况下确保幂等性。唯一键需要额外的存储和请求跟踪,乐观锁可能会在高并发环境中导致更高的拒绝率,而请求去重则依赖于维护日志,这可能会引入额外的开销。
了解这些挑战有助于为特定系统选择合适的方法,使开发人员能够构建能够抵抗故障的系统,有效处理重试,并在分布式环境中维护数据完整性。
原文标题:Idempotency in Distributed Systems: When and Why It Matters,作者:Sandeep Kumar Gond