背景

在一些业务场景中经常会出现一个请求还没执行完就有另一个相同的请求进入导致业务逻辑混乱的问题,对于这类问题可以使用分布式锁的手段根据业务的请求来判断是否相同来拦截,

java 使用 redis 解决幂等性 redis实现幂等_java

解决方案

于是本人参考网上的内容写了一个分布式锁的注解方式拦截。具体流程如下:

代码

总共三个文件结构如下:

java 使用 redis 解决幂等性 redis实现幂等_java 使用 redis 解决幂等性_02

先创建一个注释

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Target({ElementType.METHOD}) // 作用类型为方法
@Retention(RetentionPolicy.RUNTIME) // 作用时间为运行期间
@Documented
public @interface DistributionLock {

    // 锁定的redis键
    String value() default "default" ;

    // 锁定保持时间(以毫秒为单位)
    long keepMills() default 30000;

    // 时间单位
    TimeUnit timeUtil() default TimeUnit.MILLISECONDS;

    // 失败时执行的操作
    LockFailAction action() default LockFailAction.CONTINUE;

    // 失败时执行的操作--枚举
    enum LockFailAction{
        GIVEUP,
        CONTINUE;
    }

    // 重试的间隔
    long sleepMills() default 200;
    // 重试次数
    int retryTimes() default 5;
}

还有一个返回类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder // 此处使用了lombok插件 版本跟随springboot
public class CommonRes<T>{

    private String msg;

    private int code;

    private T data;
}

接下来就是对注解的拦截与实现,此处的实现原理是spring的aop实现一个切面拦截。

@Aspect // 定义切面
@Slf4j
@Configuration
public class LockImplAdvice {
	// 引入redisTemplate
    private final RedisTemplate<String, String> redisTemplate;

    public LockImplAdvice(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Pointcut("@annotation(DistributionLock)") // 定义切入点是注解
    public void lockAspect() {
    }

    @Around("lockAspect() && @annotation(distributionLock)")  // 此处写出@annotation可以在入参获取到注解
    public Object lockIt(ProceedingJoinPoint point, DistributionLock distributionLock) throws InterruptedException {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        String url = request.getRequestURL().toString();
        String sessionId = request.getRequestedSessionId();
        Object proceed = null;
        String preFix = "distributionLock:" + distributionLock.value() + ":";
        StringBuilder sb = new StringBuilder();
        Object[] args = point.getArgs();
        for (Object o : args) {
            // request 和 response 不能序列化 这里需要特别注意下
            if (o instanceof  HttpServletRequest || o instanceof HttpServletResponse){
                continue;
            }
            String s = JSON.toJSONString(o);
            sb.append(s);
        }
        sb.append(url).append(sessionId); // 拼接所有的字段
        String key = preFix + DigestUtils.sha1DigestAsHex(sb.toString());
        // 根据设定的值进行加锁
        Boolean setFlag = redisTemplate.opsForValue().setIfAbsent(key, "1", distributionLock.keepMills(), distributionLock.timeUtil());
        if (setFlag != null && !setFlag) {
            boolean failRequestFlag = true;
            // 加入重试机制
            if (distributionLock.action().equals(DistributionLock.LockFailAction.CONTINUE)) {
                int maxRetryTimes = distributionLock.retryTimes();
                int retryTime = 0;
                while (++retryTime <= maxRetryTimes && failRequestFlag) {
                    Thread.sleep(distributionLock.sleepMills()); // 如果重试则暂时休眠线程
                    Boolean setFlagV2 = redisTemplate.opsForValue().setIfAbsent(key, "1", distributionLock.keepMills(), distributionLock.timeUtil());
                    if (setFlagV2 != null) {
                        failRequestFlag = !setFlagV2;
                    }
                    log.debug("url: 【{}】第 【{}】次尝试  结果:{}",request.getRequestURL().toString(),retryTime,setFlagV2);
                }
            }
            if (failRequestFlag) {
            	// 如果多次获取执行锁失败 则提示请求请勿重复提交
                CommonRes<?> res = new CommonRes<>();
                res.setCode(90000);
                res.setMsg("请求已被锁定请稍后再试 key值 :" + key);
                return res;
            }
        }
        try {
            proceed = point.proceed(); // 执行程序
        } catch (Throwable e) {
            log.error("执行出错", e); // 如果有全局异常拦截 此处可以抛出异常 
        } finally {
            // 删除key
            redisTemplate.delete(key);
        }
        return proceed;
    }
}

结果测试

测试接口

@Slf4j
@RestController
@RequestMapping("/max")
public class TestController {

    @RequestMapping(value = {"/test"}, method = {RequestMethod.POST,RequestMethod.GET})
    @DistributionLock()
    public CommonRes<Void> test(HttpServletRequest request, String text) throws InterruptedException {
        log.info("收到请求  内容为 {} url为 {}",text,request.getRequestURI());
        CommonRes<Void> res = new CommonRes<>();
        TimeUnit.SECONDS.sleep(5); // 持续5秒
        res.setMsg(text);
        res.setCode(200);
        return res;
    }
}

对一次请求连续点击请求

java 使用 redis 解决幂等性 redis实现幂等_java_03

  • 修改重试的时间为3s每次,@DistributionLock(sleepMills = 3000)
  • 并修改日志级别为debug再次尝试

java 使用 redis 解决幂等性 redis实现幂等_分布式_04


java 使用 redis 解决幂等性 redis实现幂等_aop_05

  • 最终返回

对应环境

实现的环境配置:
Spring 5.2.7.RELEASE
SpringBoot 2.3.1.RELEASE
fastjson 1.1.46.sec10