接口幂等该如何设计和实现
前言
在程序开发的过程中是否遇到如下的问题:
- 同一件商品手速很快多点击了几次,在后台生成了两笔订单。
- 同一笔订单点了由于网络卡顿,点了两次支付,结果发现重复支付了。
- 微服务架构下应用间通过RPC调用失败,进入重试机制,导致一个请求提交多次。
- 黑客利用充值抓包到的数据,进行多次调用充值、评论、访问,造成数据的异常。
这些问题均可以通过接口幂等性设计来解决。幂等性意味着同一个请求无论被重复执行多少次,都能产生相同的结果,不会导致重复的操作或不一致的数据状态。
在现代分布式系统中,接口的幂等性设计和实现至关重要。本文将深入探讨接口幂等的重要性、实现方法以及可能面临的挑战,并提供测试接口幂等性的有效策略。
什么是接口幂等性
接口幂等性指的是一个接口或操作在相同的请求参数下,无论被执行多少次,其结果都是一致的且不会产生副作用。换句话说,如果一个请求已经成功执行,再次执行相同的请求应该不会对系统状态产生任何额外的影响。例如,一个获取用户信息的接口就是幂等的,因为多次获取同一个用户的信息不会改变系统的状态。
相反,非幂等接口可能会导致重复的操作和潜在的问题。以支付操作为例,如果没有实现幂等性,重复支付可能会给用户和商家带来不必要的麻烦和损失。
为什么需要接口幂等性
- 防止重复操作:幂等性可以确保系统不会因为重复的请求而产生重复的操作,从而避免数据错误和不一致。
- 提高系统可靠性:在网络不稳定或其他异常情况下,重复的请求是很常见的。幂等性可以帮助系统处理这些重复请求,而不会导致系统出错或不稳定。
- 增强用户体验:用户不需要担心因为不小心重复操作而导致的问题,从而提高了用户的使用体验和满意度。
- 简化错误处理:由于幂等接口可以安全地处理重复请求,因此在处理错误和恢复时更加容易,减少了复杂的错误恢复逻辑。
如何设计接口幂等性
- 使用唯一标识:为每个请求分配一个唯一的标识,例如请求 ID 或流水号。通过在请求中传递这个唯一标识,系统可以判断是否已经处理过该请求。
- 设计幂等的操作:确保操作本身是幂等的。例如,更新数据时可以采用"更新或插入"的策略,而不是直接修改已有记录。
- 使用事务:在涉及多个数据库操作的情况下,使用事务来确保整个操作的原子性和幂等性。
- 利用缓存:将请求的结果缓存起来,当接收到相同的请求时,直接返回缓存中的结果,避免重复执行操作。
如何实现接口幂等性
以下实现方式是基于demo完成,用于说明幂等性的设计和实现。
- 唯一标识:可以通过生成全局唯一的 ID(如 UUID)来标识每个请求。在请求的参数中包含这个 ID,服务器在处理请求时可以根据 ID 来判断是否已经处理过该请求。
服务端生成 requestId 之后将 requestId 放到redis中,当然需要给 ID 设置一个失效时间,超时的 ID 也会被删除。
public class RequestIdGenerator {
public static String generateRequestId() {
Stirng uuid = UUID.randomUUID().toString();
putCacheIfAbsent(uuid);
return uuid;
}
}
在接口中,将生成的请求 ID 与请求参数一起传递给服务器。
// 生成请求 ID
String requestId = RequestIdGenerator.generateRequestId();
// 构建请求参数
Map<String, String> requestParams = new HashMap<>();
requestParams.put("requestId", requestId);
requestParams.put("otherParam", "value");
// 发送请求
httpClient.sendRequest(requestParams);
服务器在接收到请求后,可以根据请求 requestId 来判断是否已经处理过该请求,并进行相应的处理。
当后端接收到订单提交的请求的时候,会先判断requestId在缓存中是否存在,第一次请求的时候,requestId一定存在,也会正常返回结果,但是第二次携带同一个requestId的时候被拒绝了。
- 幂等的操作:以订单状态更新为例,如果订单已经处于最终状态(如已支付或已发货),再次更新订单状态不会改变其实际状态,因此是幂等的。
public class OrderService {
public void updateOrderStatus(String orderId, OrderStatus status) {
// 根据 orderId 获取订单
Order order = orderIdToOrderMapper orderIdToOrder(orderId);
// 判断订单是否处于最终状态
if (order.isFinalStatus()) {
// 订单已处于最终状态,不需要进行实际的更新操作
return;
}
// 更新订单状态
order.setStatus(status);
orderRepository.save(order);
}
}
- 事务:在数据库操作中,可以使用事务来保证操作的原子性和幂等性。如果某个操作失败,事务可以回滚到之前的状态,避免不一致的数据。
@Transactional
public void performTransactionalOperation() {
// 开启事务
Transaction transaction = transactionManager.beginTransaction();
transaction.setIsolationLevel(IsolationLevel.READ_COMMITTED);
transaction.setPropagationBehavior(Propagation.REQUIRED);
// 数据库操作 1
//...
// 数据库操作 2
//...
// 提交事务
transactionManager.commit();
}
开启事务是一种悲观锁实现的方式,一开始更新数据就把数据加锁了,具有强烈的独占和排他特性。
- 缓存:通过将请求的结果缓存起来,可以避免重复执行相同的操作。当接收到相同的请求时,直接从缓存中获取结果返回。
public class CacheService {
private Map<String, Object> cache = new ConcurrentHashMap<>();
public Object getCachedResult(String key) {
// 从缓存中获取结果
if (cache.containsKey(key)) {
return cache.get(key);
}
// 执行实际的操作并获取结果
Object result = performExpensiveOperation(key);
// 将结果缓存起来
cache.put(key, result);
// 返回结果
return result;
}
}
使用幂等性接口带来了什么结果
- 并发请求处理:在高并发环境下,可能会同时接收到多个相同的请求。为了处理这种情况,可以使用分布式锁或其他并发控制机制来确保只有一个请求执行实际的操作。目前的分布式锁一般基于zookeeper或者redis实现。
- 失败请求的处理:如果请求在执行过程中失败,需要确保幂等性仍然得到维护。可以通过记录请求的状态或使用重试机制来处理失败的请求。
- 与现有系统的集成:在将幂等性引入现有系统时,可能需要对现有系统进行一些修改和适配。这可能涉及到与其他组件或服务的协调和集成。
- 测试的复杂性:由于幂等性的测试需要模拟重复请求和各种边界情况,测试的复杂性可能会增加。需要设计全面的测试用例来覆盖各种可能的情况。
怎么验证接口是否具有幂等性
- 模拟重复请求:使用测试工具或手动模拟发送相同的请求多次,检查结果是否一致。
- 验证数据一致性:检查相关的数据是否在重复请求后保持一致,没有出现重复操作或数据不一致的情况。
- 压力测试:在高并发情况下测试接口的幂等性,确保在大量请求同时到达时系统仍然能正确处理。
- 异常情况测试:模拟各种异常情况,如网络中断、服务器故障等,检查接口在这些情况下是否仍然保持幂等性。
幂等性接口的总结
实现接口的幂等性对于构建可靠和高效的系统至关重要。通过使用唯一标识、幂等操作、事务和缓存等技术,可以有效地设计和实现幂等接口。
同时,要注意处理可能面临的挑战,并通过全面的测试来确保接口的正确性和稳定性。在实际项目中,积极应用这些方法将有助于提高系统的可靠性、安全性和用户体验。
参考
- 前任开发在代码里下毒了,支付下单居然没加幂等 https://juejin.cn/post/7324186292297482290
关于作者
来自一线全栈程序员nine的八年探索与实践,持续迭代中。欢迎关注“雨林寻北”或添加个人卫星codetrend(备注技术)。