springboot+redis+lua脚本进行接口限流,解决高并发计数不准确问题

为啥用redis呢(只是此处的使用原因):

        因为redis是一个内存数据库,效率高;

        redis支持事务;

        redis支持分布式,与系统无强关联,不管系统是单机还是分布式部署都支持。

为啥用lua脚本呢:因为lua脚本可以原子性的执行redis命令。

注:千万不要使用网上那种在切面或者拦截器中直接使用redistemplate.opsForValue().get 与set的方式来进行接口请求数量的控制,因为当并发的时候,肯定会出现数量计算不准确的问题。

1.添加pom依赖

        <!-- redis引入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

2.yml配置文件:

spring:
  redis:
    #密码
    password: kevin
    #连接超时时长(毫秒)
    timeout: 30000
    cluster:
      #集群节点以逗号分隔,或换行后 - 开头
      nodes:
        - 127.0.0.1:6381
        - 127.0.0.1:6382
        - 127.0.0.1:6383
        - 127.0.0.1:6384
        - 127.0.0.1:6385
        - 127.0.0.1:6386
      # 获取失败 最大重定向次数
      max-redirects: 3
    #lettuce连接池信息
    # 连接池最大连接数(使用负值表示没有限制) 默认为8
    lettuce:
      pool:
        # 连接池最大连接数
        max-active: 1000
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1
        max-wait: -1
        # 连接池中的最大空闲连接 默认为8
        max-idle: 200
        # 连接池中的最小空闲连接 默认为 0
        min-idle: 100

3.redis配置类

package com.liu.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.liu.redisexpired.RedisMessageListenerFactory;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.Arrays;

@Configuration
public class RedisConfig {

    @Autowired
    private Environment environment;

    @Bean(value = "nodes")
    @ConfigurationProperties(prefix = "spring.redis.cluster")
    public RedisNodes nodes(){
        return new RedisNodes();
    }

    /**
     * 配置lettuce连接池
     * @author kevin
     * @return org.apache.commons.pool2.impl.GenericObjectPoolConfig
     * @date 2022/5/26
     */
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.redis.cluster.lettuce.pool")
    public GenericObjectPoolConfig<Object> redisPool() {
        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        String maxActive = environment.getProperty("spring.redis.lettuce.pool.max-active");
        if(null != maxActive && !"".equals(maxActive)) {
            poolConfig.setMaxTotal(Integer.parseInt(maxActive));
        }
        String maxWait = environment.getProperty("spring.redis.lettuce.pool.max-wait");
        if(null != maxWait && !"".equals(maxWait)) {
            poolConfig.setMaxWaitMillis(Integer.parseInt(maxWait));
        }
        String maxIdle = environment.getProperty("spring.redis.lettuce.pool.max-idle");
        if(null != maxIdle && !"".equals(maxIdle)) {
            poolConfig.setMaxIdle(Integer.parseInt(maxIdle));
        }
        String minIdle = environment.getProperty("spring.redis.lettuce.pool.min-idle");
        if(null != minIdle && !"".equals(minIdle)) {
            poolConfig.setMinIdle(Integer.parseInt(minIdle));
        }

        return new GenericObjectPoolConfig<>();
    }

    /**
     * 配置数据源的
     * @author kevin
     * @return org.springframework.data.redis.connection.RedisClusterConfiguration
     * @date 2022/5/26
     */
    @Bean("redisClusterConfig")
    @Primary
    public RedisClusterConfiguration redisClusterConfig() {
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(
                Arrays.asList(nodes().getNodes()));
        redisClusterConfiguration.setPassword(environment.getProperty("spring.redis.password"));
        String maxRedirects = environment.getProperty("spring.redis.cluster.max-redirects");
        if(null != maxRedirects && !"".equals(maxRedirects)) {
            redisClusterConfiguration.setMaxRedirects(Integer.parseInt(maxRedirects));
        }
        return redisClusterConfiguration;

    }


