场景:
项目当中经常出现相同时间点的数据存入两条一模一样的数据,通常是由于页面或客户端网络延迟导致的重复提交导致的。这种情况,如果没有唯一业务字段的索引处理,就会导致重复数据出现在我们的MySQL数据表中。这时候只能手动删除掉重复的数据,以免影响业务正常使用。
下面是针对这种问题的后端解决方案。 方案不是最好的,但是能有效避免重复数据的产生。
传送门:幂等性问题的思考和总结,防重、幂等,常用解决方案,解决方式
实现方案:
根据请求信息进行md5计算,保存到Redis,有效期1小时;
下次请求再进来判断Redis是否存在?如果已存在,提示重复请求。
具体代码实现:
1、声明注解
/**
*
* 重复请求拦截注解
* @author xxxx
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface RepeatRequestInterceptor {
}
2、使用注解
/**
* 重复请求示例:
*/
@RepeatRequestIntercept
@ApiOperation(value = "申请")
@PostMapping("/applyRefund")
public RefundApplyVO applyRefund(@Validated @RequestBody RefundApplyDTO refundApplyDTO) {
do something...
return new RefundApplyVO(refundOrder.getId());
}
3、处理注解的统一切面类
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.common.utils.Md5Utils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
*
* 重复请求拦截注解统一拦截处理
* 这里用到了Redis,需要考虑Redis的高可用问题(暂时认为Redis是一直可用)
* @author xxxx
*/
@Aspect
@Component
@Slf4j
public class RepeatRequestAspect {
private static final String REPEAT_REQUEST_REDIS_KEY = "repeat_request:";
/**
* 重复请求缓存超时时间(单位秒)
*/
private static final Integer REPEAT_REQUEST_TIMEOUT = 3600;
@Resource
private RedisClient redisClient;
@Around("@annotation(com.xxxx.web.RepeatRequestInterceptor)")
public Object before(ProceedingJoinPoint joinPoint) throws Throwable {
Long userId = UserContext.getUserId();
Object[] args = joinPoint.getArgs();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String requestInfo = joinPoint.getTarget() + method.getName() + userId + userId + JSONObject.toJSONString(args);
String md5 = Md5Utils.getMD5(requestInfo.getBytes());
String lockKey = REPEAT_REQUEST_REDIS_KEY.concat(md5);
Boolean lockFlag = false;
try {
// Redis缓存1小时基于请求内容计算的md5值
lockFlag = redisClient.setNx(lockKey, md5, REPEAT_REQUEST_TIMEOUT);
if (lockFlag) {
return joinPoint.proceed();
} else {
throw new BizException(BaseResultCodeEnum.REPETITIVE_OPERATION);
}
} finally {
if (lockFlag) {
redisClient.delete(lockKey);
}
}
}
}
下面是基于这种问题的前后端解决方案
思路大概是:
- 后端统一生成一个唯一token值,
- 前端在数据提交之前获取这个token值,数据提交的时候统一将这个token通过header传给后端,
- 后端先判断token是否存在Redis中,存在则提示重复请求,不存在就将token保存到Redis中。
1、声明RepeatRequestIntercept注解
import java.lang.annotation.*;
/**
*
* 重复请求拦截注解
* @author xxxx
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.METHOD)
public @interface RepeatRequestIntercept {
}
2、统一拦截注解AOP切面处理
import com.xxxx.server.config.context.UserInfoContext;
import com.xxxx.server.exception.BizException;
import com.xxxx.server.pojo.Admin;
import com.xxxx.server.utils.CacheUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
*
* 重复请求拦截注解统一拦截处理
* @author xxxx
*/
@Slf4j
@Aspect
@Component
public class RepeatRequestAspect {
@Resource
private RedisTemplate redisTemplate;
@Pointcut("@annotation(com.xxxx.server.common.annotation.RepeatRequestIntercept)")//指向自定义注解路径
public void repeatRequestPointCut() {
}
@Around("repeatRequestPointCut()")
public Object before(ProceedingJoinPoint joinPoint) throws Throwable {
final Admin userInfo = UserInfoContext.getUserInfo();
// 获取header的repeatToken
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
String token = request.getHeader("repeat-token");
if (redisTemplate.opsForHash().hasKey(String.format(CacheUtils.REPEAT_TOKEN_HASH_KEY, 1),
String.format(CacheUtils.REPEAT_TOKEN_HASH_KEY, token))) {
throw new BizException("重复请求!");
}
return joinPoint.proceed();
}
}
3、CacheUtils
/**
* @author: xxxx
* @date: 2022/6/14
* @description:
*/
public class CacheUtils {
public static final String REPEAT_TOKEN_HASH_KEY = "hash:repeat:token:%s";
}
4、请求示例
/**
* <p>
* 部门控制器
* </p>
* @author xxxx
*/
@RestController
@RequestMapping("/system/department")
public class DepartmentController {
@Autowired
private IDepartmentService departmentService;
@ApiOperation(value = "添加部门")
@PostMapping("/add")
@RepeatRequestIntercept
public BaseResponse add(@RequestBody DepartmentDTO departmentDTO){
return BaseResponse.success(departmentService.add(departmentDTO));
}
}
-------------如果对你有用,请给个赞,谢谢~~
-------------欢迎各位留言交流,如有不正确的地方,请予以指正。【Q:981233589】