最简单的接口重复请求处理方法

前言

常见的业务处理中,我们会遇到用户提交数据时出现重复的数据,可能出现:

  1. 用户重复点击提交按钮
  2. 接口被别有用心之人恶意请求
  3. 其它可能出现的问题网络或程序崩溃

解决

接口一定要保持对调用方的不信任

在重复请求处理中,我们的想法

  1. 用户在较短时间内,可能几秒内重复提交,可以给用户提示“重复请求”
  2. 某些接口需要处理
  3. 在执行业务方法前就知道是否是重复请求,减缓服务器压力
  4. 知道当前用户和用户请求的接口,这样才能针对用户做重复判断

结合以上想法,我们应该能想到spring aop

什么是aop

面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等。可以在不改变原有的逻辑的基础上,增加一些额外的功能。

它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

通知方法
  1. 前置通知:在我们执行目标方法之前运行**@Before**
  2. 后置通知:在我们目标方法运行结束之后 ,不管有没有异常**(@After)**
  3. 返回通知:在我们的目标方法正常返回值后运行**(@AfterReturning)**
  4. 异常通知:在我们的目标方法出现异常后运行**(@AfterThrowing)**
  5. 环绕通知:动态代理, 需要手动执行joinPoint.procced()(其实就是执行我们的目标方法执行之前相当于前置通知, 执行之后就相当于我们后置通知**(@Around)**

这里很自然的就可以使用前置通知**@Before**,然后结合自定义注解Annotation,添加注解到我们想要控制的接口上就可以了。

首先定义一个预重复请求处理注解

/**
 * 预重复请求注解
 *
 * @author MinWeikai
 * @date 2020-08-31 14:21:37
 */
//声明方法
@Target(ElementType.METHOD)
//运行时执行
@Retention(RetentionPolicy.RUNTIME)
public @interface PreDupRequest {

}

然后定义一个切面类,包含对使用该注解的处理方法

/**
 * 预重复请求切面方法
 *
 * @author MinWeikai
 * @date 2020-08-31 15:21:35
 */
@Aspect
@Component
public class PreDupRequestAspect {

	@Autowired
	private RedisSpringProxy redisSpringProxy;

	private static final Logger log = LoggerFactory.getLogger(PreDupRequestAspect.class);

	/**
	 * 业务开始处理前执行
	 *
	 * @param preDupRequest
	 * @return
	 * @throws BusinessException 抛出自定义异常,由全局异常处理捕获
	 *                           {@link GlobalExceptionHandler#handleBusinessException(BusinessException)}
	 */
    //定义切入点规则并且是使用了PreDupRequest注解的接口
	@Before("execution(public * com.mwk.controller.*Controller.*(..)) && @annotation(preDupRequest)")
	public void before(PreDupRequest preDupRequest) throws BusinessException {
		try {
			ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
             // 用户请求唯一标识,获取当前sessionId
			String sessionId = attributes.getSessionId();
             // 用户请求的uri
			String uri = attributes.getRequest().getServletPath();
             // 针对用户对应接口的唯一key
			String key = sessionId + "-" + uri;
             // 将生成的key存储在redis中,便于集群处理
			Object object = redisSpringProxy.read(key);
			if (ObjectUtil.isNull(object)) {
				redisSpringProxy.saveEx(key, 2, key);
				return;
			}
		} catch (Throwable e) {
			log.error("预防重复请求处理异常", e);
		}
		throw new BusinessException(ErrorCode.REPEATED_SUBMIT);
	}

}

说明:用户唯一请求判断可以是sessionId、token或其它用户唯一标识。因为我们要处理的是:某用户请求某接口在某时间内。

自己定义的全局处理异常

/**
 * @author MinWeikai
 * @date 2019/9/11 15:10
 */
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
	
	private static final Logger log = LoggerFactory.getLogger(ExceptionHandler.class);

	/**
	 * 处理所有业务异常
	 *
	 * @param e
	 * @return
	 */
	@org.springframework.web.bind.annotation.ExceptionHandler(BusinessException.class)
	ResponseResult handleBusinessException(BusinessException e) {
		return new ResponseResult(e.getCode(), e.getMsg());
	}
	
}

使用

在需要处理重复请求的接口所对应切入点controller并添加该注解**@PreDupRequest**

问题

在微信小程序请求接口中会出现sessionId变化的问题,因为这个请求经过微信服务器中转,对于我们的服务器来说每次请求都是新的请求,会生成新的sessionId。

我们可以做用户登陆后返回sessionId给小程序,然后小程序存储到本地,下次请求时将sessionId放在header中传过来,或使用其它用户唯一标识。

