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));
        }
        // 其他操作
    }
}

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

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

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

相关推荐
守护者1702 分钟前
JAVA学习-练习试用Java实现“一个词频统计工具 :读取文本文件,统计并输出每个单词的频率”
java·学习
bing_15813 分钟前
Spring Boot 中ConditionalOnClass、ConditionalOnMissingBean 注解详解
java·spring boot·后端
ergdfhgerty15 分钟前
斐讯N1部署Armbian与CasaOS实现远程存储管理
java·docker
lwb_011827 分钟前
RabbitMq详解
分布式·rabbitmq
勤奋的知更鸟29 分钟前
Java性能测试工具列举
java·开发语言·测试工具
三目君32 分钟前
SpringMVC异步处理Servlet
java·spring·servlet·tomcat·mvc
用户05956611920932 分钟前
Java 基础篇必背综合知识点总结包含新技术应用及实操指南
java·后端
fie888933 分钟前
Spring MVC扩展与SSM框架整合
java·spring·mvc
不太可爱的叶某人40 分钟前
【学习笔记】深入理解Java虚拟机学习笔记——第3章 垃圾收集器与内存分配策略
java·笔记·学习
YuTaoShao41 分钟前
Java八股文——JVM「类加载篇」
java·开发语言·jvm