    /**
     * 配置数据源的连接工厂
     * 这里注意:需要添加@Primary 指定bean的名称,目的是为了创建不同名称的LettuceConnectionFactory
     * @author kevin
     * @param redisPool :
     * @param redisClusterConfig :
     * @return org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
     * @date 2022/5/26
     */
    @Bean("lettuceConnectionFactory")
    @Primary
    public LettuceConnectionFactory lettuceConnectionFactory(GenericObjectPoolConfig<Object> redisPool,
                                                             @Qualifier("redisClusterConfig") RedisClusterConfiguration redisClusterConfig) {
//        LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder()
//                .poolConfig(redisPool).build();

        LettuceClientConfiguration clientConfiguration = getClientConfiguration(redisPool);

        return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
    }


    /**
     * 配置数据源的RedisTemplate
     * 注意:这里指定使用名称=factory 的 RedisConnectionFactory
     * @author kevin
     * @param redisConnectionFactory :
     * @return org.springframework.data.redis.core.RedisTemplate
     * @date 2022/5/26
     */
    @Bean("redisTemplate")
    @Primary
    public RedisTemplate<String, Object> redisTemplate(@Qualifier("lettuceConnectionFactory")
                                                               RedisConnectionFactory redisConnectionFactory) {
        return getRedisTemplate(redisConnectionFactory);

    }

    /**
      * lettuce配置信息构建获取--redis集群高可用
      * @author kevin
      * @param redisPool :
      * @return org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration
      * @date 2022/5/27 12:00
      */
    private LettuceClientConfiguration getClientConfiguration(GenericObjectPoolConfig<Object> redisPool) {
        //支持自适应集群拓扑刷新和静态刷新源
        ClusterTopologyRefreshOptions clusterTopologyRefreshOptions =  ClusterTopologyRefreshOptions.builder()
                //.enablePeriodicRefresh(Duration.ofSeconds(10)) // 启用定期集群拓扑更新
                .enableAllAdaptiveRefreshTriggers() // 自适应拓扑刷新
                .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(10)) // 自适应拓扑更新的超时时间
                .build();
        ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder().timeoutOptions(
                TimeoutOptions.enabled(Duration.ofSeconds(30)))  // 超时修改为30秒
//                .autoReconnect(false)  //启用或禁用连接丢失时的自动重新连接--默认true
//                .pingBeforeActivateConnection(Boolean.TRUE) //在激活连接标志之前设置 PING
//                .cancelCommandsOnReconnectFailure(Boolean.TRUE) //允许在重新连接失败的情况下取消排队的命令。默认为 false
//                .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) //设置连接处于断开状态时的命令调用行为
                .topologyRefreshOptions(clusterTopologyRefreshOptions) //设置ClusterTopologyRefreshOptions拓扑更新的详细控制
                .build();

        return LettucePoolingClientConfiguration.builder()
                .poolConfig(redisPool) //设置GenericObjectPoolConfig驱动程序使用的连接池配置
                //.readFrom(ReadFrom.NEAREST)  //配置ReadFrom
                .clientOptions(clusterClientOptions) //配置ClientOptions
                .build();
    }

    /**
      * redisTemplate redis操作工具获取
      * @author kevin
      * @param factory : redis连接工厂
      * @return org.springframework.data.redis.core.RedisTemplate<java.lang.String,java.lang.Object>
      * @date 2022/5/27 12:00
      */
    private RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL) //过期方法
        om.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance ,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.WRAPPER_ARRAY);
        jackson2JsonRedisSerializer.setObjectMapper(om);

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

RedisNodes类:

package com.liu.config;

public class RedisNodes {
    private String[] nodes;

    public String[] getNodes() {
        return nodes;
    }

    public void setNodes(String[] nodes) {
        this.nodes = nodes;
    }
}

4.编写接口限流redis工具类

package com.liu.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.data.redis.serializer.GenericToStringSerializer;
import org.springframework.stereotype.Component;

import java.util.Collections;

/**
 * redis工具类
 *
 * @author kevin
 * @date 2021/3/30
 */
@Slf4j
@Component
@SuppressWarnings({"unused"})
public class RedisUtils {
    private static final Long SUCCESS = 1L;

    /*
     * 注入redisTemplate bean(可配置不同的template secondaryRedisTemplate)
     */
    @Autowired
    @Qualifier(value = "redisTemplate") //指定注入的template模版
    private RedisTemplate<String, Object> redisTemplate;
    //======================接口限流计数法--处理请求单位时间内接口请求次数 开始========================

