接口幂等性
幂等概述

深入理解幂等性
在计算机领域中,幂等(Idempotence)是指任意一个操作的多次执行总是能获得相同的结果,不会对系统状态产生额外影响。在Java后端开发中,幂等性的实现通常通过确保方法或服务调用的结果具有确定性,无论调用次数如何,结果都是可预期的。
上面的定义是目前大多数文章和书籍对幂等的描述,然而,在实际的互联网服务开发中,幂等性的理论定义与业务逻辑间的冲突是常见的。
例如,考虑查询操作,当A系统调用B系统的查询接口时,如果首次调用由于B系统中的程序错误而导致业务逻辑失败,即使在程序修复后系统A重新使用相同参数进行重试,B系统可能仍然返回相同的失败响应。尽管这符合幂等性的定义,却与实际业务逻辑不符。同样,以订单支付为例,首次调用由于账户余额不足而返回“余额不足”提示,用户充值后再次使用相同参数发起支付请求,服务仍然返回“余额不足”响应,也符合幂等性的定义,但同样不符合业务逻辑。
因此,在实现幂等性方案时,应该遵循幂等性方案的目标,而不仅仅是严格遵循幂等性的定义。尤其是涉及写操作的服务,应当更关注防止重复请求带来的不良副作用,例如重复扣款或退款。
幂等性的必要性
在微服务和分布式架构中,一个请求可能需要多个服务协作才能完成。在这个过程中,网络抖动、系统运行异常等不确定因素使得请求的成功率不可能达到100%,一旦发生失败或未知异常,最常见的处理方式就是重试,而重试必然会导致重复请求问题。
幂等设计主要是为了处理重复请求而生的,好的幂等方案可以保证重复请求获得预期结果,而不产生副作用。
在实际开发中,以下场景会产生重复请求:
- 用户不可靠:用户通过客户端发起请求,由于手抖或有意重复点击,很容易造成导致极短时间内发起多次重复请求。
- 网络不可靠:网络抖动、网关内部抖动有可能触发重试机制,这个在使用消息队列投递消息时经常会遇到。
- 服务不可靠:在需要保证数据一致性的场景中,如果调用下游服务超时,在无法确认执行结果的情况下,常用的处理方法是重试。
幂等与并发的关系
在具有并发写操作的场景下,通常需要考虑幂等问题。例如,当用户在极短时间内多次提交表单或者使用特殊手段同时提交多个表单时,这就是典型的并发场景,需要进行幂等性处理。为了防止重复请求被执行,服务端需要实施幂等性控制,以避免产生不符合预期的结果。
虽然并发场景大都存在幂等问题,但幂等问题却并非并发场景所特有。幂等设计是为了识别并处理重复请求,而并发仅仅是重复请求的一种特殊情况。 事实上,只要重复请求涉及写操作,无论是否并发,都需要做好幂等处理。举个例子,用户在pc端同时开了两个窗口,间隔10分钟分别提交表单,所有参数完全相同,这显然不属于并发,但仍需要进行幂等处理。
在互联网领域,并发处理与幂等性问题紧密相关,这也导致了一些人认为解决幂等性就是解决高并发的问题。
幂等号的设计
幂等性设计的目的是确保即使在多次接收相同请求的情况下,也只执行一次操作,防止重复处理。要实现这一点,通常需要事先约定一个具有唯一性的标识符,如Token或业务流水号,我们称之为幂等号(Idempotency Key)。
幂等号有三个关键特性:唯一性、不变性和传递性。
唯一性确保每个请求都能被准确识别,不变性保证在请求处理期间幂等号保持不变,传递性则确保在多系统处理同一请求时,幂等号能够被传递和保持。
幂等号通常有两种设计方式:
非业务幂等号:通过唯一标识符(如UUID、时间戳或业务流水号)在调用方和被调用方之间明确实现幂等性。由于非业务幂等号难以通过业务上下文追溯,因此调用双方都必须将其持久化,从而保证请求与幂等号的关系有迹可循。
例如,在DailyMart案例中,订单服务在调用库存服务时会传递订单流水号作为幂等号,以便在多次请求时识别重复操作。
业务幂等号:由业务元素组合构成的幂等号,如“用户ID+活动ID”。使用此方法时,调用方无需单独持久化幂等号,被调用方可以根据请求参数和业务上下文直接获取并组合这些参数。例如,通过设置“用户ID”和“活动ID”的联合唯一索引来实现幂等性。
幂等的实现方案
Token机制(防抖利器)
1)当用户访问表单页面时,客户端请求服务端接口以获取唯一的Token(可以是UUID或全局ID),服务端生成的Token会被存储在Redis或数据库中。
2)用户首次提交表单时,将Token与表单一起发送至服务端,服务端会验证Token的存在性,如果Token存在,则执行业务逻辑,并在完成后销毁Token。
3)用户再次提交表单时,同样携带Token一起发送至服务端。但由于Token已被销毁,服务端无法找到对应的Token,从而拒绝重复提交请求。

