当前位置: 首页 > news >正文

记一次api接口出现重复请求的处理过程

背景
    在学校的Java项目中,开发了一个文本聊天的api接口,由于当时考虑欠缺,加上调用方没有做限制,导致出现了一个用户在一秒内重复发出了多次请求的情况,从而出现了脏数据。


既然是接口重复请求了,那么就需要限制一个用户在一秒内只能发起一次请求。这时候就会引出一个概念【接口的幂等性】。
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次。
现实的场景如下

1. ​​订单与支付类操作​

  • ​订单创建​​:用户多次点击提交订单,需保证仅生成一个有效订单。
  • ​订单支付​​:支付接口因网络超时重试时,需确保扣款仅一次,避免重复扣款。
  • ​订单状态变更​​:如订单发货、完成等操作,重复请求需维持最终状态一致。

​示例​​:
用户支付时因网络延迟重复调用支付接口,若未做幂等,可能生成两笔扣款记录。


2. ​​资源分配与扣减​

  • ​库存扣减​​:秒杀场景中,多次请求需确保库存不被超卖。
  • ​优惠券/积分发放​​:用户领取优惠券或积分时,需防止重复领取。
  • ​账号注册​​:同一手机号或邮箱多次注册,需返回已存在的结果。

​示例​​:
库存扣减时,若重复扣减未校验幂等性,可能导致库存变为负数。


接口幂等性主要应用于 ​​可能因重复请求导致数据错误或资源冲突​​ 的业务场景,以下是典型业务操作分类:


1. ​​订单与支付类操作​

  • ​订单创建​​:用户多次点击提交订单,需保证仅生成一个有效订单。
  • ​订单支付​​:支付接口因网络超时重试时,需确保扣款仅一次,避免重复扣款。
  • ​订单状态变更​​:如订单发货、完成等操作,重复请求需维持最终状态一致。

​示例​​:
用户支付时因网络延迟重复调用支付接口,若未做幂等,可能生成两笔扣款记录。


2. ​​资源分配与扣减​

  • ​库存扣减​​:秒杀场景中,多次请求需确保库存不被超卖。
  • ​优惠券/积分发放​​:用户领取优惠券或积分时,需防止重复领取。
  • ​账号注册​​:同一手机号或邮箱多次注册,需返回已存在的结果。

​示例​​:
库存扣减时,若重复扣减未校验幂等性,可能导致库存变为负数。


3. ​​数据更新与状态流转​

  • ​状态机操作​​:如工单从“处理中”变更为“已完成”,需确保状态仅变更一次。
  • ​字段更新​​:如用户余额增减(充值、提现),需避免重复累加或扣减。
  • ​配置修改​​:系统参数调整多次提交,需保证最终值与最后一次提交一致。

​示例​​:
用户提现时接口超时重试,若未做幂等,可能多次扣减余额。


4. ​​消息队列消费场景​

  • ​消息重复消费​​:MQ因ACK失败重发消息时,需保证业务逻辑仅执行一次。
  • ​异步任务处理​​:如批量导入数据,需确保任务ID唯一,避免重复导入。

​示例​​:
订单支付成功后发送MQ通知物流系统,若消息重复消费,需避免重复创建物流单。


为了保持接口的幂等性,一般的解决方案有以下

1. ​​Token 机制(一次性令牌)​

  • ​原理​​:客户端在发起请求前先获取一个唯一Token(如UUID),服务端存储该Token(如Redis)。接口处理时校验Token是否存在,存在则执行业务并删除Token,确保仅一次有效。

2. ​​唯一索引/数据库约束​

  • ​原理​​:利用数据库的唯一索引(如订单号、业务流水号),插入重复数据时触发唯一键冲突,拦截重复请求。 
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    order_no VARCHAR(32) UNIQUE, -- 唯一约束
    ...
);

try {
    orderDao.insert(order);
} catch (DuplicateKeyException e) {
    // 捕获重复异常,返回幂等结果
}

3. ​​状态机校验​

  • ​原理​​:业务数据具有明确状态流转(如订单从“待支付”到“已支付”),接口处理前校验状态是否允许变更。
Order order = orderDao.findById(orderId);
if (!order.getStatus().equals(Status.PENDING)) {
    throw new IllegalStateException("状态已更新,拒绝操作");
}

4. ​​乐观锁(版本号/时间戳)​

  • ​原理​​:在数据更新时,通过版本号或时间戳实现乐观锁,确保只有匹配当前版本的请求生效 
UPDATE table SET value = 'new', version = version + 1 
WHERE id = #{id} AND version = #{currentVersion};



