业务幂等性技术架构体系之接口幂等深入剖析

news2025/1/15 13:45:02

在实际应用中,由于网络不稳定、系统延迟等原因,客户端可能会重复发送相同的请求。如果这些重复请求都被服务器处理并执行,就可能导致意想不到的问题,比如重复扣款、多次下单或者数据不一致等。

这就是为什么我们需要接口幂等性。简单来说,接口幂等性意味着同一个操作不论被重复执行多少次,其最终结果都是一样的,不会因为多次调用而产生副作用或额外的影响。换句话说,对于一个幂等的接口,第一次调用它会按照预期完成任务;之后无论再怎么重复调用这个接口,都不会改变已经达成的结果状态。

举个例子来帮助理解:

  • 非幂等的操作:想象一下你正在网上购买一件商品,并且不小心点击了两次“提交订单”按钮。如果没有幂等机制保护,这可能会导致你的账户被扣费两次,并生成两个独立的订单。

  • 幂等的操作:现在假设同样的场景下,该电商平台实现了良好的幂等性设计。即使你误点了两次按钮,系统也能识别出这是同一笔交易的不同尝试,并只创建一个订单,避免了不必要的重复扣款。

接口幂等的解决思路主要有:前端防重、PRG模式、Token机制。

一、前端防重

通过前端防重保证幂等是最简单的实现方式,前端相关属性和JS代码即可完成设置。可靠性并不好,有经验的人员 可以通过工具跳过页面仍能重复提交。主要适用于表单重复提交或按钮重复点击。

二、PRG模式

PRG模式即POST-REDIRECT-GET。当用户进行表单提交时,会重定向到另外一个提交成功页面,而不是停留在原 先的表单页面。这样就避免了用户刷新导致重复提交。同时防止了通过浏览器按钮前进/后退导致表单重复提交。 是一种比较常见的前端防重策略。

三、Token机制

方案介绍

通过token机制来保证幂等是一种非常常见的解决方案,同时也适合绝大部分场景。该方案需要前后端进行一定程度的交互来完成。

方案1

对于该方案:

  • 1)服务端提供获取token接口,供客户端进行使用。服务端生成token后,如果当前为分布式架构,将token存放 于redis中,如果是单体架构,可以保存在jvm缓存中。
  • 2)当客户端获取到token后,会携带着token发起请求。
  • 3)服务端接收到客户端请求后,首先会判断该token在redis中是否存在。如果存在,则完成进行业务处理,业务 处理完成后,再删除token。如果不存在,代表当前请求是重复请求,直接向客户端返回对应标识。

但是现在有一个问题,当前是先执行业务再删除token。在高并发下,很有可能出现第一次访问时token存在,完 成具体业务操作。但在还没有删除token时,客户端又携带token发起请求,此时,因为token还存在,第二次请求 也会验证通过,执行具体业务操作。

对于这个问题的解决方案的思想就是并行变串行。会造成一定性能损耗与吞吐量降低。

第一种方案:对于业务代码执行和删除token整体加线程锁。当后续线程再来访问时,则阻塞排队。

第二种方案:借助redis单线程和incr是原子性的特点。当第一次获取token时,以token作为key,对其进行自增。 然后将token进行返回,当客户端携带token访问执行业务代码时,对于判断token是否存在不用删除,而是对其继 续incr。如果incr后的返回值为2。则是一个合法请求允许执行,如果是其他值,则代表是非法请求,直接返回。如下图所示

方案2

那如果先删除token再执行业务呢?架构如下图

其实也会存在问题,假设具体业务代码执行超时或失败,没有向客户端返回明确结果,那客户端就很有可能会进行重试,但此时之前的token已经被删除了,则会被认为是重复请求,不再进行业务处理。 

解决:这种方案无需进行额外处理,一个token只能代表一次请求。一旦业务执行出现异常,则让客户端重新获取令牌, 重新发起一次访问即可。推荐使用先删除token方案

总结:

