那么,我们该怎么做才能保证多次执行操作的结果与仅执行一次的结果相同呢?
让系统具备幂等性!!!
什么是幂等性?
幂等性是指一个系统或过程在多次执行相同操作时能够产生相同结果的能力。幂等性保证了多次执行相同操作不会引入意外的副作用,从而防止了意外的重复处理或不期望的更改。例如,在重复检查中,幂等系统确保重复请求不会导致重复处理,因此如果向系统发出重复请求,幂等系统要么忽略它,要么返回首次处理的状态。
等一下,我有点困惑!!!重复检查和幂等操作是一样的吗?
重复检查 vs 幂等性
重复检查旨在防止不必要的状态更改,确保我们不会多次处理同一事件,而幂等性则允许我们再次处理相同的事件,但结果将是相同的。简单来说,通过幂等性,我们可以在至少一次的消息传递系统中处理重复事件,确保多次处理相同事件仍然产生相同的效果。换句话说,幂等性是处理重复事件的最佳方式。
这听起来不错。但我们该如何实现它,可能会遇到什么挑战呢?
幂等性实现策略
为了构建幂等性,最重要的任务是为每个请求找到或创建一个幂等键。我制定了一个简单的算法策略,并添加了一些标准键来构建稳健的幂等性。
策略:
- 为每个请求找到一个唯一的关联标识符,我们可以依赖它并存储在数据存储中,以便检查每个传入请求。
- 如果没有唯一标识符,可以通过对有效负载进行哈希处理来创建校验和,并将其用于幂等性。
- 如果有效负载包含UUID或时间戳(如创建时间戳),可能在每次重试时会改变,尽可能忽略这些字段并创建校验和。如果无法忽略,请确保使用硬编码值。
- 对于POST API,在请求头中添加x-idempotency-key,并要求消费者提供一个唯一标识符和一个可选的过期时间。
- 在服务网格中,服务协作执行任务时,使用状态变化模型和重复检查来确保系统的幂等性。
- 定义幂等键的有效期,例如你的数据存储将存储该键的时间长度。
- 可以使用如下示例中展示的通用模型来创建复合幂等键:
public class IdempotentKey {
private String key; // 键
private long ttl; // 生存时间
private String result; // 响应
}
幂等键
可用于构建幂等键的标准键:
- UUID v4: 使用标准的java.util.UUID创建唯一标识符。
- 有效负载哈希: 创建请求有效负载的哈希(摘要)作为幂等键,保证相同有效负载的请求生成相同的键。•键元素: 在幂等键中包含用户ID、交易详情和时间戳等唯一参数,创建精确识别的复合键。
- 令牌: 发放并要求客户端在请求中包含令牌,作为幂等键和安全措施。
- 时间戳: 使用时间戳或组合时间戳作为时间基幂等键,确保操作在定义的时间窗口内是幂等的。
幂等模型
幂等性如何实现,以及如何处理不同场景?下面是我开发的幂等性的shell级实现。
- 客户端将有效负载“p12345”发送到接收服务进行处理。客户端可以选择在请求头中发送x-idempotency-key,作为处理的幂等键。如果客户端未发送幂等键头,则我们可以通过对有效负载进行哈希处理创建一个,并将键存储在Mapper中。(如果订单ID尚未生成,系统将为每个新请求生成一个)。
- 接收服务将执行重复检查,查看订单是否已存在系统中;如果是,它将返回订单的当前状态;如果不是,它将持久化订单及其当前状态—RCVD。
- 接下来,接收服务可以进行必要的验证并进行支付。如果成功,则加载订单的当前状态(因为它可能已经是PAID状态),如果状态是RCVD,则处理支付并更新状态为—PAID。如果接收服务由于网络故障或其他原因重试支付现有订单,我们的状态模型将救场,重试将被拒绝,因为订单已经是PAID状态。
- 成功支付后,接收服务将订单发送进行履行。如果订单状态是PAID,订单将被履行并达到最终状态—FULFILLED。如果此处发生重试,状态模型将救场。
- 注意,如果接收服务或支付服务有多个实例(pods)运行,确保在更新状态时拥有集群锁。
- 另一个要点是,为了加快幂等性的处理,可以使用缓存层存储映射信息,但在我的案例中,主数据存储足以处理高负载(避免过早优化)。
这是幂等性的一个演示用例。对于生产环境,你需要考虑你的用例,以及如何将该模型应用于实际场景。
结论
幂等性对于构建大规模、数据完整性不受影响的弹性分布式系统至关重要。幂等性的重试策略是分布式事务的一个优秀替代方案,后者更复杂且随着扩展更难以管理。