    /**
     * 判断单位时间内请求数量是否超过限制--使用lua脚本执行,保证原子性
     *
     * @param key    : 键
     * @param time : 单位时间(秒)
     * @param count  : 最大请求次数
     * @return boolean true成功 false失败
     * @author kevin
     * @date 2022/5/31
     */
    public boolean requestLimit(String key, int time, int count) {
        //lua 脚本,进行请求次数的叠加,并判断请求次数是否超过限制
        String script = "local val = redis.call('incr', KEYS[1]) " +
                "local expire = tonumber(ARGV[1]) " +
                "if val == 1 " +
                "then redis.call('expire', KEYS[1], expire) " +
                "else if redis.call('ttl', KEYS[1]) == -1 " +
                "then redis.call('expire', KEYS[1], expire) " +
                "end " +
                "end " +
                "if val > tonumber(ARGV[2]) " +
                "then return 0 " +
                "end " +
                "return 1";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);

        // execute使用的redis的默认的序列化方式,需要设置参数--arg的序列化方式,以及result结果的序列化方式
        // 此处传参只要能转为Object就行(因为数字不能直接强转为String,所以不能用String序列化)
        // 结果的类型需要根据脚本定义,此处是数字--定义的是Long类型
        Long result = redisTemplate.execute(redisScript, new GenericToStringSerializer<>(Object.class),
                new GenericToStringSerializer<>(Long.class), Collections.singletonList(key), time, count);

        return SUCCESS.equals(result);
    }
    //======================接口限流计数法--处理请求单位时间内接口请求次数 结束========================


}

5.自定义限流注解

package com.liu.requestlimit;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

/**
  * 限制单位时间内,接口被请求的次数
  * @author kevin
  * @date 2022/5/26
  */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLimit {

    /*
        限流的key
     */
    String key() default "request_limit_";

    /*
        单位时间
     */
    int time() default 60;

    /*
        限流时间单位,默认秒
     */
    TimeUnit timeunit() default TimeUnit.SECONDS;

    /*
        单位时间内可访问次数
     */
    int count() default 100;

    /*
        限流的类型:全局,IP
     */
    RequestLimitType limitType() default RequestLimitType.DEFAULT;

    String msg() default "请求过于频繁,请稍后再试!";
}

6.编写AOP切面

package com.liu.requestlimit;

import com.liu.utils.RedisUtils;
import com.liu.utils.WebUtils;
import com.liu.vo.ResponseVo;
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.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.StringJoiner;

@Slf4j
@Aspect
@Component
public class RequestLimitAspect {

    @Resource
    private RedisUtils redisUtils;

    @Pointcut("@annotation(requestLimit)")
    public void doBefore(RequestLimit requestLimit){

        //切点定义
    }

    @Around(value = "doBefore(requestLimit)", argNames = "joinPoint,requestLimit")
    public Object doAround(ProceedingJoinPoint joinPoint, RequestLimit requestLimit) throws Throwable {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if(null == requestAttributes){
            log.info("限流失败,未获取到请求request");
            return joinPoint.proceed();
        }
        HttpServletRequest request = requestAttributes.getRequest();

        int time = requestLimit.time();
        int maxCount = requestLimit.count();

        StringJoiner key = new StringJoiner("");
        key.add(requestLimit.key());
        if(RequestLimitType.IP.code == requestLimit.limitType().code){
            key.add(WebUtils.getIP(request) + ":");
        }
        key.add(request.getRequestURI());
        String keyStr = key.toString();
        // 使用lua脚本执行,保证原子性,进行请求次数限制
        boolean isOutOfLimit = redisUtils.requestLimit(keyStr, time, maxCount);
        if(!isOutOfLimit){
            HttpServletResponse response = requestAttributes.getResponse();
            responseFailed(response, requestLimit.msg());
            return null;
        }

        return joinPoint.proceed();
    }

    private void responseFailed(HttpServletResponse response, String msg){
        ResponseVo result = new ResponseVo.Builder().error().message(msg).build();
        log.info(msg);
        WebUtils.responseOutJson(response, result);
    }
}

7.在需要限流的接口上加自定义的注解

package com.liu.controller;