无论先删token还是后删token,都会有一个相同的问题。每次业务请求都回产生一个额外的请求去获取 token。但是,业务失败或超时,在生产环境下,一万个里最多也就十个左右会失败,那为了这十来个请求,让其 他九千九百多个请求都产生额外请求,就有一些得不偿失了。虽然redis性能好,但是这也是一种资源的浪费。

方案实现

基于自定义业务流程实现

业务流程图

1)修改token_service_order工程中OrderController,新增生成令牌方法genToken

@Autowired
private IdWorker idWorker;

@Autowired
private RedisTemplate redisTemplate;

@GetMapping("/genToken")
public String genToken(){

    String token = String.valueOf(idWorker.nextId());

    redisTemplate.opsForValue().set(token,0,30, TimeUnit.MINUTES);

    return token;
}

 2) 修改token_service_api工程,新增OrderFeign接口。

@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderFeign {

    @GetMapping("/genToken")
    public String genToken();
}

 3)修改token_web_order工程中WebOrderController,新增获取token方法

@RestController
@RequestMapping("worder")
public class WebOrderController {

    @Autowired
    private OrderFeign orderFeign;

    /**
		* 服务端生成token
		* @return
	*/
    @GetMapping("/genToken")
    public String genToken(){

        String token = orderFeign.genToken();

        return token;
    }

}

 4)修改token_common,新增feign拦截器

@Component
public class FeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {

        //传递令牌
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        if (requestAttributes != null){

            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();

            if (request != null){

                Enumeration<String> headerNames = request.getHeaderNames();

                while (headerNames.hasMoreElements()){

                    String headerName = headerNames.nextElement();

                    if ("token".equals(headerName)){

                        String headerValue = request.getHeader(headerName);

                        //传递token
                        requestTemplate.header(headerName,headerValue);
                    }
                }
            }
        }
    }
}

 5)修改token_web_order启动类

@Bean
public FeignInterceptor feignInterceptor(){
    return new FeignInterceptor();
}

 6)修改token_service_orderOrderController,新增添加订单方法

/**
     * 生成订单
     * @param order
     * @return
     */
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order order, HttpServletRequest request){

    //获取令牌
    String token = request.getHeader("token");

    //校验令牌
    try {
        if (redisTemplate.delete(token)){

            //令牌删除成功,代表不是重复请求,执行具体业务
            order.setId(String.valueOf(idWorker.nextId()));
            order.setCreateTime(new Date());
            order.setUpdateTime(new Date());
            int result = orderService.addOrder(order);

            if (result == 1){
                System.out.println("success");
                return "success";
            }else {
                System.out.println("fail");
                return "fail";
            }
        }else {

            //删除令牌失败,重复请求
            System.out.println("repeat request");
            return "repeat request";
        }
    }catch (Exception e){
        throw new RuntimeException("系统异常,请重试");
    }
}

 7)修改token_service_order_apiOrderFeign

@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderFeign {

    @PostMapping("/genOrder")
    public String genOrder(@RequestBody Order order);

    @GetMapping("/genToken")
    public String genToken();
}

 8)修改token_web_orderWebOrderController,新增添加订单方法

/**
     * 新增订单
     */
@PostMapping("/addOrder")
public String addOrder(@RequestBody Order order){

    String result = orderFeign.genOrder(order);

    return result;
}

基于自定义注解实现

直接把token实现嵌入到方法中会造成大量重复代码的出现。因此可以通过自定义注解将上述代码进行改造。在需 要保证幂等的方法上,添加自定义注解即可。

1)在token_common中新建自定义注解Idemptent

