分布式场景下接口的限流、幂等、防止重复提交

简单实现

定义注解

import java.lang.annotation.*;

/**
 * @author 向振华
 * @date 2022/11/21 18:16
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limiter {

    /**
     * 限制时间(秒)
     *
     * @return
     */
    long limitTime() default 2L;

    /**
     * 限制后的错误提示信息
     *
     * @return
     */
    String errorMessage() default "请求频繁,请稍后重试";
}

定义切面

import com.alibaba.fastjson.JSONObject;
import com.xzh.web.ApiResponse;
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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;

/**
 * @author 向振华
 * @date 2022/11/21 18:16
 */
@Aspect
@Component
@Slf4j
public class LimiterAspect {

    @Resource
    private RedisTemplate redisTemplate;

    @Around("@annotation(com.xzh.aop.Limiter)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Limiter annotation = method.getAnnotation(Limiter.class);
        if (annotation != null) {
            // 获取限制key
            String limitKey = getKey(joinPoint);

            if (limitKey != null) {
                log.info("limitKey ---> " + limitKey);
                Boolean hasKey = redisTemplate.hasKey(limitKey);
                if (Boolean.TRUE.equals(hasKey)) {
                    // 返回限制后的返回内容
                    return ApiResponse.fail(annotation.errorMessage());
                } else {
                    // 存入限制的key
                    redisTemplate.opsForValue().set(limitKey, "", annotation.limitTime(), TimeUnit.SECONDS);
                }
            }
        }

        return joinPoint.proceed();
    }

    public String getKey(ProceedingJoinPoint joinPoint) {
        // 参数
        StringJoiner asj = new StringJoiner(",");
        Object[] args = joinPoint.getArgs();
        Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
        if (asj.toString().isEmpty()) {
            return null;
        }
        // 切入点
        String joinPointString = joinPoint.getSignature().toString();
        // 限制key = 切入点 + 参数
        return joinPointString + ":" + asj.toString();
    }
}

使用

import com.xzh.web.ApiResponse;
import com.xzh.aop.Limiter;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 向振华
 * @date 2021/11/21 18:03
 */
@RestController
public class TestController {

    @Limiter(limitTime = 10L)
    @PostMapping("/test1")
    public ApiResponse test1(@RequestBody Test1DTO dto) {
        return ApiResponse.success("成功");
    }

    @Limiter
    @PostMapping("/test2")
    public ApiResponse test2(Long id, String name) {
        return ApiResponse.success("成功");
    }
} 
  

扩展一:自定义限制key的获取方法

定义限制key获取接口

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * @author 向振华
 * @date 2022/11/21 18:22
 */
public interface LimiterKeyGetter {

    /**
     * 获取限制key
     *
     * @param joinPoint
     * @return
     */
    String getKey(ProceedingJoinPoint joinPoint);
}

定义默认的限制key获取类

限制key = 切入点 + 请求参数,需要注意请求参数的大小,避免redis key过大。

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;

import java.util.Arrays;
import java.util.StringJoiner;

/**
 * @author 向振华
 * @date 2022/11/22 13:39
 */
public class DefaultLimiterKeyGetter implements LimiterKeyGetter {

    @Override
    public String getKey(ProceedingJoinPoint joinPoint) {
        // 参数
        StringJoiner asj = new StringJoiner(",");
        Object[] args = joinPoint.getArgs();
        Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
        if (asj.toString().isEmpty()) {
            return null;
        }
        // 切入点
        String joinPointString = joinPoint.getSignature().toString();
        // 限制key = 切入点 + 参数
        return joinPointString + ":" + asj.toString();
    }
}

备用url + sessionId的限制key获取类

import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @author 向振华
 * @date 2022/11/22 13:39
 */
public class UrlSessionLimiterKeyGetter implements LimiterKeyGetter {

    @Override
    public String getKey(ProceedingJoinPoint joinPoint) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        if (servletRequestAttributes == null) {
            return null;
        }
        HttpServletRequest request = servletRequestAttributes.getRequest();
        // 限制key = url + sessionId
        return request.getRequestURL() + ":" + request.getSession().getId();
    }
}

备用sha1处理的限制key获取类

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;

import java.util.Arrays;
import java.util.StringJoiner;

/**
 * @author 向振华
 * @date 2022/11/22 15:38
 */
public class Sha1LimiterKeyGetter implements LimiterKeyGetter {

    @Override
    public String getKey(ProceedingJoinPoint joinPoint) {
        // 参数
        StringJoiner asj = new StringJoiner(",");
        Object[] args = joinPoint.getArgs();
        Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
        if (asj.toString().isEmpty()) {
            return null;
        }
        // 序列号
        byte[] serialize = ObjectUtil.serialize(asj.toString().hashCode());
        // sha1处理
        String sha1 = DigestUtil.sha1Hex(serialize).toLowerCase();
        // 切入点
        String joinPointString = joinPoint.getSignature().toString();
        // 限制key = 切入点 + sha1值
        return joinPointString + ":" + sha1;
    }
}

key的获取方式

            // 获取限制key
            String limitKey = null;
            try {
                limitKey = annotation.keyUsing().newInstance().getKey(joinPoint);
            } catch (Exception ignored) {
            }

扩展二:自定义限制后的返回策略

定义返回策略枚举类

/**
 * @author 向振华
 * @date 2022/11/22 15:50
 */
public enum ReturnStrategy {

    /**
     * 返回错误提示信息
     */
    ERROR_MESSAGE,

    /**
     * 返回上次执行的结果
     */
    LAST_RESULT,
}

 LAST_RESULT策略的实现逻辑:

将执行结果和限制key一起存入redis,然后判断需要限制时,从redis取出执行结果并返回出去。

扩展三:提供重试规则

重试一定次数

在被限制时,重试n次,n次后如果依然被限制,则不再重试。

等待一定时间后重试

等待n秒后重试1次,如果依然被限制,则不再重试。

等待一定时间后重试一定次数

等待n秒后重试n次,如果依然被限制,则不再重试。

扩展后的注解

import com.xzh.aop.key.DefaultLimiterKeyGetter;
import com.xzh.aop.key.LimiterKeyGetter;

import java.lang.annotation.*;

/**
 * @author 向振华
 * @date 2022/11/21 18:16
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limiter {

    /**
     * 限制时间(秒)
     *
     * @return
     */
    long limitTime() default 2L;

    /**
     * 限制后的错误提示信息
     *
     * @return
     */
    String errorMessage() default "请求频繁,请稍后重试";

    /**
     * 限制key获取类
     *
     * @return
     */
    Class keyUsing() default DefaultLimiterKeyGetter.class;

    /**
     * 限制后的返回策略
     *
     * @return
     */
    ReturnStrategy returnStrategy() default ReturnStrategy.ERROR_MESSAGE;
}

你可能感兴趣的:(架构设计,分布式,java,开发语言,幂等)