Current limiting, idempotence, and prevention of duplicate submissions for interfaces in distributed scenarios

Simple implementation

Definition Annotation

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 "请求频繁,请稍后重试";
}

define aspect

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();
    }
}

use

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("成功");
    }
}

Extension 1: How to obtain a custom restriction key

Define the limit key acquisition interface

import org.aspectj.lang.ProceedingJoinPoint;

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

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

Define the default restricted key acquisition class

Limit key = entry point + request parameter, you need to pay attention to the size of the request parameter to avoid too large 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();
    }
}

Restricted key acquisition class for alternate url + sessionId

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();
    }
}

Restricted key acquisition class for alternate sha1 processing

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;
    }
}

How to get the key

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

Extension 2: Return policy after custom restriction

Define return strategy enumeration class

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

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

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

 The implementation logic of the LAST_RESULT strategy:

Store the execution result and the limit key in redis, and then when it is judged that the limit is needed, take the execution result from redis and return it.

Extension 3: Provide retry rules

retry a certain number of times

When restricted, retry n times, and if it is still restricted after n times, no more retries will be made.

Wait for a certain amount of time and try again

Retry once after waiting for n seconds, if it is still restricted, no more retry.

Wait for a certain amount of time and retry a certain number of times

Retry n times after waiting for n seconds, if it is still restricted, no more retrying.

Extended Notes

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;
}

Guess you like

Origin blog.csdn.net/Anenan/article/details/127983154