问题:
在项目开发过程,我们也会经常遇到这种问题,前端未拦截,或者拦截失败,导致后端接收到大量重复请求,结果把这些重复请求入库后,产生大量垃圾数据。
- 数据不一致:如果一个接口被重复提交,可能会导致数据重复插入或更新,从而导致数据不一致。例如,用户提交了订单,但是因为网络延迟等原因,订单被重复提交了,这样就会在数据库中产生重复的订单数据。
- 资源占用:如果一个接口被重复提交,可能会导致不必要的资源占用,例如CPU、内存和数据库连接等。这可能会影响系统的性能和稳定性。
- 业务逻辑混乱:如果一个接口被重复提交,可能会导致业务逻辑的混乱。例如,在支付场景中,如果用户重复提交支付请求,可能会导致多次扣款,给用户带来不必要的损失。
- 系统故障:如果一个接口被重复提交的次数非常多,可能会导致系统崩溃或故障。这可能会对整个系统造成影响,甚至导致业务中断。
前端方式(自己做小项目可以用):
直接简单粗暴,使用计时器和状态禁止几秒内点击
<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即可