java编程中,保证接口幂等性的实现方案讨论

一、什么是幂等性

数学中的幂等是指f(x) = f(f(x)),编程领域的术语是指同一个操作,在重复提交的情况下,最终产生的影响是不变的。举例说:

  • 提交订单时,用户在购物车界面,重复点击"下单",服务端有且只会创建一个订单。(客户端在做交互设计的时候,用户点击了"下单"就不能再次点击,立马把按钮置灰。但是,这里会有网络的重试或者弱网等异常,导致服务端可能接收到两次http请求。如果服务端没有对http接口做幂等,那么可能会生成两个重复的订单)
  • 用户支付完成后,第三方支付会回调服务端的支付回调接口,服务端然后更新支付订单的状态,最后回调通知其他系统,比如订单系统和积分系统。这里的幂等性是指无论被回调多少次,不会重复更新支付订单的状态和回调通知其他系统。

总结: 对于重要的接口,在实现接口时,一定要考虑是否具备幂等性。

既然不是所有的接口,那都有哪些场景的接口,会需要考虑幂等性呢?

二、哪些场景需要实现幂等

1、异步回调

比如上文说的支付回调接口,也包括在其他的异步操作下的对外接口。

你都必须考虑是否支持幂等。

举个例子,课程服务调用课堂服务的批量创建课堂接口,由于后者是异步的操作,所以待创建完成,课堂服务将要回调通知课程服务。

于是,课程服务需要开放一个接口出来,此接口就必须支持幂等。

2、消息队列

有些业务,需要削峰填谷,会先把异步操作的消息给到Mq,然后在监听方法中消费该mq消息。这里举一个异步复制某文件夹及其下的所有文件为例。

3、网络重试

当出现网络超时,需要重试的时候,你的接口是否支持幂等呢?

比如openfeign就很好地支持retry机制。

三、哪些是天然幂等的

在进一步阐述如何实现幂等前,我们有必要搞清楚,哪些是天然幂等的。

都说自己每天写的代码都是CRUD,我们现在就来分析下它们是否幂等。

  • 查询方法,都说查询的实现往往是最复杂的代码,幸好,它就是幂等的。虽然你前后的查询返回结果可能不一样,然而并不会影响服务端的分毫。

  • 基于版本号或者状态机的更新操作,具体到sql语句,有两种写法,见下:

sql 复制代码
# 不建议的写法
update PayTrade t set t.status = :newStatus, t.outTradeNo = :outTradeNo, t.payOkDate = :payOkDate where t.channelTradeNo = :channelTradeNo

# 建议你这么写
update PayTrade t set t.status = :newStatus, t.outTradeNo = :outTradeNo, t.payOkDate = :payOkDate where t.channelTradeNo = :channelTradeNo and t.status = :oldStatus

他们的区别就是是否要求前一个状态,后面的sql多了一个" t.status = :oldStatus "

我们说,它就是一个幂等操作,而上面的写法则不建议,它不是幂等的。

这里举例了基于状态机的更新,基于版本号的类似,就不一一赘述了。

  • 最后说一说CRUD的删除操作D,它也是天然幂等的。因为删除了的数据,第二次想要再删除而不得了。

  • 大多数的新增操作,都需要最引起你的重视,极其容易导致重复操作。

四、设计思路

1、请求唯一标识

可以是一个随机值token,也可以是业务上的一个字段。

前者是客户端根据约定生成的一个值,这个一般使用userId+时间戳,后者则是业务上的唯一值,比如订单号、课程编号等。

那么随机token,可不可以由服务端来颁发给客户端呢?

也行,原本一个接口就能完成的,拆分成了两个接口。(增加了一个颁发token的接口)

下文将详细说明,如果使用分布式锁实现接口的幂等。

2、每次操作前,先查询

订单服务接收支付服务的支付回调,先根据订单号查询订单详情,如果数据库返回支付流水号和回调接口中的入参支付流水号相同,则忽略该回调,不做任何后续操作。

下面展示一下其简略代码:

java 复制代码
final Order order = orderRepository.findByNo(orderPaymentMarkRequest.getOrderNo());

// 如果流水号一样,认为曾经标记支付成功,保持幂等
if (!StringUtils.equals(order.getPayNo(), orderPaymentMarkRequest.getPayNo())) {
   // 业务处理,更新订单等等
}


public class OrderPaymentMarkRequest {

    @ApiModelProperty(notes = "订单号")
    private String orderNo;
    
    @ApiModelProperty(notes = "平台支付流水号")
    private String payNo;
}

3、使用乐观锁实现

引入状态机或者版本号等机制,前文已有举例,这里不再重复。

五、分布式锁实现幂等

代码示例:

java 复制代码
    @Lock(name = "CourseCenter", key = "'createLecture:token:'+ #request.token")
    @Transactional(rollbackFor = Throwable.class)
    public ApiResult createLecture(LectureCreateRequest request) {
        if (log.isInfoEnabled()) {
            log.info("创建讲次,入参是{}", JsonUtils.toJsonString(request));
        }
        // 其他操作
    }
}

这里使用了自定义注解封装分布式锁,所以代码本身比较简单。

鉴于篇幅,具体实现分布式锁,网上也比较多的实现方案。

本文就不具体展开论述了。

相关推荐
zhanghaha13141 小时前
HTML系列教程:14_HTML 表单与输入框 <form>、<input> 零基础详解
java·前端·javascript
傻啦嘿哟7 小时前
某招聘平台爬虫:爬取招聘岗位数据,分析各城市薪资水平
开发语言·爬虫·python
2501_933670797 小时前
2026秋招量化分析岗技能栈:Python、SQL、统计建模、回测项目怎么准备
开发语言·python·sql
yxlalm7 小时前
Spring AI+RAG 01-项目背景与技术选型
java·人工智能·spring
程序员清风7 小时前
Java 后端高并发设计:线程池、限流、熔断与降级
java·数据库·oracle
李少兄7 小时前
JavaScript 数据类型完全指南
开发语言·javascript·ecmascript
Seoyoneh8 小时前
Agentic Workflow编排架构:云客服从“被动响应”迈向“主动执行”的技术实现
java·开发语言·架构
海南java第二人8 小时前
对外 API 如何保证幂等性?插入数据 / 上传表格场景全方案汇总
java·幂等性
tryxr8 小时前
Chat2Excel 文件服务模块上传文件功能开发
java·项目·oss·easyexcel·文件服务
Tangyuewei8 小时前
Java 写 Agent:模型只是组件
java·开发语言