zoukankan      html  css  js  c++  java
  • 优雅解决分布式限流

    SpringBoot 是为了简化 Spring 应用的创建、运行、调试、部署等一系列问题而诞生的产物,自动装配的特性让我们可以更好的关注业务本身而不是外部的XML配置,我们只需遵循规范,引入相关的依赖就可以轻易的搭建出一个 WEB 工程

    在前面的两篇文章中,介绍了一些限流的类型和策略,本篇从 Spring BootRedis 应用层面来实现分布式的限流….

    分布式限流

    单机版中我们了解到 AtomicIntegerRateLimiterSemaphore 这几种解决方案,但它们也仅仅是单机的解决手段,在集群环境下就透心凉了,后面又讲述了 Nginx 的限流手段,可它又属于网关层面的策略之一,并不能解决所有问题。例如供短信接口,你无法保证消费方是否会做好限流控制,所以自己在应用层实现限流还是很有必要的。

    本章目标

    利用 自定义注解Spring AopRedis Cache 实现分布式限流….

    具体代码

    很简单…

    导入依赖

    在 pom.xml 中添加上 starter-webstarter-aopstarter-data-redis 的依赖即可,习惯了使用 commons-lang3 和 guava 中的一些工具包…

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <dependencies>
    <!-- 默认就内嵌了Tomcat 容器,如需要更换容器也极其简单-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>21.0</version>
    </dependency>
    <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    </dependencies>

    属性配置

    在 application.properites 资源文件中添加 redis 相关的配置项

    1
    2
    3
    spring.redis.host=localhost
    spring.redis.port=6379
    spring.redis.password=battcn

    Limit 注解

    创建一个 Limit 注解,不多说注释都给各位写齐全了….

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    package com.battcn.limiter.annotation;


    import com.battcn.limiter.LimitType;

    import java.lang.annotation.*;

    /**
    * 限流
    *
    * @author Levin
    * @since 2018-02-05
    */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Limit {

    /**
    * 资源的名字
    *
    * @return String
    */
    String name() default "";

    /**
    * 资源的key
    *
    * @return String
    */
    String key() default "";

    /**
    * Key的prefix
    *
    * @return String
    */
    String prefix() default "";

    /**
    * 给定的时间段
    * 单位秒
    *
    * @return int
    */
    int period();

    /**
    * 最多的访问限制次数
    *
    * @return int
    */
    int count();

    /**
    * 类型
    *
    * @return LimitType
    */
    LimitType limitType() default LimitType.CUSTOMER;
    }

    public enum LimitType {
    /**
    * 自定义key
    */
    CUSTOMER,
    /**
    * 根据请求者IP
    */
    IP;
    }

    RedisTemplate

    默认情况下 spring-boot-data-redis 为我们提供了StringRedisTemplate 但是满足不了其它类型的转换,所以还是得自己去定义其它类型的模板….

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    package com.battcn.limiter;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;

    import java.io.Serializable;

    /**
    * @author Levin
    * @since 2018/8/2 0002
    */
    @Configuration
    public class RedisLimiterHelper {

    @Bean
    public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
    RedisTemplate<String, Serializable> template = new RedisTemplate<>();
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.setConnectionFactory(redisConnectionFactory);
    return template;
    }
    }

    Limit 拦截器(AOP)

    熟悉 Redis 的朋友都知道它是线程安全的,我们利用它的特性可以实现分布式锁、分布式限流等组件,在一起来学SpringBoot | 第二十三篇:轻松搞定重复提交(分布式锁)中讲述了分布式锁的实现,限流相比它稍微复杂一点,官方虽然没有提供相应的API,但却提供了支持 Lua 脚本的功能,我们可以通过编写 Lua 脚本实现自己的API,同时他是满足原子性的….

    下面核心就是调用 execute 方法传入我们的 Lua 脚本内容,然后通过返回值判断是否超出我们预期的范围,超出则给出错误提示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    package com.battcn.limiter;


    import com.battcn.limiter.annotation.Limit;
    import com.google.common.collect.ImmutableList;
    import org.apache.commons.lang3.StringUtils;
    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.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.data.redis.core.script.RedisScript;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;

    import javax.servlet.http.HttpServletRequest;
    import java.io.Serializable;
    import java.lang.reflect.Method;

    /**
    * @author Levin
    * @since 2018/2/5 0005
    */
    @Aspect
    @Configuration
    public class LimitInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);

    private final RedisTemplate<String, Serializable> limitRedisTemplate;

    @Autowired
    public LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {
    this.limitRedisTemplate = limitRedisTemplate;
    }


    @Around("execution(public * *(..)) && @annotation(com.battcn.limiter.annotation.Limit)")
    public Object interceptor(ProceedingJoinPoint pjp) {
    MethodSignature signature = (MethodSignature) pjp.getSignature();
    Method method = signature.getMethod();
    Limit limitAnnotation = method.getAnnotation(Limit.class);
    LimitType limitType = limitAnnotation.limitType();
    String name = limitAnnotation.name();
    String key;
    int limitPeriod = limitAnnotation.period();
    int limitCount = limitAnnotation.count();
    switch (limitType) {
    case IP:
    key = getIpAddress();
    break;
    case CUSTOMER:
    // TODO 如果此处想根据表达式或者一些规则生成 请看 一起来学Spring Boot | 第二十三篇:轻松搞定重复提交(分布式锁)
    key = limitAnnotation.key();
    break;
    default:
    key = StringUtils.upperCase(method.getName());
    }
    ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));
    try {
    String luaScript = buildLuaScript();
    RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
    Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
    logger.info("Access try count is {} for name={} and key = {}", count, name, key);
    if (count != null && count.intValue() <= limitCount) {
    return pjp.proceed();
    } else {
    throw new RuntimeException("You have been dragged into the blacklist");
    }
    } catch (Throwable e) {
    if (e instanceof RuntimeException) {
    throw new RuntimeException(e.getLocalizedMessage());
    }
    throw new RuntimeException("server exception");
    }
    }

    /**
    * 限流 脚本
    *
    * @return lua脚本
    */
    public String buildLuaScript() {
    StringBuilder lua = new StringBuilder();
    lua.append("local c");
    lua.append(" c = redis.call('get',KEYS[1])");
    // 调用不超过最大值,则直接返回
    lua.append(" if c and tonumber(c) > tonumber(ARGV[1]) then");
    lua.append(" return c;");
    lua.append(" end");
    // 执行计算器自加
    lua.append(" c = redis.call('incr',KEYS[1])");
    lua.append(" if tonumber(c) == 1 then");
    // 从第一次调用开始限流,设置对应键值的过期
    lua.append(" redis.call('expire',KEYS[1],ARGV[2])");
    lua.append(" end");
    lua.append(" return c;");
    return lua.toString();
    }

    private static final String UNKNOWN = "unknown";

    public String getIpAddress() {
    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    String ip = request.getHeader("x-forwarded-for");
    if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
    ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
    ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
    ip = request.getRemoteAddr();
    }
    return ip;
    }
    }

    控制层

    在接口上添加 @Limit() 注解,如下代码会在 Redis 中生成过期时间为 100s 的 key = test 的记录,特意定义了一个 AtomicInteger 用作测试…

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.battcn.controller;

    import com.battcn.limiter.annotation.Limit;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.concurrent.atomic.AtomicInteger;

    /**
    * @author Levin
    * @since 2018/8/2 0002
    */
    @RestController
    public class LimiterController {

    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();

    @Limit(key = "test", period = 100, count = 10)
    @GetMapping("/test")
    public int testLimiter() {
    // 意味著 100S 内最多允許訪問10次
    return ATOMIC_INTEGER.incrementAndGet();
    }
    }

    主函数

    就一个普通的不能在普通的主函数类了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.battcn;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    /**
    * @author Levin
    */
    @SpringBootApplication
    public class Chapter27Application {

    public static void main(String[] args) {
    SpringApplication.run(Chapter27Application.class, args);
    }
    }

    测试

    完成准备事项后,启动 Chapter27Application 自行测试即可,测试手段相信大伙都不陌生了,如 浏览器postmanjunitswagger,此处基于 postman,如果你觉得自带的异常信息不够友好,那么配上一起来学SpringBoot | 第十八篇:轻松搞定全局异常 可以轻松搞定…

    未达设定的阀值时

    正确响应正确响应

    达到设置的阀值时

    错误响应错误响应

  • 相关阅读:
    静态绑定与动态绑定
    Java之浅拷贝与深拷贝
    python类对象及类实例的创建过程
    以订单和商品为例进行详细的组内数据获取的分析
    根据给定时间及偏移的年份求偏移后时间的前一天(支持偏移量为正和负)
    mysql取到组内的前几条数据
    python进程通信的几种实现方式
    python-redis之数据类型二
    python-redis之数据类型
    python-redis
  • 原文地址:https://www.cnblogs.com/lywJ/p/10715367.html
Copyright © 2011-2022 走看看