【面试】解决前端重复请求导致数据出现重复问题,幂等性实现基于Redis,多端操作重复数据幂等问题,附代码

场景:

项目当中经常出现相同时间点的数据存入两条一模一样的数据,通常是由于页面或客户端网络延迟导致的重复提交导致的。这种情况,如果没有唯一业务字段的索引处理,就会导致重复数据出现在我们的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);
            }
        }
    }
}

下面是基于这种问题的前后端解决方案
思路大概是:

  1. 后端统一生成一个唯一token值,
  2. 前端在数据提交之前获取这个token值,数据提交的时候统一将这个token通过header传给后端,
  3. 后端先判断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));
	}
}

  • 如果对你有用,请给个在看,谢谢~~欢迎各位留言交流,
  • 如有不正确的地方,请予以指正。【W:编程心声】
  • 如有任何问题,关注公众号编程心声后,留言即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_夜半钟声到客船

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值