Spring Cloud项目如何防止重复提交,防重复提交幂等校验,Redis+aop+自定义Annotation实现接口

Posted MateCloud微服务

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud项目如何防止重复提交,防重复提交幂等校验,Redis+aop+自定义Annotation实现接口相关的知识,希望对你有一定的参考价值。

需求说明

在项目开发过程,我们也会经常遇到这种问题,前端未拦截,或者拦截失败,导致后端接收到大量重复请求,结果把这些重复请求入库后,产生大量垃圾数据。

解释下幂等的概念:

任意多次执行所产生的影响均与一次执行的影响相同

那这个听起来是问题的解决之道,访如何实现,通常有如下方式:

  1. 建立数据库索引,保证最终插入的只有一条数据;
  2. token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token;
  3. 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
  4. 先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。

幂等的原理图

实战演练

实战中对上述进行一些优化,支持Header和判断参数值两种形式,详细看如下代码:

package vip.mate.core.ide.enums;


import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 幂等枚举类
 *
 * @author pangu
 */
@Getter
@AllArgsConstructor
public enum IdeTypeEnum 

	/**
	 * 0+1
	 */
	ALL(0, "ALL"),
	/**
	 * ruid 是针对每一次请求的
	 */
	RID(1, "RID"),
	/**
	 * key+val 是针对相同参数请求
	 */
	KEY(2, "KEY");

	private final Integer index;
	private final String title;

拦截器业务逻辑

package vip.mate.core.ide.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import vip.mate.core.common.aspect.BaseAspect;
import vip.mate.core.ide.annotation.Ide;
import vip.mate.core.ide.enums.IdeTypeEnum;
import vip.mate.core.ide.exception.IdeException;
import vip.mate.core.redis.core.RedisService;

import javax.servlet.http.HttpServletRequest;

/**
 * 注解执行器 处理重复请求 和串行指定条件的请求
 * <p>
 * 两种模式的拦截
 * 1.rid 是针对每一次请求的
 * 2.key+val 是针对相同参数请求
 * </p>
 *
 * @author pangu
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
@ConditionalOnClass(RedisService.class)
public class IdeAspect extends BaseAspect 

	private final ThreadLocal<String> PER_FIX_KEY = new ThreadLocal<String>();

	/**
	 * 配置注解后 默认开启
	 */
	private final boolean enable = true;

	/**
	 * request请求头中的key
	 */
	private final static String HEADER_RID_KEY = "RID";

	/**
	 * redis中锁的key前缀
	 */
	private static final String REDIS_KEY_PREFIX = "RID:";

	/**
	 * 锁等待时长
	 */
	private static final int LOCK_WAIT_TIME = 10;

	private final RedisService redisService;

	@Pointcut("@annotation(vip.mate.core.ide.annotation.Ide)")
	public void watchIde() 

	

	@Before("watchIde()")
	public void doBefore(JoinPoint joinPoint) 
		Ide ide = getAnnotation(joinPoint, Ide.class);

		if (enable && null != ide) 
			ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
			if (null == attributes) 
				throw new IdeException("请求数据为空");
			
			HttpServletRequest request = attributes.getRequest();

			//1.判断模式
			if (ide.ideTypeEnum() == IdeTypeEnum.ALL || ide.ideTypeEnum() == IdeTypeEnum.RID) 
				//2.1.通过rid模式判断是否属于重复提交
				String rid = request.getHeader(HEADER_RID_KEY);

				if (StringUtils.isNotBlank(rid)) 
					Boolean result = redisService.tryLock(REDIS_KEY_PREFIX + rid, LOCK_WAIT_TIME);
					if (!result) 
						throw new IdeException("命中RID重复请求");
					
					log.debug("msg1=当前请求已成功记录,且标记为0未处理,,=", HEADER_RID_KEY, rid);
				 else 
					log.warn("msg1=header没有rid,防重复提交功能失效,,remoteHost=" + request.getRemoteHost());
				
			

			if (ide.ideTypeEnum() == IdeTypeEnum.ALL
					|| ide.ideTypeEnum() == IdeTypeEnum.KEY) 
				//2.2.通过自定义key模式判断是否属于重复提交
				String key = ide.key();
				if (StringUtils.isNotBlank(key)) 
					String val = "";
					Object[] paramValues = joinPoint.getArgs();
					String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
					//获取自定义key的value
					for (int i = 0; i < paramNames.length; i++) 
						String params = JSON.toJSONString(paramValues[i]);
						if (params.startsWith("")) 
							//如果是对象
							//通过key获取value
							JSONObject jsonObject = JSON.parseObject(params);
							val = jsonObject.getString(key);
						 else if (key.equals(paramNames[i])) 
							//如果是单个k=v
							val = params;
						 else 
							//如果自定义的key,在请求参数中没有此参数,说明非法请求
							log.warn("自定义的key,在请求参数中没有此参数,防重复提交功能失效");
						
					

					//判断重复提交的条件
					String perFix = ide.perFix();
					if (StringUtils.isNotBlank(val)) 
						perFix = perFix + ":" + val;

						try 
							Boolean result = redisService.tryLock(perFix, LOCK_WAIT_TIME);
							if (!result) 
								String targetName = joinPoint.getTarget().getClass().getName();
								String methodName = joinPoint.getSignature().getName();
								log.error("msg1=不允许重复执行,,key=,,targetName=,,methodName=", perFix, targetName, methodName);
								throw new IdeException("不允许重复提交");
							
							//存储在当前线程
							PER_FIX_KEY.set(perFix);
							log.info("msg1=当前请求已成功锁定:", perFix);
						 catch (Exception e) 
							log.error("获取redis锁发生异常", e);
							throw e;
						
					 else 
						log.warn("自定义的key,在请求参数中value为空,防重复提交功能失效");
					
				
			
		
	

	@After("watchIde()")
	public void doAfter(JoinPoint joinPoint) throws Throwable 
		try 
			Ide ide = getAnnotation(joinPoint, Ide.class);
			if (enable && null != ide) 

				if (ide.ideTypeEnum() == IdeTypeEnum.ALL
						|| ide.ideTypeEnum() == IdeTypeEnum.RID) 
					ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
					HttpServletRequest request = attributes.getRequest();
					String rid = request.getHeader(HEADER_RID_KEY);
					if (StringUtils.isNotBlank(rid)) 
						try 