import com.liu.requestlimit.RequestLimit;
import com.liu.requestlimit.RequestLimitType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {


    /**
      * 测试接口限流
      *
      * @author kevin
      * @date 2022/6/2 10:42
      */
    @RequestLimit(time = 10, count = 5, limitType = RequestLimitType.IP)
    @GetMapping("/testLimitRequest")
    public void testLimitRequest() throws InterruptedException {

        log.info("接口限流测试,开始处理被限流接口的业务......");
        Thread.sleep(2000);
        log.info("接口限流测试,处理被限流接口的业务结束!");
    }

}

限流类型枚举类--RequestLimitType:

package com.liu.requestlimit;

@SuppressWarnings({"unused"})
public enum RequestLimitType {

    DEFAULT(1, "全局限制"),
    IP(2, "通过IP限制");

    int code;

    String desc;

    RequestLimitType(int code, String desc){
        this.code = code;
        this.desc = desc;
    }


    /**
     * 通过代码值获取枚举
     * @author kevin
     * @param code :
     * @return com.cetccloud.base.enums.RequestLimitType
     * @date 2020/12/23 16:09
     */
    public static RequestLimitType getByCode(int code){
        for(RequestLimitType c : RequestLimitType.values()){
            if(code == c.getCode()){
                return c;
            }
        }
        return null;
    }

    /**
     * 通过描述获取枚举
     * @author kevin
     * @param desc :
     * @return com.cetccloud.base.enums.RequestLimitType
     * @date 2022/5/26
     */
    public static RequestLimitType getByDesc(String desc){
        for(RequestLimitType c : RequestLimitType.values()){
            if(desc.equals(c.getDesc())){
                return c;
            }
        }
        return null;
    }

    /**
     * 通过代码值获得代码描述
     * @author kevin
     * @param code :
     * @return java.lang.String
     * @date 2022/5/26
     */
    public static String getDesc(int code){
        for(RequestLimitType c : RequestLimitType.values()){
            if(code == c.getCode()){
                return c.desc;
            }
        }
        return null;
    }

    /**
     * 通过代码描述获得代码值
     * @author kevin
     * @param desc :
     * @return int
     * @date 2022/5/26
     */
    public static int getCode(String desc){
        for(RequestLimitType c : RequestLimitType.values()){
            if(desc.equals(c.getDesc())){
                return c.code;
            }
        }
        return -99;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

8.直接jmeter测试并发

2022-06-06 14:55:53.430  INFO 13428 --- [nio-8081-exec-2] com.liu.controller.TestController        : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430  INFO 13428 --- [nio-8081-exec-4] com.liu.controller.TestController        : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430  INFO 13428 --- [nio-8081-exec-3] com.liu.controller.TestController        : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430  INFO 13428 --- [nio-8081-exec-1] com.liu.controller.TestController        : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.430  INFO 13428 --- [nio-8081-exec-6] com.liu.controller.TestController        : 接口限流测试,开始处理被限流接口的业务......
2022-06-06 14:55:53.555  INFO 13428 --- [nio-8081-exec-5] com.liu.requestlimit.RequestLimitAspect  : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.555  INFO 13428 --- [nio-8081-exec-7] com.liu.requestlimit.RequestLimitAspect  : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.555  INFO 13428 --- [nio-8081-exec-9] com.liu.requestlimit.RequestLimitAspect  : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.555  INFO 13428 --- [nio-8081-exec-8] com.liu.requestlimit.RequestLimitAspect  : 请求过于频繁,请稍后再试!
2022-06-06 14:55:53.648  INFO 13428 --- [io-8081-exec-10] com.liu.requestlimit.RequestLimitAspect  : 请求过于频繁,请稍后再试!
2022-06-06 14:55:55.445  INFO 13428 --- [nio-8081-exec-6] com.liu.controller.TestController        : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445  INFO 13428 --- [nio-8081-exec-2] com.liu.controller.TestController        : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445  INFO 13428 --- [nio-8081-exec-1] com.liu.controller.TestController        : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445  INFO 13428 --- [nio-8081-exec-4] com.liu.controller.TestController        : 接口限流测试,处理被限流接口的业务结束!
2022-06-06 14:55:55.445  INFO 13428 --- [nio-8081-exec-3] com.liu.controller.TestController        : 接口限流测试,处理被限流接口的业务结束!

 

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值