幂等性接口指的是一个接口在多次调用的情况下,对系统的影响是一致的,不会因为调用多次而产生不同的结果。也就是说,无论调用多少次,对系统的影响都是一样的,不会重复执行相同的操作,也不会对系统状态造成影响。
应用场景
- 避免重复提交
- 防止并发操作
- 接口超时重试
- 消息重复消费
常见实现方案
唯一索引
在数据库中为需要幂等的接口请求参数设置唯一索引,当重复请求到来时,数据库会抛出唯一索引冲突的异常,此时可以将异常捕获并返回幂等结果。
需要注意的是,这种实现方式需要依赖数据库的唯一索引特性,如果数据库没有设置唯一索引或者唯一索引被删除,那么该实现方式就会失效。另外,这种实现方式需要对数据库的性能和并发量进行评估和测试,以确保数据库能够承受高并发的请求。
乐观锁
乐观锁的实现方式是在数据表中增加一个版本号或时间戳,每次更新数据时都会对版本号或时间戳进行比较,如果版本号或时间戳一致,则更新成功,否则更新失败。
在实现幂等接口时,可以将请求参数中的唯一标识作为版本号或时间戳,每次请求时都进行版本号或时间戳的比较,如果一致则表示请求已经处理过,不再处理。
需要注意的是,乐观锁的实现方式需要保证并发情况下的数据一致性,否则会出现数据异常的情况。
假设有一个账户余额为 100 元,同时有两个线程 A 和 B 都要对该账户进行扣款操作,扣款金额为 50 元。如果采用乐观锁的方式实现幂等性,那么在执行扣款操作时,线程 A 和 B 都会先查询该账户的余额,然后判断余额是否足够进行扣款。如果余额足够,则进行扣款并更新账户余额,否则不进行扣款。此时,如果线程 A 和线程 B 同时查询到账户余额为 100 元,都判断余额足够进行扣款,然后都进行了扣款操作并更新了账户余额,那么最终账户余额就会变成了 -50 元,出现了数据异常的情况。这种情况称为“并发更新问题”,是乐观锁需要解决的一个问题。
悲观锁
悲观锁是指在操作数据时,认为数据随时可能被其他线程修改,因此在操作数据时会对数据进行加锁,确保数据操作的独占性,避免其他线程对数据进行修改。
悲观锁的操作步骤一般是在操作数据前,先对数据进行加锁,然后进行操作,操作完成后再释放锁。
悲观锁的实现方式一般有数据库锁、Java 中的 synchronized 关键字、ReentrantLock 等。悲观锁的优点是可以保证数据的一致性,避免数据出现异常情况,但是悲观锁的缺点是在高并发的情况下,锁的竞争会导致性能下降,影响系统的吞吐量。因此,在实现幂等接口时,悲观锁不是一个很好的选择。
另外,如果确定要使用数据库悲观锁,一定要使用主键或者唯一建,不然会将整张表都被锁住,那么其他的用户就无法操作了。
分布式锁
分布式锁可以保证同一时间只有一个线程或进程能够执行特定的代码块。
具体实现方式可以使用分布式锁框架,如 Redisson、ZooKeeper 等。
需要注意的是,在使用分布式锁时,应该考虑锁的超时时间、锁的粒度以及锁的重入等问题。同时,还需要特别注意锁的释放,以避免锁的持有时间过长或锁的释放不及时导致的问题。
Token 机制
在调用接口前,服务端生成一个唯一的 Token 并返回给客户端,客户端在调用接口时携带该 Token,服务端在接收到请求时校验该 Token 的合法性,如果该 Token 已经被使用过,则返回幂等性校验失败的结果。这种方式可以保证同一个请求在一段时间内只能被执行一次,从而实现幂等性。
需要注意的是,Token 机制需要保证 Token 的唯一性,避免 Token 被重复使用。同时,Token 的有效期也需要考虑,避免 Token 过期后仍然可以被使用。
在有的实现里,是由客户端生成 Token,这样就要求客户端来保证 Token 的唯一性,否则可能会导致重复提交的问题。此外,如果客户端生成的 Token 不够随机或者不够安全,也可能会被攻击者破解,从而导致安全问题。因此,建议还是由服务端来生成 Token。
有限状态机
有限状态机是一种模型,用于描述系统在不同状态下的行为和转移规则,它可以根据当前状态和输入执行相应的操作,并转移到下一个状态。在实现幂等接口时,可以将幂等的状态作为有限状态机的状态,每次接收到请求时,根据当前状态和请求参数判断是否需要执行幂等操作,并根据执行结果转移到下一个状态。这样可以确保在不同状态下,系统的行为是一致的,从而实现幂等性。
需要注意的是,在使用有限状态机实现幂等接口时,需要考虑状态的转移规则,以及状态之间的关系,确保系统的行为符合预期。
假设有一个订单支付接口,请求参数包含订单号和支付金额,要求该接口实现幂等性。
使用有限状态机实现时,可以将订单状态分为“未支付”、“支付中”、“已支付”三种状态,每当有一笔支付请求到达时,首先根据订单号查询订单状态,然后根据当前状态判断是否需要执行支付操作。 具体实现方式如下:
1. 当订单状态为“已支付”时,直接返回支付成功结果,不执行支付操作。
2. 当订单状态为“未支付”时,将订单状态修改为“支付中”,执行支付操作,支付成功后将订单状态修改为“已支付”,并返回支付成功结果。
3. 当订单状态为“支付中”时,等待一段时间后再次查询订单状态,如果订单状态已经变为“已支付”,则返回支付成功结果,否则返回支付失败结果。
使用有限状态机实现幂等接口的优点是实现简单,易于理解和维护。缺点是可能需要占用较多的存储空间,因为需要为每个订单维护一个状态。此外,如果有多个接口需要实现幂等性,需要为每个接口都单独实现一个状态机,增加了开发和维护的难度。
校验请求参数
具体实现方式是在每次请求时,将请求参数做摘要计算,然后将摘要值作为幂等性校验的依据。在第一次请求时,服务器会将请求参数摘要值存储到数据库或缓存中,并返回响应结果。在后续的请求中,服务器会先对请求参数做摘要计算,然后与之前存储的摘要值进行比对,如果两者相同,则认为该请求是幂等的,直接返回之前的响应结果即可。
需要注意的是,在使用请求参数校验实现幂等接口时,需要确保请求参数的唯一性,否则可能会出现误判的情况。同时,由于请求参数可能会比较复杂,因此在进行摘要计算时,需要选择合适的算法,并且确保算法的稳定性和正确性。
Token 机制实现
- 定义幂等性注解,用于标识幂等接口。
/** * 用于标记接口为幂等接口,即无论客户端发送多少次请求,服务端只会处理一次 */ @Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiIdempotence { }
- 创建拦截器,用于拦截幂等接口请求。
/** * 拦截幂等请求,校验当前请求是否执行过,如果已执行过,则抛出 {@link IdempotentException} 异常 */ public class IdempotenceInterceptor implements HandlerInterceptor { private static final String IDEMPOTENCE_ID = "idempotenceId"; private final IdempotenceIdStore idempotenceIdStore; public IdempotenceInterceptor(IdempotenceIdStore idempotenceIdStore) { this.idempotenceIdStore = idempotenceIdStore; } @Override public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { if (handler instanceof HandlerMethod handlerMethod) { Method method = handlerMethod.getMethod(); ApiIdempotence apiIdempotence = method.getAnnotation(ApiIdempotence.class); if (apiIdempotence != null) { String idempotentId = this.parseIdempotenceId(request); // 不存在则表示已经删除了 if (!idempotenceIdStore.exist(idempotentId)) { throw new IdempotentException("Repeated requests will not be processed"); } // 防止多个请求同时执行到这里,只有第一个删除成功的才能继续执行 if (!idempotenceIdStore.delete(idempotentId)) { throw new IdempotentException("Repeated requests will not be processed"); } } } return true; } /** * 从 {@link HttpServletRequest} 中解析幂等号 * * @param request HttpServletRequest * @return 幂等号 */ private String parseIdempotenceId(HttpServletRequest request) { String idempotentId = request.getHeader(IDEMPOTENCE_ID); if (StringUtils.hasText(idempotentId)) { return idempotentId; } idempotentId = request.getParameter(IDEMPOTENCE_ID); if (StringUtils.hasText(idempotentId)) { return idempotentId; } throw new IdempotentException("Missing idempotenceId parameter"); } }
- 创建 Controller,用于生成幂等号。
/** * 幂等号 controller * * @author cdrcool * @date 2022/01/01 08:00 */ @Tag(name = "幂等号") @RequestMapping("idempotence-id") @RestController public class IdempotenceIdController { private final IdempotenceIdStore idempotenceIdStore; public IdempotenceIdController(IdempotenceIdStore idempotenceIdStore) { this.idempotenceIdStore = idempotenceIdStore; } @Operation(summary = "生成幂等号", description = "在请求幂等方法之前,需要先请求该方法以获取幂等号," + "之后携带幂等号(参数名:idempotenceId,支持在请求头或查询参数中传递幂等号)请求幂等方法") @PostMapping("/generate") public String generate() { return idempotenceIdStore.generate(); } }
-
定义幂等号存储类,用于幂等号的存储、删除等。