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


    简单实现

    定义注解

    1. import java.lang.annotation.*;
    2. /**
    3. * @author 向振华
    4. * @date 2022/11/21 18:16
    5. */
    6. @Target(ElementType.METHOD)
    7. @Retention(RetentionPolicy.RUNTIME)
    8. @Documented
    9. public @interface Limiter {
    10. /**
    11. * 限制时间(秒)
    12. *
    13. * @return
    14. */
    15. long limitTime() default 2L;
    16. /**
    17. * 限制后的错误提示信息
    18. *
    19. * @return
    20. */
    21. String errorMessage() default "请求频繁,请稍后重试";
    22. }

    定义切面

    1. import com.alibaba.fastjson.JSONObject;
    2. import com.xzh.web.ApiResponse;
    3. import lombok.extern.slf4j.Slf4j;
    4. import org.aspectj.lang.ProceedingJoinPoint;
    5. import org.aspectj.lang.annotation.Around;
    6. import org.aspectj.lang.annotation.Aspect;
    7. import org.aspectj.lang.reflect.MethodSignature;
    8. import org.springframework.data.redis.core.RedisTemplate;
    9. import org.springframework.stereotype.Component;
    10. import javax.annotation.Resource;
    11. import java.lang.reflect.Method;
    12. import java.util.Arrays;
    13. import java.util.StringJoiner;
    14. import java.util.concurrent.TimeUnit;
    15. /**
    16. * @author 向振华
    17. * @date 2022/11/21 18:16
    18. */
    19. @Aspect
    20. @Component
    21. @Slf4j
    22. public class LimiterAspect {
    23. @Resource
    24. private RedisTemplate redisTemplate;
    25. @Around("@annotation(com.xzh.aop.Limiter)")
    26. public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    27. Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
    28. Limiter annotation = method.getAnnotation(Limiter.class);
    29. if (annotation != null) {
    30. // 获取限制key
    31. String limitKey = getKey(joinPoint);
    32. if (limitKey != null) {
    33. log.info("limitKey ---> " + limitKey);
    34. Boolean hasKey = redisTemplate.hasKey(limitKey);
    35. if (Boolean.TRUE.equals(hasKey)) {
    36. // 返回限制后的返回内容
    37. return ApiResponse.fail(annotation.errorMessage());
    38. } else {
    39. // 存入限制的key
    40. redisTemplate.opsForValue().set(limitKey, "", annotation.limitTime(), TimeUnit.SECONDS);
    41. }
    42. }
    43. }
    44. return joinPoint.proceed();
    45. }
    46. public String getKey(ProceedingJoinPoint joinPoint) {
    47. // 参数
    48. StringJoiner asj = new StringJoiner(",");
    49. Object[] args = joinPoint.getArgs();
    50. Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
    51. if (asj.toString().isEmpty()) {
    52. return null;
    53. }
    54. // 切入点
    55. String joinPointString = joinPoint.getSignature().toString();
    56. // 限制key = 切入点 + 参数
    57. return joinPointString + ":" + asj.toString();
    58. }
    59. }

    使用

    1. import com.xzh.web.ApiResponse;
    2. import com.xzh.aop.Limiter;
    3. import org.springframework.web.bind.annotation.PostMapping;
    4. import org.springframework.web.bind.annotation.RequestBody;
    5. import org.springframework.web.bind.annotation.RestController;
    6. /**
    7. * @author 向振华
    8. * @date 2021/11/21 18:03
    9. */
    10. @RestController
    11. public class TestController {
    12. @Limiter(limitTime = 10L)
    13. @PostMapping("/test1")
    14. public ApiResponse test1(@RequestBody Test1DTO dto) {
    15. return ApiResponse.success("成功");
    16. }
    17. @Limiter
    18. @PostMapping("/test2")
    19. public ApiResponse test2(Long id, String name) {
    20. return ApiResponse.success("成功");
    21. }
    22. }
    23. 扩展一:自定义限制key的获取方法

      定义限制key获取接口

      1. import org.aspectj.lang.ProceedingJoinPoint;
      2. /**
      3. * @author 向振华
      4. * @date 2022/11/21 18:22
      5. */
      6. public interface LimiterKeyGetter {
      7. /**
      8. * 获取限制key
      9. *
      10. * @param joinPoint
      11. * @return
      12. */
      13. String getKey(ProceedingJoinPoint joinPoint);
      14. }

      定义默认的限制key获取类

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

      1. import com.alibaba.fastjson.JSONObject;
      2. import org.aspectj.lang.ProceedingJoinPoint;
      3. import java.util.Arrays;
      4. import java.util.StringJoiner;
      5. /**
      6. * @author 向振华
      7. * @date 2022/11/22 13:39
      8. */
      9. public class DefaultLimiterKeyGetter implements LimiterKeyGetter {
      10. @Override
      11. public String getKey(ProceedingJoinPoint joinPoint) {
      12. // 参数
      13. StringJoiner asj = new StringJoiner(",");
      14. Object[] args = joinPoint.getArgs();
      15. Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
      16. if (asj.toString().isEmpty()) {
      17. return null;
      18. }
      19. // 切入点
      20. String joinPointString = joinPoint.getSignature().toString();
      21. // 限制key = 切入点 + 参数
      22. return joinPointString + ":" + asj.toString();
      23. }
      24. }

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

      1. import org.aspectj.lang.ProceedingJoinPoint;
      2. import org.springframework.web.context.request.RequestAttributes;
      3. import org.springframework.web.context.request.RequestContextHolder;
      4. import org.springframework.web.context.request.ServletRequestAttributes;
      5. import javax.servlet.http.HttpServletRequest;
      6. /**
      7. * @author 向振华
      8. * @date 2022/11/22 13:39
      9. */
      10. public class UrlSessionLimiterKeyGetter implements LimiterKeyGetter {
      11. @Override
      12. public String getKey(ProceedingJoinPoint joinPoint) {
      13. RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
      14. ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
      15. if (servletRequestAttributes == null) {
      16. return null;
      17. }
      18. HttpServletRequest request = servletRequestAttributes.getRequest();
      19. // 限制key = url + sessionId
      20. return request.getRequestURL() + ":" + request.getSession().getId();
      21. }
      22. }

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

      1. import cn.hutool.core.util.ObjectUtil;
      2. import cn.hutool.crypto.digest.DigestUtil;
      3. import com.alibaba.fastjson.JSONObject;
      4. import org.aspectj.lang.ProceedingJoinPoint;
      5. import java.util.Arrays;
      6. import java.util.StringJoiner;
      7. /**
      8. * @author 向振华
      9. * @date 2022/11/22 15:38
      10. */
      11. public class Sha1LimiterKeyGetter implements LimiterKeyGetter {
      12. @Override
      13. public String getKey(ProceedingJoinPoint joinPoint) {
      14. // 参数
      15. StringJoiner asj = new StringJoiner(",");
      16. Object[] args = joinPoint.getArgs();
      17. Arrays.stream(args).forEach(a -> asj.add(JSONObject.toJSONString(a)));
      18. if (asj.toString().isEmpty()) {
      19. return null;
      20. }
      21. // 序列号
      22. byte[] serialize = ObjectUtil.serialize(asj.toString().hashCode());
      23. // sha1处理
      24. String sha1 = DigestUtil.sha1Hex(serialize).toLowerCase();
      25. // 切入点
      26. String joinPointString = joinPoint.getSignature().toString();
      27. // 限制key = 切入点 + sha1值
      28. return joinPointString + ":" + sha1;
      29. }
      30. }

      key的获取方式

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

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

      定义返回策略枚举类

      1. /**
      2. * @author 向振华
      3. * @date 2022/11/22 15:50
      4. */
      5. public enum ReturnStrategy {
      6. /**
      7. * 返回错误提示信息
      8. */
      9. ERROR_MESSAGE,
      10. /**
      11. * 返回上次执行的结果
      12. */
      13. LAST_RESULT,
      14. }

       LAST_RESULT策略的实现逻辑:

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

      扩展三:提供重试规则

      重试一定次数

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

      等待一定时间后重试

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

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

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

      扩展后的注解

      1. import com.xzh.aop.key.DefaultLimiterKeyGetter;
      2. import com.xzh.aop.key.LimiterKeyGetter;
      3. import java.lang.annotation.*;
      4. /**
      5. * @author 向振华
      6. * @date 2022/11/21 18:16
      7. */
      8. @Target(ElementType.METHOD)
      9. @Retention(RetentionPolicy.RUNTIME)
      10. @Documented
      11. public @interface Limiter {
      12. /**
      13. * 限制时间(秒)
      14. *
      15. * @return
      16. */
      17. long limitTime() default 2L;
      18. /**
      19. * 限制后的错误提示信息
      20. *
      21. * @return
      22. */
      23. String errorMessage() default "请求频繁,请稍后重试";
      24. /**
      25. * 限制key获取类
      26. *
      27. * @return
      28. */
      29. Classextends LimiterKeyGetter> keyUsing() default DefaultLimiterKeyGetter.class;
      30. /**
      31. * 限制后的返回策略
      32. *
      33. * @return
      34. */
      35. ReturnStrategy returnStrategy() default ReturnStrategy.ERROR_MESSAGE;
      36. }

    24. 相关阅读:
      荧光素FITC标记单糖(丙糖、丁糖、戊糖、己糖、果糖、半乳糖、核糖和脱氧核糖)
      MongoDB 聚合查询详解
      C++ 引用的数组和数组的引用
      【数据结构】基础:队列(C语言)
      OpenGL 曝光度调节
      如何通过ChatGPT来快速写论文,怎么提问关键词?
      【HCIE】跨域MPLS-VPN Option C 方式一
      SSM框架整合
      view的context一定是Activity吗
      解决发邮件错误javax.mail.MessagingException: Could not connect to SMTP host
    25. 原文地址:https://blog.csdn.net/Anenan/article/details/127983154