1 |
|
唯一索引(简单有效)
唯一索引方案依赖于数据库表中不允许存在具有相同索引值的重复行。这种策略在关系型数据库中广泛支持,并且能有效利用唯一性约束来确保幂等性。在高并发场景中,唯一索引能保证当多个线程尝试同时插入相同记录时,只有一个线程能成功执行,而其他线程将会因违反唯一性约束而抛出异常。
适用场景:创建类操作(注册、下单等)
1 | CREATE TABLE orders ( |
异常处理示例:
1 | try { |
乐观锁(更新操作首选)
乐观锁主要依靠”带条件更新”(update with condition)来确保多次外部请求的一致性。在系统设计中,可以在数据表中添加版本号字段,用于标识当前数据的版本。每次对该数据表的记录进行更新时,都需要提供上一次更新的版本号
订单状态变更示例:
1 | UPDATE orders |
分布式锁机制
分布式锁与悲观锁本质上相似,都通过串行化请求处理来实现幂等性。与悲观锁不同的是,分布式锁更轻量。在系统接收请求后,首先尝试获取分布式锁。如果成功获取锁,则执行业务逻辑;如果获取失败,则立即拒绝请求。
分布式锁的核心是识别重复请求,实现串行化处理。但要注意,获取锁成功后,业务逻辑的执行并没有可靠保证。因此,在实际应用中,分布式锁需要结合事务机制和重试机制,以形成完整的幂等性解决方案。
1 | public String deductStock(String productId) throws InterruptedException { |
状态机机制
在许多业务单据中,存在有限数量的状态,并且这些状态之间的流转顺序是固定的。如果状态已经处于下一个状态,那么再次应用上一个状态的变更逻辑是不会产生任何效果的,这就确保了有限状态机的幂等性。

请求序列号(复杂业务流)

方案选型指南
| 方案 | 适用场景 | 性能影响 | 实现复杂度 | 可靠性 |
|---|---|---|---|---|
| Token机制 | 表单提交类场景 | 中 | 中 | 高 |
| 唯一索引 | 数据创建类操作 | 低 | 低 | 高 |
| 乐观锁 | 数据更新类操作 | 低 | 中 | 高 |
| 分布式锁 | 高并发写操作 | 高 | 高 | 中 |
| 状态机 | 多状态流转业务 | 低 | 高 | 高 |
| 请求序列号 | 金融级复杂事务 | 中 | 高 | 最高 |
选型建议:
1.简单业务优先使用唯一索引/乐观锁
2.高并发场景选择Redis+Token机制
3.资金交易类必须使用请求序列号
4.复杂业务流程结合状态机设计
作者:草捏子
链接:https://juejin.cn/post/7496340361351954441
作者:大鱼技术挖掘
链接:https://juejin.cn/post/7345071716681531392
来源:稀土掘金
仅用于学习交流,如有侵权,请联系删除。