自定义redission装配和集成分布式开源限流业务组件ratelimiter-spring-boot-starter的正确姿势

自定义redission装配和集成分布式开源限流业务组件ratelimiter-spring-boot-starter的正确姿势

文章目录

  • 1.说明
    • [1.1 pom依赖](#1.1 pom依赖)
    • [1.2 引入redisson不引入redisson-spring-boot-starter依赖](#1.2 引入redisson不引入redisson-spring-boot-starter依赖)
    • [1.3 引入redisson-spring-boot-starter不引入redisson,启动类排除redisson-spring-boot-starter的自动装配](#1.3 引入redisson-spring-boot-starter不引入redisson,启动类排除redisson-spring-boot-starter的自动装配)
  • 2.自定义redission装配
    • [2.1 RedissonLockProperties](#2.1 RedissonLockProperties)
    • [2.2 RedissonLockAutoConfiguration](#2.2 RedissonLockAutoConfiguration)
    • [2.4 RedisConfig](#2.4 RedisConfig)
    • [2.3 nacos配置](#2.3 nacos配置)
  • 3.集成分布式开源限流组件ratelimiter-spring-boot-starter
    • [3.1 引入依赖](#3.1 引入依赖)
    • [3.2 nacos配置](#3.2 nacos配置)
    • [3.3 基础使用](#3.3 基础使用)
      • [3.3.1 在需要加限流逻辑的方法上,添加注解 @RateLimit](#3.3.1 在需要加限流逻辑的方法上,添加注解 @RateLimit)
      • [3.3.2 @RateLimit 注解说明](#3.3.2 @RateLimit 注解说明)
      • [3.3.3 限流的粒度,限流 key](#3.3.3 限流的粒度,限流 key)
      • [3.3.4 触发限流后的行为](#3.3.4 触发限流后的行为)
    • [3.4 进阶用法](#3.4 进阶用法)
      • [3.4.1 自定义限流的 key](#3.4.1 自定义限流的 key)
        • [3.4.1.1 @RateLimitKey 的方式](#3.4.1.1 @RateLimitKey 的方式)
        • [3.4.1.2 指定 keys 的方式](#3.4.1.2 指定 keys 的方式)
        • [3.4.1.3 自定义 key 获取函数](#3.4.1.3 自定义 key 获取函数)
      • [3.4.2 自定义限流后的行为](#3.4.2 自定义限流后的行为)
        • [3.4.2.1 配置响应内容](#3.4.2.1 配置响应内容)
        • [3.4.2.2 自定义限流触发异常处理器](#3.4.2.2 自定义限流触发异常处理器)
        • [3.4.2.3 自定义触发限流处理函数,限流降级](#3.4.2.3 自定义触发限流处理函数,限流降级)
      • [3.4.3 动态设置限流大小](#3.4.3 动态设置限流大小)
        • [3.4.3.1 rateExpression 的使用](#3.4.3.1 rateExpression 的使用)
    • [3.5 直接使用限流器服务-RateLimiterService](#3.5 直接使用限流器服务-RateLimiterService)
    • 3.6压力测试
    • 3.7版本更新
  • 4.总结

1.说明

1.1 pom依赖

xml 复制代码
<dependency>
  <groupId>com.github.taptap</groupId>
  <artifactId>ratelimiter-spring-boot-starter</artifactId>
  <version>1.3</version>
</dependency>

<dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>3.3.0</version>
</dependency>

 <dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson-spring-boot-starter</artifactId>
   <version>${redisson.version}</version>
 </dependency>

由于使用了redisson-spring-boot-starter,在自定义redisson装配的时候会被redisson-spring-boot-starter里面的start默认装配了,同时在使用开源分布式限流组件ratelimiter-spring-boot-starter的时候,这个里面也会自动装配一个redisson,所以就会产生冲突,容器中会有2个redisson的bean从而导致报错,所以解决办法是移除redisson-spring-boot-starter的依赖,加入redisson的依赖,或者不加redisson的依赖,redisson-spring-boot-starter里面包含了redisson-spring-boot-starter的依赖,是在启动类上将redisson-spring-boot-starter的start排除:

1.2 引入redisson不引入redisson-spring-boot-starter依赖

xml 复制代码
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.13.14</version>
</dependency>

redisson

https://github.com/redisson/redisson#quick-start

1.3 引入redisson-spring-boot-starter不引入redisson,启动类排除redisson-spring-boot-starter的自动装配

java 复制代码
@SpringBootApplication(exclude = {
        RedissonAutoConfiguration.class})
@EnableTransactionManagement
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

此时启动容器中还是会有2个redisson的bean,所以需要自定义装配一个,然后加上@Primary为主的redisson

2.自定义redission装配

2.1 RedissonLockProperties

java 复制代码
package xxx.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "redisson.lock.config")
public class RedissonLockProperties {

    private String address;

    private String password;

    /**
     * 1.single
     * 2.master
     * 3.sentinel
     * 4.cluster
     */
    private int mode = 1;

    /**
     * 在master模式下需配置这个
     */
    private String masterAddress;

    /**
     * 在master模式下需配置这个
     */
    private String[] slaveAddress;

    /**
     * 在sentinel模式下需配置这个
     */
    private String masterName;

    /**
     * 在sentinel模式下需配置这个
     */
    private String[] sentinelAddress;

    /**
     * 在cluster模式下需配置这个
     */
    private String[] nodeAddress;

    private int database = 5;

    private int poolSize = 64;

    private int idleSize = 24;

    private int connectionTimeout = 10000;

    private int timeout = 3000;

}

2.2 RedissonLockAutoConfiguration

java 复制代码
package xx.config;

import jodd.util.StringUtil;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.redisson.config.MasterSlaveServersConfig;
import org.redisson.config.SentinelServersConfig;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * 分布式锁自动化配置
 *
 * @author zlf
 */
@Configuration
@ConditionalOnClass(RedissonClient.class)
@EnableConfigurationProperties(RedissonLockProperties.class)
@ConditionalOnProperty(value = "redisson.lock.enabled", havingValue = "true")
public class RedissonLockAutoConfiguration {

    private static Config singleConfig(RedissonLockProperties properties) {
        Config config = new Config();
        SingleServerConfig serversConfig = config.useSingleServer();
        serversConfig.setAddress(properties.getAddress());
        String password = properties.getPassword();
        if (StringUtil.isNotBlank(password)) {
            serversConfig.setPassword(password);
        }
        serversConfig.setDatabase(properties.getDatabase());
        serversConfig.setConnectionPoolSize(properties.getPoolSize());
        serversConfig.setConnectionMinimumIdleSize(properties.getIdleSize());
        serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
        serversConfig.setConnectTimeout(properties.getConnectionTimeout());
        serversConfig.setTimeout(properties.getTimeout());
        return config;
    }

    private static Config masterSlaveConfig(RedissonLockProperties properties) {
        Config config = new Config();
        MasterSlaveServersConfig serversConfig = config.useMasterSlaveServers();
        serversConfig.setMasterAddress(properties.getMasterAddress());
        serversConfig.addSlaveAddress(properties.getSlaveAddress());
        String password = properties.getPassword();
        if (StringUtil.isNotBlank(password)) {
            serversConfig.setPassword(password);
        }
        serversConfig.setDatabase(properties.getDatabase());
        serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
        serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
        serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
        serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
        serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
        serversConfig.setConnectTimeout(properties.getConnectionTimeout());
        serversConfig.setTimeout(properties.getTimeout());
        return config;
    }

    private static Config sentinelConfig(RedissonLockProperties properties) {
        Config config = new Config();
        SentinelServersConfig serversConfig = config.useSentinelServers();
        serversConfig.setMasterName(properties.getMasterName());
        serversConfig.addSentinelAddress(properties.getSentinelAddress());
        String password = properties.getPassword();
        if (StringUtil.isNotBlank(password)) {
            serversConfig.setPassword(password);
        }
        serversConfig.setDatabase(properties.getDatabase());
        serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
        serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
        serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
        serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
        serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
        serversConfig.setConnectTimeout(properties.getConnectionTimeout());
        serversConfig.setTimeout(properties.getTimeout());
        return config;
    }

    private static Config clusterConfig(RedissonLockProperties properties) {
        Config config = new Config();
        ClusterServersConfig serversConfig = config.useClusterServers();
        serversConfig.addNodeAddress(properties.getNodeAddress());
        String password = properties.getPassword();
        if (StringUtil.isNotBlank(password)) {
            serversConfig.setPassword(password);
        }
        serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
        serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
        serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
        serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
        serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
        serversConfig.setConnectTimeout(properties.getConnectionTimeout());
        serversConfig.setTimeout(properties.getTimeout());
        return config;
    }

    @Bean
    @Primary
    public RedissonClient redissonClient(RedissonLockProperties properties) {
        int mode = properties.getMode();
        Config config = null;
        switch (mode) {
            case 1:
                config = singleConfig(properties);
                return Redisson.create(config);
            case 2:
                config = masterSlaveConfig(properties);
                return Redisson.create(config);
            case 3:
                config = sentinelConfig(properties);
                return Redisson.create(config);
            case 4:
                config = clusterConfig(properties);
                return Redisson.create(config);
        }
        return null;
    }

}

2.4 RedisConfig

这里采用jedis的连接池工厂来装配一个redisTemplateLimit,这个是上一篇文章中的一个配置,这里需要修改一下的,不然有可能会报错的

自定义注解实现Redis分布式锁、手动控制事务和根据异常名字或内容限流的三合一的功能

https://mp.weixin.qq.com/s/aW4PU_wlNVfzPc6uGFnndA
java 复制代码
package xxx.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;

@Slf4j
@RefreshScope
@Component
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.database}")
    private String database;

    @Value("${spring.redis.jedis.pool.max-active}")
    private String maxActive;
    @Value("${spring.redis.jedis.pool.max-idle}")
    private String maxIdle;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private String minIdle;

    //RedisConnectionFactory是这个spring-boot-starter-data-redis中的redis的连接工厂,如果不用jedis需要引入spring-boot-starter-data-redis即可,默认redisson-spring-boot-starter里面有这个依赖,如果没有redisson-spring-boot-starter需要引入spring-boot-starter-data-redis可以使用的
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 定义泛型为 <String, Object> 的 RedisTemplate
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 设置连接工厂
        template.setConnectionFactory(factory);
        // 定义 Json 序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        // Json 转换工具
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //方法二:解决jackson2无法反序列化LocalDateTime的问题
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModule(new JavaTimeModule());
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 定义 String 序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    JedisPool redisPoolFactory() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(Integer.valueOf(maxActive).intValue());
        jedisPoolConfig.setMaxIdle(Integer.valueOf(maxIdle).intValue());
        jedisPoolConfig.setMinIdle(Integer.valueOf(minIdle).intValue());
        JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, Integer.valueOf(port).intValue(), Protocol.DEFAULT_TIMEOUT, password, database);
        log.info("JedisPool注入成功!!");
        log.info("redis地址:" + host + ":" + port);
        return jedisPool;
    }

    @Bean
    RedisTemplate<String, Long> redisTemplateLimit(JedisConnectionFactory factory) {
        final RedisTemplate<String, Long> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericToStringSerializer<>(Long.class));
        template.setValueSerializer(new GenericToStringSerializer<>(Long.class));
        return template;
    }

    //springboot报错:Could not resolve placeholder 'xxx' in value "${XXXX}
    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
        PropertySourcesPlaceholderConfigurer placeholderConfigurer = new PropertySourcesPlaceholderConfigurer();
        placeholderConfigurer.setIgnoreUnresolvablePlaceholders(true);
        return placeholderConfigurer;
    }

}

2.3 nacos配置

yaml 复制代码
spring:
  redis:
    host: xxx
    port: 6379
    password: xxxx
    database: 5
    # jedis配置
    jedis:
      pool:
        max-active: 200
        max-idle: 20
        max-wait: 2000
        min-idle: 5
    lettuce:
      shutdown-timeout: 0ms
redisson:
  lock:
    enabled: true
    config:
      address: redis://xxx:6379
      password: xxxx

3.集成分布式开源限流组件ratelimiter-spring-boot-starter

ratelimiter-spring-boot-starter

https://github.com/TapTap/ratelimiter-spring-boot-starter#ratelimiter-spring-boot-starter

3.1 引入依赖

maven

xml 复制代码
<dependency>
  <groupId>com.github.taptap</groupId>
  <artifactId>ratelimiter-spring-boot-starter</artifactId>
  <version>1.3</version>
</dependency>

gradle

groovy 复制代码
implementation 'com.github.taptap:ratelimiter-spring-boot-starter:1.3'

3.2 nacos配置

yaml 复制代码
spring:
  ratelimiter:
    enabled: true
    redis-address: redis://xxx:6379
    redis-password: xxxx
    response-body: "您请求的太快了,请慢点,不然会有点受不了哦!"
    status-code: 500

3.3 基础使用

3.3.1 在需要加限流逻辑的方法上,添加注解 @RateLimit

如下所示:

java 复制代码
@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s")
    public String get(String name) {
        return "hello";
    }
}

3.3.2 @RateLimit 注解说明

属性 单位 默认值 是否必填 描述
mode enum(TIME_WINDOW/TOKEN_BUCKET) TIME_WINDOW 限流模式,目前可选时间窗口和令牌桶
rate int 时间窗口模式表示每个时间窗口内的请求数量、令牌桶模式表示每秒的令牌生产数量
rateInterval String 1s 用于时间窗口模式,表示时间窗口
rateExpression String 通过 EL 表达式从 Spring Config 上下文中获取 rate 的值,rateExpression 的优先级比 rate
fallbackFunction String 自定义触发限流时的降级策略方法,默认触发限流会抛 RateLimitException 异常
customKeyFunction String 自定义获取限流 key 的方法
bucketCapacity int 用于令牌桶模式,表示令牌桶的桶的大小,这个参数控制了请求最大并发数
bucketCapacityExpression String 通过 EL 表达式从 Spring Config 上下文中获取 bucketCapacity 的值,bucketCapacityExpression 的优先级比 bucketCapacity
requestedTokens int 1 用于令牌桶模式,表示每次获取的令牌数,一般不用改动这个参数值,除非你知道你在干嘛

@RateLimit 注解可以添加到任意被 spring 管理的 bean 上,不局限于 controller ,service 、repository 也可以。在最基础限流功能使用上,以上三个步骤就已经完成了。

3.3.3 限流的粒度,限流 key

限流的粒度是通过限流的 key 来做的,在最基础的设置下,限流的 key 默认是通过方法名称拼出来的,规则如下:

key=RateLimiter_ + 类名 + 方法名

除了默认的 key 策略,ratelimiter-spring-boot-starter 充分考虑了业务限流时的复杂性,提供了多种方式。结合业务特征,达到更细粒度的限流控制。

3.3.4 触发限流后的行为

默认触发限流后 程序会返回一个 http 状态码为 429 的响应,响应值如下:

json 复制代码
{
  "code": 429,
  "msg": "Too Many Requests"
}

同时,响应的 header 里会携带一个 Retry-After 的时间值,单位 s,用来告诉调用方多久后可以重试。当然这一切都是可以自定义的,进阶用法可以继续往下看

3.4 进阶用法

3.4.1 自定义限流的 key

自定义限流 key 有三种方式,当自定义限流的 key 生效时,限流的 key 就变成了(默认的 key + 自定义的 key)。下面依次给出示例

3.4.1.1 @RateLimitKey 的方式
java 复制代码
@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s")
    public String get(@RateLimitKey String name) {
        return "get";
    }
}

@RateLimitKey 注解可以放在方法的入参上,要求入参是基础数据类型,上面的例子,如果 name = kl。那么最终限流的 key 如下:

properties 复制代码
key=RateLimiter_com.taptap.ratelimiter.web.TestController.get-kl
3.4.1.2 指定 keys 的方式
java 复制代码
@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s", keys = {"#name"})
    public String get(String name) {
        return "get";
    }

    @GetMapping("/hello")
    @RateLimit(rate = 5, rateInterval = "10s", keys = {"#user.name", "user.id"})
    public String hello(User user) {
        return "hello";
    }
}

keys 这个参数比 @RateLimitKey 注解更智能,基本可以包含 @RateLimitKey 的能力,只是简单场景下,使用起来没有 @RateLimitKey 那么便捷。keys 的语法来自 spring 的 Spel ,可以获取对象入参里的属性,支持获取多个,最后会拼接起来。使用过 spring-cache 的同学可能会更加熟悉 如果不清楚 Spel 的用法,可以参考 spring-cache 的注解文档

3.4.1.3 自定义 key 获取函数
java 复制代码
@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s", customKeyFunction = "keyFunction")
    public String get(String name) {
        return "get";
    }

    public String keyFunction(String name) {
        return "keyFunction" + name;
    }
}

当 @RateLimitKey 和 keys 参数都没法满足时,比如入参的值是一个加密的值,需要解密后根据相关明文内容限流。可以通过在同一类里自定义获取 key 的函数,这个函数要求和被限流的方法入参一致,返回值为 String 类型。返回值不能为空,为空时,会回退到默认的 key 获取策略。

3.4.2 自定义限流后的行为

3.4.2.1 配置响应内容
properties 复制代码
spring.ratelimiter.enabled=true
spring.ratelimiter.response-body=Too Many Requests
spring.ratelimiter.status-code=509

添加如上配置后,触发限流时,http 的状态码就变成了 509 。响应的内容变成了 Too Many Requests 了

3.4.2.2 自定义限流触发异常处理器

默认的触发限流后,限流器会抛出一个异常,限流器框架内定义了一个异常处理器来处理。自定义限流触发处理器,需要先禁用系统默认的限流触发处理器,禁用方式如下:

properties 复制代码
spring.ratelimiter.exceptionHandler.enable=false

然后在项目里添加自定义处理器,如下:

java 复制代码
@ControllerAdvice
public class RateLimitExceptionHandler {

    private final RateLimiterProperties limiterProperties;

    public RateLimitExceptionHandler(RateLimiterProperties limiterProperties) {
        this.limiterProperties = limiterProperties;
    }

    @ExceptionHandler(value = RateLimitException.class)
    @ResponseBody
    public String exceptionHandler(HttpServletResponse response, RateLimitException e) {
        response.setStatus(limiterProperties.getStatusCode());
        response.setHeader("Retry-After", String.valueOf(e.getRetryAfter()));
        return limiterProperties.getResponseBody();
    }
}
3.4.2.3 自定义触发限流处理函数,限流降级
java 复制代码
@RequestMapping("/test")
public class TestController {

    @GetMapping("/get")
    @RateLimit(rate = 5, rateInterval = "10s", fallbackFunction = "getFallback")
    public String get(String name) {
        return "get";
    }

    public String getFallback(String name) {
        return "Too Many Requests" + name;
    }

}

这种方式实现和使用和 2.1.3、自定义 key 获取函数类似。但是多一个要求,返回值的类型需要和原限流函数的返回值类型一致,当触发限流时,框架会调用 fallbackFunction 配置的函数执行并返回,达到限流降级的效果

3.4.3 动态设置限流大小

3.4.3.1 rateExpression 的使用

v1.2 版本开始,在 @RateLimit 注解里新增了属性 rateExpression。该属性支持 Spel 表达式从 Spring 的配置上下文中获取值。 当配置了 rateExpression 后,rate 属性的配置就不生效了。使用方式如下:

java 复制代码
@GetMapping("/get2")
@RateLimit(rate = 2, rateInterval = "10s", rateExpression = "${spring.ratelimiter.max}")
public String get2(){
        return"get";
}

集成 apollo 等配置中心后,可以做到限流大小的动态调整在线热更。

3.5 直接使用限流器服务-RateLimiterService

v1.3 版本开始,限流器框架内部提供了一个限流器服务,可以直接使用。当使用 RateLimiterService 后,则不用关心限流注解的逻辑了,所有限流逻辑都可以高度定制,如下:

java 复制代码
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private RateLimiterService limiterService;

    @GetMapping("/limiterService/time-window")
    public String limiterServiceTimeWindow(String key) {
        Rule rule = new Rule(Mode.TIME_WINDOW); // 限流策略,设置为时间窗口
        rule.setKey(key); //限流的 key
        rule.setRate(5); //限流的速率
        rule.setRateInterval(10); //时间窗口大小,单位为秒
        Result result = limiterService.isAllowed(rule);
        if (result.isAllow()) { //如果允许访问
            return "ok";
        } else {
            //触发限流
            return "no";
        }
    }

    @GetMapping("/limiterService/token-bucket")
    public String limiterServiceTokenBucket(String key) {
        Rule rule = new Rule(Mode.TOKEN_BUCKET); // 限流策略,设置为令牌桶
        rule.setKey(key); //限流的 key
        rule.setRate(5); //每秒产生的令牌数
        rule.setBucketCapacity(10); //令牌桶容量
        rule.setRequestedTokens(1); //请求的令牌数
        Result result = limiterService.isAllowed(rule);
        if (result.isAllow()) { //如果允许访问
            return "ok";
        } else {
            //触发限流
            return "no";
        }
    }
}

3.6压力测试

  • 压测工具 wrk: https://github.com/wg/wrk

  • 测试环境: 8 核心 cpu ,jvm 内存给的 -Xms2048m -Xmx2048m ,链接的本地的 redis

    #压测数据
    kldeMacBook-Pro-6:ratelimiter-spring-boot-starter kl$ wrk -t16 -c100 -d15s --latency http://localhost:8080/test/wrk
    Running 15s test @ http://localhost:8080/test/wrk
    16 threads and 100 connections
    Thread Stats Avg Stdev Max +/- Stdev
    Latency 6.18ms 20.70ms 281.21ms 98.17%
    Req/Sec 1.65k 307.06 2.30k 76.44%
    Latency Distribution
    50% 3.57ms
    75% 4.11ms
    90% 5.01ms
    99% 115.48ms
    389399 requests in 15.03s, 43.15MB read
    Requests/sec: 25915.91
    Transfer/sec: 2.87MB

压测下,所有流量都过限流器,qps 可以达到 2w+。

3.7版本更新

3.7.1 (v1.1.1)版本更新内容

  • 1、触发限流时,header 的 Retry-After 值,单位由 ms ,调整成了 s

3.7.2(v1.2)版本更新内容

  • 1、触发限流时,响应的类型从 text/plain 变成了 application/json
  • 2、优化了限流的 lua 脚本,将原来的两步 lua 脚本请求,合并成了一个,减少了和 redis 的交互
  • 3、限流的时间窗口大小,支持 Spel 从 Spring 的配置上下文中获取,结合 apollo 等配置中心后,支持规则的动态下发热更新

3.7.3(v1.3)版本更新内容

  • 1、配置策略变化,不在从应用的上下文中获取 Redis 数据源,而是必须配置。但是配置的数据源在 Spring 上下文中声明了 rateLimiterRedissonBeanName,应用也可以获取使用
  • 2、代码重构,新增了令牌桶的限流策略支持
  • 3、抽象了限流器服务 RateLimiterService,并在 Spring 上下文中声明了,应用可以直接注入使用

4.总结

这个也是在生产实践后遇到的坑的一个总结,ratelimiter-spring-boot-starter、redisson-spring-boot-starter同时使用会有冲突,已经RedisTemplate装配上一篇文章的redisConfig配置会有报错,所以这篇文章做了一个代码调整,总结和分享,也是方便以后快速使用,不至于搞半天,所以总结成文是很有必要的,也是对以后的一种方便,ratelimiter-spring-boot-starter开源分布式限流组件(偏业务)的使用也是非常简单,参看官网就可以学会的,源码写的也是很好的,就不需要自己重复的去制造轮子了,有这种开源好用的轮子直接拿来使用解决业务的燃眉之急,ratelimiter-spring-boot-starter可以针对一个接口使用令牌桶(接口总体上的限流)限流 + 时间窗口限流(针对一个用户主键key,用户唯一标识,对这个用户限制在3s内只能点击一次的操作,防止重复点击) + redisson分布式锁(比如说锁一个用户唯一标识3s钟释放锁,这里存在一个问题就是3s内执行的太快就容易点击多次,取决于用户3s内的手续和接口每次执行的快慢),经过这3个步骤,就可以让系统接口的具有3保险,也就是系统接口鲁棒性得到了大大的增强,希望我的分享对你有帮助,请一键三连,么么么哒!

相关推荐
儿时可乖了9 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol10 分钟前
java基础概念37:正则表达式2-爬虫
java
简 洁 冬冬12 分钟前
046 购物车
redis·购物车
xmh-sxh-131427 分钟前
jdk各个版本介绍
java
soulteary42 分钟前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django