<think>好的,我现在需要解决用户关于如何防止处理API接口重复请求的问题。用户提到他们已经有前端防重复提交的代码,比如遮罩层,但有时候还是没拦住,所以想在后端做处理。我需要根据提供的三个引用资料来整理解决方案。 首先,引用[1]提到Token验证的方法,这应该是常见的解决方案之一。Token验证的思路是在请求前先获取一个唯一的Token,提交时必须带上这个Token,服务器验证后使其失效,防止重复使用。这个方案需要前后端配合,前端在提交表单前先请求Token,后端生成并存储,可能用缓存如Redis。不过要注意Token的存储和过期时间,避免资源浪费。 接下来,引用[2]和[3]都讨论了后端拦截的方法,可能包括幂等性设计和请求去重。幂等性是指同一请求多次执行结果相同,比如使用唯一标识(如订单ID)。用户可以在请求中带一个唯一ID,服务器检查该ID是否已处理过。另外,请求参数去重可以通过计算参数的哈希值,并在一段时间内缓存,拒绝重复的哈希值。这需要处理时间窗口,比如设置5秒内相同请求视为重复。 还有可能提到的方案是限流和加锁,比如分布式锁,确保同一用户或IP在短时间内不能重复提交。或者使用队列异步处理,将请求排队,避免并发重复。 需要考虑每种方案的优缺点。Token验证需要前端配合,但实现相对简单;幂等性设计适合写操作,如支付,但需要业务逻辑支持;请求去重可能对参数变化敏感,需合理设置时间窗口;分布式锁可能需要额外的基础设施,如Redis。 还要注意用户提到的前端已经做了措施但失效,可能原因有网络延迟、用户恶意请求或前端代码漏洞,所以后端防护是必要的。综合三个引用,应该总结出几种主要方案:Token验证、幂等性设计、请求参数去重、分布式锁、异步队列,并给出实现步骤和注意事项。 最后生成相关问题,可能涉及具体实现细节、优缺点比较、适用场景等。需要确保回答结构清晰,步骤明确,引用正确。</think>### 防止处理API接口重复请求解决方案 #### 1. **Token验证机制** **实现思路** - 在用户发起请求前,先向后端申请一个唯一Token(例如UUID),并将Token存储于缓存(如Redis)中,设置合理过期时间。 - 用户提交请求时必须携带该Token,后端验证Token有效性:若存在则处理请求并删除Token;若不存在则判定为重复请求,直接拒绝[^1]。 **代码示例(伪代码)** ```python def generate_token(): token = uuid4() redis.setex(token, 300, "valid") # 设置5分钟有效期 return token def handle_request(request): token = request.headers.get("X-Request-Token") if not redis.exists(token): return {"error": "重复请求"} redis.delete(token) # 处理业务逻辑 ``` **优点** - 实现简单,适用于表单提交等场景。 **缺点** - 需前端配合,对网络延迟敏感。 --- #### 2. **幂等性设计** **实现思路** - 为每个业务操作分配唯一标识(如订单ID),客户端在请求中携带该标识。 - 服务端通过数据库唯一索引或缓存记录标识,若已存在则直接返回之前的结果[^3]。 **适用场景** - 支付、订单创建等需保证唯一性的写操作。 **实现步骤** 1. 客户端生成唯一业务ID(如`order_id`)。 2. 服务端检查`order_id`是否已处理: ```sql INSERT INTO idempotent_keys (id) VALUES (:order_id) ON CONFLICT DO NOTHING; ``` 3. 若插入成功则处理请求,否则返回已缓存的结果。 --- #### 3. **请求参数去重** **实现思路** - 计算请求参数的哈希值(如MD5),并在缓存中记录该值及时间窗口(如5秒)。 - 若相同哈希值在时间窗口内重复出现,则判定为重复请求[^2]。 **代码示例** ```python def request_deduplicate(request): params_hash = md5(request.params).hexdigest() key = f"req_hash:{params_hash}" if redis.setnx(key, 1): # 仅当key不存在时设置 redis.expire(key, 5) # 处理请求 else: return {"error": "请求过于频繁"} ``` **注意事项** - 时间窗口需根据业务场景调整(如支付类设置更长)。 - 对参数顺序敏感,需规范化参数(如按字母排序)。 --- #### 4. **分布式锁** **实现思路** - 对用户或会话加锁,例如基于用户ID或IP使用Redis分布式锁,确保同一用户短时间内无法重复提交。 **实现示例** ```python from redis import Redis from redis.lock import Lock def handle_request(user_id): lock_key = f"lock:{user_id}" lock = Lock(redis, lock_key, timeout=5) if lock.acquire(blocking=False): try: # 处理业务逻辑 finally: lock.release() else: return {"error": "请勿重复提交"} ``` --- #### 5. **异步队列处理** **实现思路** - 将请求放入消息队列(如RabbitMQ、Kafka),由消费者异步处理,天然避免并发重复。 - 结合数据库状态标记(如“处理中”),防止多次消费。 --- ### 方案对比与选型建议 | 方案 | 适用场景 | 复杂度 | 依赖基础设施 | |-----------------|------------------------|--------|--------------| | Token验证 | 表单提交、短时操作 | 低 | 缓存服务 | | 幂等性设计 | 支付、订单类接口 | 中 | 数据库 | | 请求参数去重 | 参数固定的查询类接口 | 低 | 缓存服务 | | 分布式锁 | 高并发用户场景 | 高 | Redis | | 异步队列 | 允许延迟响应的业务 | 高 | 消息队列 | ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值