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

Posted 抓手

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式场景下接口的限流幂等防止重复提交相关的知识,希望对你有一定的参考价值。

简单实现

定义注解

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<String, Object> 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<Object> test1(@RequestBody Test1DTO dto) 
        return ApiResponse.success("成功");
    

    @Limiter
    @PostMapping("/test2")
    public ApiResponse<Object> 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<? extends LimiterKeyGetter> keyUsing() default DefaultLimiterKeyGetter.class;

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

以上是关于分布式场景下接口的限流幂等防止重复提交的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud项目如何防止重复提交,防重复提交幂等校验,Redis+aop+自定义Annotation实现接口

分布式锁不是控制并发幂等的方式

SpringBoot自定义注解+AOP+redis实现防接口幂等性重复提交,从概念到实战

分布式系统中的限流与熔断

接口设计之幂等性

高并发场景下的限流策略