Spring Cloud项目如何防止重复提交,防重复提交幂等校验,Redis+aop+自定义Annotation实现接口
Posted MateCloud微服务
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud项目如何防止重复提交,防重复提交幂等校验,Redis+aop+自定义Annotation实现接口相关的知识,希望对你有一定的参考价值。
需求说明
在项目开发过程,我们也会经常遇到这种问题,前端未拦截,或者拦截失败,导致后端接收到大量重复请求,结果把这些重复请求入库后,产生大量垃圾数据。
解释下幂等的概念:
任意多次执行所产生的影响均与一次执行的影响相同
那这个听起来是问题的解决之道,访如何实现,通常有如下方式:
- 建立数据库索引,保证最终插入的只有一条数据;
- token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token;
- 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
- 先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
幂等的原理图
实战演练
实战中对上述进行一些优化,支持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实现接口的主要内容,如果未能解决你的问题,请参考以下文章