Spring Cloud项目如何防止重复提交(自定义注解)

本文讨论了项目开发中前端拦截失败导致的重复请求问题,介绍了前端使用计时器限制点击和后端通过Redis+AOP实现的防止重复提交的方法,以及这些措施对数据一致性、资源利用和业务逻辑的影响。
摘要由CSDN通过智能技术生成

问题:

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

  1. 数据不一致:如果一个接口被重复提交,可能会导致数据重复插入或更新,从而导致数据不一致。例如,用户提交了订单,但是因为网络延迟等原因,订单被重复提交了,这样就会在数据库中产生重复的订单数据。
  2. 资源占用:如果一个接口被重复提交,可能会导致不必要的资源占用,例如CPU、内存和数据库连接等。这可能会影响系统的性能和稳定性。
  3. 业务逻辑混乱:如果一个接口被重复提交,可能会导致业务逻辑的混乱。例如,在支付场景中,如果用户重复提交支付请求,可能会导致多次扣款,给用户带来不必要的损失。
  4. 系统故障:如果一个接口被重复提交的次数非常多,可能会导致系统崩溃或故障。这可能会对整个系统造成影响,甚至导致业务中断。

前端方式(自己做小项目可以用):

直接简单粗暴,使用计时器和状态禁止几秒内点击

<template>  
  <button @click="handleClick" :disabled="isClickDisabled">点击我</button>  
</template>  
  
<script>  
export default {  
  data() {  
    return {  
      isClickDisabled: false,  
      lastClickTime: 0,  
    };  
  },  
  methods: {  
    handleClick() {  
      const now = Date.now();  
      // 设置三秒内禁止重复点击按钮
      if (now - this.lastClickTime < 3000) {  
        this.isClickDisabled = true;  
        setTimeout(() => {  
          this.isClickDisabled = false;  
        }, 3000);  
      } else {  
        this.lastClickTime = now;  
        // 在这里执行点击事件的逻辑  
      }  
    },  
  },  
};  
</script>

后端(推荐):

使用Redis+Aop实现,结合 Redis 来实现这个功能,我们将用户的请求信息存储在缓存中,这样就可以实时跟踪用户请求的状态,同时也可以提高系统的性能。为了限制用户在短时间内重复提交相同的请求,我们可以设置一个时间间隔来限制重复提交

@Aspect
@Component
public class NoRepeatSubmitAspect {
 
	@Autowired
	private RedisUtils redisUtils;

	@Pointcut("@annotation(com.syzh.nrs.annotation.NoRepeatSubmit)")
    public void noRepeatSubmitPointcut() {}
 
    @Before("noRepeatSubmitPointcut() && @annotation(noRepeatSubmit)")
    public void before(JoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) {
        try {
        	HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        	/**
        	 * 	istimestamp为true表示需要从headers中获取timestamp参数值,然后与系统时间比较,如果误差超过timestampmax值,那么抛出异常;
        	 */
        	if (noRepeatSubmit.istimestamp()) {
    			String timestamp = request.getHeader("timestamp");
    			if (StringUtils.isBlank(timestamp)) {
    				throw new BusinessException("获取Headers参数timestamp失败");
    			}
    			long result = Math.abs(System.currentTimeMillis() - Long.valueOf(timestamp).longValue());
    			if (result > noRepeatSubmit.timestampmax()) {
    				throw new BusinessException("请检查Headers参数timestamp的取值范围,参考:0~"+ noRepeatSubmit.timestampmax() + "毫秒");
    			}
        	}
        	/**
             * 	如果缓存中有这个url视为重复提交
             */
            String sessionId = request.getSession().getId();
            String key = BaseEnumRedis.NoRepeatSubmit.nrc.getNo() + sessionId + ":" + request.getServletPath();
            /**
             * 	setIfAbsent如果键不存在则新增(返回true),存在则不改变已经有的值(返回false)
             */
            Boolean flag = redisUtils.getValueOps().setIfAbsent(key, "0", noRepeatSubmit.timeout(), TimeUnit.SECONDS);
            if (!flag.booleanValue()) {
            	throw new BusinessException("地址请求过于频繁,请稍后重试...");
            }
        } catch (BusinessException e) {
			throw e;
		} catch (Exception e) {
			SysLogUtils.printLogger(e);
			throw new BusinessException("验证重复提交时,出现未知异常", e);
		}
    }
    
    @AfterReturning("noRepeatSubmitPointcut() && @annotation(noRepeatSubmit)")
    public void doAfterReturning(JoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) {
    	try {
    		if (noRepeatSubmit.isdelete()) {
        		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                String sessionId = request.getSession().getId();
                String key = BaseEnumRedis.NoRepeatSubmit.nrc.getNo() + sessionId + ":" + request.getServletPath();
                redisUtils.getRedisTemplate().delete(key);
    		}
    	} catch (Exception e) {
			SysLogUtils.printLogger(e);
			throw new BusinessException("验证重复提交时,出现未知异常", e);
		}
    }
    
    @AfterThrowing(pointcut = "noRepeatSubmitPointcut() && @annotation(noRepeatSubmit)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit, Throwable e) {
    	try {
    		if (noRepeatSubmit.isdelete()) {
        		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
                String sessionId = request.getSession().getId();
                String key = BaseEnumRedis.NoRepeatSubmit.nrc.getNo() + sessionId + ":" + request.getServletPath();
                redisUtils.getRedisTemplate().delete(key);
    		}
    	} catch (Exception ex) {
			SysLogUtils.printLogger(ex);
			/**
			 * 	这里抛出时,取e的信息还是比较合理的
			 */
			throw new BusinessException("验证重复提交时,出现未知异常", e);
		}
    }
    
}

Redis工具类

public class RedisUtils {

	private RedisTemplate<String, String> redisTemplate;
	private ValueOperations<String, String> valueOps;
	private SetOperations<String, String> setOps;
	private ZSetOperations<String, String> zSetOps;
	private HashOperations<String, String, String> hashOps;
	private ListOperations<String, String> listOps;

	public void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
		this.redisTemplate = redisTemplate;
		this.valueOps = redisTemplate.opsForValue();
		this.setOps = redisTemplate.opsForSet();
		this.zSetOps = redisTemplate.opsForZSet();
		this.hashOps = redisTemplate.opsForHash();
		this.listOps = redisTemplate.opsForList();
	}

	public RedisTemplate<String, String> getRedisTemplate() {
		return redisTemplate;
	}

	public ValueOperations<String, String> getValueOps() {
		return valueOps;
	}

	public SetOperations<String, String> getSetOps() {
		return setOps;
	}

	public ZSetOperations<String, String> getzSetOps() {
		return zSetOps;
	}

	public HashOperations<String, String, String> getHashOps() {
		return hashOps;
	}

	public ListOperations<String, String> getListOps() {
		return listOps;
	}

}

使用时直接在方法上使用@NoRepeatSubmit即可

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值