//							redisService.unLock(REDIS_KEY_PREFIX + rid);
							log.info("msg1=当前请求已成功处理,,rid=", rid);
						 catch (Exception e) 
							log.error("释放redis锁异常", e);
						
					
					PER_FIX_KEY.remove();
				

				if (ide.ideTypeEnum() == IdeTypeEnum.ALL
						|| ide.ideTypeEnum() == IdeTypeEnum.KEY) 
					// 自定义key
					String key = ide.key();
					if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(PER_FIX_KEY.get())) 
						try 
//							redisService.unLock(PER_FIX_KEY.get());
							log.info("msg1=当前请求已成功释放,,key=", PER_FIX_KEY.get());
							PER_FIX_KEY.set(null);
							PER_FIX_KEY.remove();
						 catch (Exception e) 
							log.error("释放redis锁异常", e);
						
					
				
			
		 catch (Exception e) 
			log.error(e.getMessage(), e);
		
	

使用样例

package vip.mate.system.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import vip.mate.core.common.api.Result;
import vip.mate.core.ide.annotation.Ide;
import vip.mate.core.ide.enums.IdeTypeEnum;

/**
 * @author xuzhanfu
 */
@RestController
public class SysIdeTestController 

	@GetMapping("/ide/test")
	@Ide(perFix = "TEST_", key = "key", ideTypeEnum = IdeTypeEnum.KEY)
	public Result<?> test(@RequestParam String key) 
		return Result.data(key);
	


项目代码

以上是关于Spring Cloud项目如何防止重复提交,防重复提交幂等校验,Redis+aop+自定义Annotation实现接口的主要内容,如果未能解决你的问题,请参考以下文章

使用Redis实现接口防重复提交

使用Redis实现接口防重复提交

Spring Cloud Stream 如何防止应用程序的实例接收重复消息?

在winform当中提交数据,如何防止重复提交?

订单交易系统中的幂等设计

「SpringCloud」(三十九)使用分布式锁实现微服务重复请求控制