int rows = jdbcTemplate.update(sql, params);
if (rows == 0) {
    throw new OptimisticLockException("数据已被修改");
}

5. ​​分布式锁控制​

  • ​原理​​:使用分布式锁(如Redis或ZooKeeper)保证同一业务ID的请求串行处理。
String lockKey = "order_lock_" + orderId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
    throw new BusyException("请勿重复提交");
}
try {
    // 处理业务
} finally {
    redisTemplate.delete(lockKey);
}

6. ​​请求去重表​

  • ​原理​​:单独维护一张去重表,记录请求的唯一标识(如业务ID + 场景),处理前先查询是否已存在。
CREATE TABLE idempotent_record (
    id VARCHAR(64) PRIMARY KEY, -- 业务ID + 类型
    created_time TIMESTAMP
);

String recordId = bizId + "_" + type;
if (idempotentDao.exists(recordId)) {
    return; // 直接返回历史结果
}
idempotentDao.insert(recordId);

 7. ​​限流与重试策略​

  • ​辅助方案​​:通过限流(如令牌桶)降低重复请求频率,结合客户端退避重试(Exponential Backoff)减少无效调用。
  • ​工具​​:使用Guava RateLimiter或Sentinel实现限流。

由于调用方目前无法配合预请求token的动作,我这次采用的是第五种方式,采用SpringAop+redis+注解的方式来做接口限制。

注解

import java.lang.annotation.*;

/**
 *  在需要保证接口幂等性的Controller的方法上使用此注解
 *  重复提交校验注解
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReSubmitCheck {
    //校验几秒内重复提交
    int seconds()  default 2;
}

核心逻辑,主要是通过注解+aop 的形式,在请求该接口前,先做判断,利用redis的原子操作,如果key在redis中存在,则说明在指定时间内,已经请求过了。

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class PreventReSummitAspect {

    /**
     * redis工具类
     */
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Before("@annotation(reSubmitCheck)")
    public void preventReSubmit(JoinPoint joinPoint, ReSubmitCheck reSubmitCheck) {

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //获取用户登录的accesstoken
        HttpServletRequest request = attributes.getRequest();
        String studentId = request.getParameter("studentId");
        if (studentId == null) {
            throw new RuntimeException("学生ID不能为空");
        }
        String senderType = StringUtils.isBlank(request.getParameter("senderType"))?"1":request.getParameter("senderType");
        String lockKey = "ReSubmit:" + studentId + "_" +senderType+"_"+request.getServletPath();
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, reSubmitCheck.seconds(), TimeUnit.SECONDS);
        if (!result) {
            System.out.println("重复请求:"+lockKey);
            throw new RuntimeException("重复请求:"+lockKey);
        }
    }


}

使用

    @PostMapping(value = "/sendMsg")
    @ReSubmitCheck(seconds=2)
    public Result<String> sendMsg() throws IOException {

       
    }

postman测试

第一次请求



 紧接着第二次请求

 

相关文章:

  • 使用人工智能大模型腾讯元宝,如何免费快速做高质量的新闻稿?
  • 【 vue + js 】引入图片、base64 编译显示图片
  • 项目日志配置模板示例
  • python的多线程和多进程程序编程
  • 语音识别——根据声波能量、VAD 和 频谱分析实时输出文字
  • 企业数据孤岛如何破
  • Harmony实战之简易计算器
  • AI中的RAG技术:检索增强生成的全面解析
  • Python自动化爬虫:Scrapy+APScheduler定时任务
  • Cesium实现鹰眼图和主地图联动
  • 管道魔法木马利用Windows零日漏洞部署勒索软件
  • 【15】Strongswan watcher详解2
  • 力扣 905 按奇偶排序数组:双指针法的优雅实现
  • 服务器(一种管理计算资源的计算机)
  • 下列软件包有未满足的依赖关系: python3-catkin-pkg : 冲突: catkin 但是 0.8.10-
  • Sping Cloud配置和注册中心
  • 【vLLM 学习】API 客户端
  • 设计模式 - 单例
  • 深入解析 C# 中的模板方法设计模式
  • 分享一些使用DeepSeek的实际案例
  • 一个不懂技术的人如何做网站/热搜词排行榜
  • 如何自己做微信小程序/seo初级入门教程
  • 发布了一个网站 显示建设中/平台seo什么意思
  • 怎么样自己建设网站/百度一下官网首页百度一下
  • 怎么做好网站运营/顺德搜索seo网络推广
  • c语言除了做网站还能干什么/推广普通话的宣传语