/**
 * 幂等性注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idemptent {
}

2)在token_common中新建拦截器

public class IdemptentInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();

        Idemptent annotation = method.getAnnotation(Idemptent.class);
        if (annotation != null){
            //进行幂等性校验
            checkToken(request);
        }

        return true;
    }


    @Autowired
    private RedisTemplate redisTemplate;

    //幂等性校验
    private void checkToken(HttpServletRequest request) {
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)){
            throw new RuntimeException("非法参数");
        }

        boolean delResult = redisTemplate.delete(token);
        if (!delResult){
            //删除失败
            throw new RuntimeException("重复请求");
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

3)修改token_service_order启动类,让其继承WebMvcConfigurerAdapter

@Bean
public IdemptentInterceptor idemptentInterceptor() {
    return new IdemptentInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    //幂等拦截器
    registry.addInterceptor(idemptentInterceptor());
    super.addInterceptors(registry);
}

4)更新token_service_ordertoken_service_order_api,新增添加订单方法,并且方法添加自定义幂等注解

@Idemptent
@PostMapping("/genOrder2")
public String genOrder2(@RequestBody Order order){

    order.setId(String.valueOf(idWorker.nextId()));
    order.setCreateTime(new Date());
    order.setUpdateTime(new Date());
    int result = orderService.addOrder(order);

    if (result == 1){
        System.out.println("success");
        return "success";
    }else {
        System.out.println("fail");
        return "fail";
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2277032.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

sql模糊关联匹配

需求目标&#xff1a; 建立临时表 drop table grafana_bi.zbj_gift_2024;USE grafana_bi; CREATE TABLE zbj_gift_2024 (id INT AUTO_INCREMENT PRIMARY KEY,userName VARCHAR(255),giftName VARCHAR(255),giftNum INT,points INT,teacher VARCHAR(255),sendDate DATETIME,…

《蜜蜂路线》

题目背景 无 题目描述 一只蜜蜂在下图所示的数字蜂房上爬动,已知它只能从标号小的蜂房爬到标号大的相邻蜂房,现在问你&#xff1a;蜜蜂从蜂房 mm 开始爬到蜂房 nn&#xff0c;m<nm<n&#xff0c;有多少种爬行路线&#xff1f;&#xff08;备注&#xff1a;题面有误&am…

LeetCode100之搜索二维矩阵(46)--Java

1.问题描述 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target 在矩阵中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回…

JS爬虫实战演练

在这个小红书私信通里面进行一个js的爬虫 文字发送 async function sendChatMessage(content) {const url https://pro.xiaohongshu.com/api/edith/ads/pro/chat/chatline/msg;const params new URLSearchParams({porch_user_id: 677e116404ee000000000001});const messageD…

自动连接校园网wifi脚本实践(自动网页认证)

目录 起因执行步骤分析校园网登录逻辑如何判断当前是否处于未登录状态&#xff1f; 书写代码打包设置开机自动启动 起因 我们一般通过远程控制的方式访问实验室电脑&#xff0c;但是最近实验室老是断电&#xff0c;但重启后也不会自动连接校园网账户认证&#xff0c;远程工具&…

WPS计算机二级•表格函数计算

听说这里是目录哦 函数基础知识 相对绝对混合引用&#x1f32a;️相对引用绝对引用混合引用 常用求和函数 SUM函数&#x1f326;️语法说明 函数快速求 平均数最值⚡平均数最值 实用统计函数 实现高效统计&#x1f300;COUNTCOUNTIF 实用文本函数 高效整理数据&#x1f308;RIG…

自动化测试工具Ranorex Studio(八十九)-解决方案浏览器

解决方案浏览器 除了为项目添加条目外&#xff0c;’Solution Explorer’允许你编辑解决方案的其他辅助选项。 例如&#xff0c;增加文件夹从而将项目中的录制模块和代码模块分离开来。 图&#xff1a;在solution browser中为项目添加文件夹 另外&#xff0c;你可以删除不用的…

2025 年 UI 大屏设计新风向

在科技日新月异的 2025 年&#xff0c;UI 大屏设计领域正经历着深刻的变革。随着技术的不断进步和用户需求的日益多样化&#xff0c;新的设计风向逐渐显现。了解并掌握这些趋势&#xff0c;对于设计师打造出更具吸引力和实用性的 UI 大屏作品至关重要。 一、沉浸式体验设计 如…

绘制三角形、正六边形、五角星、六角星

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>绘制图形</title><style>body {displ…

LLM实现视频切片合成 前沿知识调研

1.相关产品 产品链接腾讯智影https://zenvideo.qq.com/可灵https://klingai.kuaishou.com/即梦https://jimeng.jianying.com/ai-tool/home/Runwayhttps://aitools.dedao.cn/ai/runwayml-com/Descripthttps://www.descript.com/?utm_sourceai-bot.cn/Opus Cliphttps://www.opu…

Node.js - HTTP

1. HTTP请求 HTTP&#xff08;Hypertext Transfer Protocol&#xff0c;超文本传输协议&#xff09;是客户端和服务器之间通信的基础协议。HTTP 请求是由客户端&#xff08;通常是浏览器、手机应用或其他网络工具&#xff09;发送给服务器的消息&#xff0c;用来请求资源或执行…

鸿蒙中自定义slider实现字体大小变化

ui&#xff1a; import { display, mediaquery, router } from kit.ArkUI import CommonConstants from ./CommonConstants; import PreferencesUtil from ./PreferencesUtil; import StyleConstants from ./StyleConstants;// 字体大小 Entry Component struct FontSize {Sta…

Springboot + vue 小区物业管理系统

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e; &#x1f496;&#x1f4d5;&#x1f389;&#x1f525; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论 &#x1f525;&#x1f525;&…

uni-app编写微信小程序使用uni-popup搭配uni-popup-dialog组件在ios自动弹出键盘。

uni-popup-dialog 对话框 将 uni-popup 的type属性改为 dialog&#xff0c;并引入对应组件即可使用对话框 &#xff0c;该组件不支持单独使用 示例 <button click"open">打开弹窗</button> <uni-popup ref"popup" type"dialog"…

RabbitMQ中有哪几种交换机类型?

大家好&#xff0c;我是锋哥。今天分享关于【RabbitMQ中有哪几种交换机类型&#xff1f;】面试题。希望对大家有帮助&#xff1b; RabbitMQ中有哪几种交换机类型&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在RabbitMQ中&#xff0c;交换机&#xf…

Uniapp中实现加载更多、下拉刷新、返回顶部功能

一、加载更多&#xff1a; 在到达底部时&#xff0c;将新请求过来的数据追加到原来的数组即可&#xff1a; import {onReachBottom } from "dcloudio/uni-app";const pets ref([]); // 显示数据function network() {uni.request({url: "https://api.thecatap…

Kotlin 循环语句详解

文章目录 循环类别for-in 循环区间整数区间示例1&#xff1a;正向遍历示例2&#xff1a;反向遍历 示例1&#xff1a;遍历数组示例2&#xff1a;遍历区间示例3&#xff1a;遍历字符串示例4&#xff1a;带索引遍历 while 循环示例&#xff1a;计算阶乘 do-while 循环示例&#xf…

【零基础租赁实惠GPU推荐及大语言模型部署教程01】

租赁GPU推荐及大语言模型部署简易教程 1 官网地址2 注册账号及登录3 租用GPU3.1 充值&#xff08;不限制充值最低金额&#xff0c;1元亦可&#xff09;3.2 容器实例&#xff08;实际就是你租用的GPU电脑&#xff09;3.3 选择镜像&#xff08;选择基础环境&#xff1a;框架版本和…

Centos 宝塔安装

yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh 安装成功界面 宝塔说明文档 https://www.bt.cn/admin/servers#wcu 或者可以注册宝塔账号 1 快速部署 安装docker 之后 2 需要在usr/bin下下载do…

新版AndroidStudio通过系统快捷创建带BottomNavigationView的项目踩坑记录

选择上面这个玩意创建的项目 坑点1 &#xff1a;配置的写法和不一样了 镜像的写法&#xff1a; 新的settings.gradle.kts中配置镜像的代码&#xff1a; pluginManagement {repositories {mavenCentral()google {content {includeGroupByRegex("com\\.android.*")…