2021-07-31_redis&Lua限流框架设计学习笔记

20210731_redis&Lua限流框架设计学习笔记

1概述

本节主要学习分布式限流场景下,使用redis+lua实现特定时间窗内某个接口的请求数限流。

涉及SpringBoot(v2.1.4)、Redis单机模式、Lua限流脚本、Jmeter压测工具。后面代码我会放在git上,供大家学习交流。

项目结构说明:

aop:切面相关,拦截需要限量的api接口,并进行流控。

config:配置相关。

controller:业务请求相关。

errorresp:全局异常处理。

util和myanno:工具类及注解。

resources:静态资源相关(可自定义异常页面)。

具体如下:

[图片上传失败...(image-b10ff8-1628073817252)]

1.1限流算法理论

常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。

1.4.1令牌桶

令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。

Guava框架提供了令牌桶算法实现,可直接拿来使用。Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

1.4.2漏桶

漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(Traffic Policing)

1.4.3令牌桶和漏桶对比

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
  • 漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
  • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
  • 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。

1.4.3计数器

1.2应用级限流

1.2.1限流总并发、连接、请求数

对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值

1.2.2限流总资源数

1.2.3限流某个接口的总并发/请求数

可以使用Java中的AtomicLong进行限流。

1.2.4限流某个接口的时间窗请求数

即一个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时我们要对每秒/每分钟的调用量进行限速。

1.3单机限流

单机限流,可以用到 SemaphoreAtomicIntegerRateLimiter 这些。

使用Semaphore一定要确保release方法被调用,例如放到finally中,否则许可证得不到释放,将会导致接口被全部占用,无法接收请求。

1.4分布式限流

但是在分布式中,如何处理分布式限流呢?

网关层限流:常用分布式限流用 Nginx 限流,但是它属于网关层面,不能解决所有问题,例如内部服务,短信接口,你无法保证消费方是否会做好限流控制,所以自己在应用层实现限流还是很有必要的。

接入层限流:采用nginx自带的连接数限流模块和请求限流模块。

应用层限流:Nginx+Lua、Redis+Lua。

1.4.1限流服务原子化

分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使使用redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现的高并发和高性能。

1.5高并发系统相关知识

首先我们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。

Lua本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法。

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。

限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。

一般开发高并发系统常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

先有缓存这个银弹,后有限流来应对618、双十一高并发流量,在处理高并发问题上可以说是如虎添翼,不用担心瞬间流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务;限流需要评估好,不可乱用,否则会正常流量出现一些奇怪的问题而导致用户抱怨。

在实际应用时也不要太纠结算法问题,因为一些限流算法实现是一样的只是描述不一样;具体使用哪种限流技术还是要根据实际场景来选择,不要一味去找最佳模式,白猫黑猫能解决问题的就是好猫。

2代码实战

2.1Maven 依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>myspringbootrateframework</artifactId>
        <groupId>com.kikop</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>myredisluademo</artifactId>

    <name>myredisluademo</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
    </properties>

    <dependencies>

        <!--1.spring-boot-web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--2.spring-boot-test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>


        <!--3.spring-boot-starter-data-redis-->
        <!--基于spring-data-redis:v2.1.6-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--4.spring-boot-starter-aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!--5.commons-lang3-->
        <!--日期格式化:DateFormatUtils-->
        <!--DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss")-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>


        <!--6.该依赖作用是在使用IDEA编写配置文件有代码提示-->
        <!--非必需-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

        <!--7.fastjson-->
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>

        <!--8.配置模板引擎:thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

</project>

2.2定义注解

package com.kikop.myanno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author kikop
 * @version 1.0
 * @project Name: myredisluademo
 * @file Name: RateLimit
 * @desc 一定的时间窗口内, 控制访问频率。Eg:1秒中可以访问5次
 * @date 2021/7/31
 * @time 13:00
 * @by IDE: IntelliJ IDEA
 */

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {

    /**
     * 限流唯一标识
     *
     * @return
     */
    String key() default "";

    /**
     * 限流时间
     *
     * @return
     */
    int time();

    /**
     * 限流次数,[0,count]
     *
     * @return
     */
    int count();
}

2.3定义配置类

package com.kikop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

import java.io.Serializable;


/**
 * @author kikop
 * @version 1.0
 * @project Name: myredisluademo
 * @file Name: AppConfig
 * @desc
 * @date 2021/7/31
 * @time 13:00
 * @by IDE: IntelliJ IDEA
 */
@Configuration
public class AppConfig extends WebMvcConfigurationSupport {


    /**
     * 读取限流脚本
     * DefaultRedisScript
     *
     * @return
     */
    @Bean
    public DefaultRedisScript<Number> redisluaScript() {
        DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("mysemalimit.lua")));
        redisScript.setResultType(Number.class);
        return redisScript;
    }

    /**
     * RedisTemplate
     *
     * @param redisConnectionFactory 构造注入 RedisStandaloneConfiguration
     * @return
     */
    @Bean
    public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> template = new RedisTemplate<String, Serializable>();

        // key serial
        template.setKeySerializer(new StringRedisSerializer());

        // value serial
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        template.setConnectionFactory(redisConnectionFactory);
        return template;

    }


    /**
     * 重写 addResourceHandlers
     * 通过继承 WebMvcConfigurationSupport(springboot2.x后用此类)
     *
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {

        // 1.http://localhost:8080/myredisluademo/viewtoken.png
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/META-INF/resources/",
                        "classpath:/resources/",
                        "classpath:/public/",
                        "classpath:/static/");

        // 2.http://localhost:8080/myredisluademo/mypicture/btiles/6/8/2.jpg
        registry.addResourceHandler("/mypicture/**")
                //表示文件路径,这里的意思是picture包下的所有文件,所有/picture/开头的请求 都会去后面配置的路径下查找资源
                .addResourceLocations("file:D:/workdirectory/mapexperiment/");
        //表示要开放的资源
        super.addResourceHandlers(registry);
    }

    /**
     * 增加业务系统拦截器
     *
     * @param registry
     */
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        super.addInterceptors(registry);
    }
}

2.4定义切面

package com.kikop.aop;


import com.kikop.myanno.RateLimit;
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.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;
import java.util.Collections;
import java.util.List;

/**
 * @author kikop
 * @version 1.0
 * @project Name: myredisluademo
 * @file Name: LimitAspect
 * @desc 拦截器
 * @date 2021/7/31
 * @time 13:00
 * @by IDE: IntelliJ IDEA
 */
@Aspect
@Configuration
public class LimitAspect {

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

    // 当前访问流量:Number
    @Autowired
    private DefaultRedisScript<Number> redisluaScript;

    @Autowired
    private RedisTemplate<String, Serializable> limitRedisTemplate;


    @Around("execution(* com.kikop.controller ..*(..) )")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();

        // 获取方法 RateLimit注解
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);

        if (rateLimit != null) {
            HttpServletRequest request = ((ServletRequestAttributes)
                    RequestContextHolder.getRequestAttributes()).getRequest();
            String ipAddress = getIpAddr(request);

            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append(ipAddress).append("-") // ip
                    .append(targetClass.getName()).append("- ") // class
                    .append(method.getName()).append("-") // method
                    .append(rateLimit.key()); // key

            // keys:
            // [0]:0:0:0:0:0:0:0:1-com.kikop.controller.RedisLuaController- luaLimiter-limitKey
            List<String> keys = Collections.singletonList(stringBuffer.toString());

            // script
            // keys:
            // [0]: 0:0:0:0:0:0:0:1-com.kikop.controller.RedisLuaController- luaLimiter-limitKey,作为lua参数:KEYS[1]
            // args:
            // [0]:count,作为lua参数:ARGV[1]
            // [1]:time,作为lua参数:ARGV[2]
            // 实际执行次数,包括上限
            Number numberInAction = limitRedisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());

            if (numberInAction != null && numberInAction.intValue() != 0 && numberInAction.intValue() <= rateLimit.count()) {
                logger.info("这是您的第:{} 次访问,thread:{}", numberInAction.toString(), Thread.currentThread().getId());

                // 限流内访问次数:放到请求体中
                request.setAttribute("numberInAction",numberInAction.toString());
                return joinPoint.proceed();
            }
            // number.intValue==0 或者大于 rateLimit.count

        } else { // 无限流控制的API,调用 业务Controller方法
            return joinPoint.proceed();
        }

        throw new RuntimeException("已经到最大限流次数,请稍后再试!");
    }

    public static String getIpAddr(HttpServletRequest request) {
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getHeader("WL-Proxy-Client-IP");
            }
            if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
                ipAddress = request.getRemoteAddr();
            }
            // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
            if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
                // = 15
                if (ipAddress.indexOf(",") > 0) {
                    ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
                }
            }
        } catch (Exception e) {
            ipAddress = "";
        }
        return ipAddress;
    }
}

2.5定义控制类

package com.kikop.controller;


import com.kikop.myanno.RateLimit;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicInteger;
import org.springframework.http.MediaType;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Date;

/**
 * @author kikop
 * @version 1.0
 * @project Name: myredisluademo
 * @file Name: RedisLuaController
 * @desc
 * @date 2021/7/31
 * @time 13:00
 * @by IDE: IntelliJ IDEA
 */
@RestController
@RequestMapping("/myredislua")
public class RedisLuaController {


    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 正常范围内的方法
     * 1秒中可以访问5次
     *
     * @return
     */
    @RateLimit(key = "limitKey", time = 1, count = 5)
    // consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
    // produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
    // 1.有乱码问题
        @GetMapping(value = "/luaLimiter")
    // 2.收发类型不匹配
    // @GetMapping(value = "/luaLimiter",consumes = MediaType.APPLICATION_JSON_VALUE, produces = {"application/json; charset=UTF-8"})
    // 3.解决乱码问题,对方发送的编码方式为UTF-8,所以接收数据的时候也设置编码格式
    @RequestMapping(value = "/luaLimiter", produces = {"application/json; charset=UTF-8"})
    public String luaLimiter(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {


        // http://localhost:8080/myredisluademo/myredislua/luaLimiter

        String result = "";
        // 1.测试计数
        RedisAtomicInteger entityIdCounter = new RedisAtomicInteger("myredisLuaCounter",
                redisTemplate.getConnectionFactory());

        // 2.打印结果
//        String currentDate = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
        String currentDate = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
        String numberInAction = (String) request.getAttribute("numberInAction");
        String nativeResult = String.format("当前时间:%s,限流内访问次数:%s,累计访问次数:%s"
                ,currentDate, numberInAction, String.valueOf(entityIdCounter.getAndIncrement() + 1));


        // 这里对编码方式修改,无效 todo
//        response.setHeader("Content-type","application/json;charset=UTF-8");
        return nativeResult;
    }

    @GetMapping("/userinfo")
    public ModelAndView userinfo() {
        ModelAndView mv = new ModelAndView("userinfo");
        mv.addObject("message", "errorMsg");
        return mv;
    }

    @GetMapping("/testByZero")
    @ResponseBody
    public String testByZero() {
        int isTest = 1;
        if (isTest == 1) {
//        int result = 100 / 0;
            throw new RuntimeException("测试出错了!");
        }
        return "ok";

    }

}

2.6统一的Rest异常

package com.kikop.errorresp;


import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.MediaType;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.NoHandlerFoundException;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @author kikop
 * @version 1.0
 * @project Name: myredisluademo
 * @file Name: MyCustomExceptionJsonResp
 * @desc 全局异常, 返回统一的Json格式
 * @date 2021/7/31
 * @time 13:00
 * @by IDE: IntelliJ IDEA
 */
@RestController
public class MyCustomExceptionJsonResp implements ErrorController {

    private static final String ERROR_PATH = "/error";

    @Autowired
    private ErrorAttributes errorAttributes;

    @Override
    public String getErrorPath() {
        return ERROR_PATH;
    }


    /**
     * 封装统一的 Json对象
     *
     * @param request
     * @return
     * @throws Exception
     */
    @ExceptionHandler(value = Exception.class)
    @RequestMapping(value = ERROR_PATH, produces = {MediaType.APPLICATION_JSON_VALUE})
    public JSONObject defaultErrorHandler(HttpServletRequest request) {

        JSONObject result = new JSONObject();
        result.put("success", false);

        // 1.获取 servletWebRequest
        ServletWebRequest servletWebRequest = new ServletWebRequest(request);

        // 2.封装所有出错kv
        Map<String, Object> body = this.errorAttributes.getErrorAttributes(servletWebRequest, true);

        // 3.提取核心异常提示
        result.put("message", body.get("message"));
        result.put("method", body.get("path"));
        return result;
    }


}

2.7文件配置

2.7.1application.yml

spring:

# 指定部署环境:开发
 profiles:
  active: dev

2.7.2application-dev.properties

server.port=8080
server.servlet.context-path=/myredisluademo


## 配置thymeleaf视图解析器,主要是为了该suffix,否则都不用写
spring.thymeleaf.cache=false
# 默认 prefix:classpath:/templates/
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.jsp
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8


# 默认情况下Spring Boot将日志输出到控制台,不会写到日志文件。如果要编写除控制台输出之外的日志文件,\
# 则需在application.properties中设置logging.file或logging.path属性
#注:二者 file,path不能同时使用,如若同时使用,则只有logging.file生效
#设置目录,会在该目录下创建 spring.log文件,并写入日志内容
#logging.path=D:/mqexperimentlog/myredisluademo
#logging.myredis=classpath:log/logback-myredis.xml
#spring.resources.static-locations=\
#  classpath:/META-INF/resources/,\
#  classpath:/resources/,\
#  classpath:/public/,\
#  classpath:/static/
spring.application.name=myredisluademo


# Redis数据库索引
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒),执行命令最长等待时间 ok
spring.redis.timeout=20000

2.8定义主类

package com.kikop;

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


/**
 * @author kikop
 * @version 1.0
 * @project Name: mysemaphoredemo
 * @file Name: MyRedisLuaApplication
 * @desc
 * @date 2021/7/31
 * @time 13:00
 * @by IDE: IntelliJ IDEA
 */
@SpringBootApplication
public class MyRedisLuaApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext configurableApplicationContext =
                SpringApplication.run(MyRedisLuaApplication.class, args);
    }

}

2.9Jemeter压力测试

模拟场景:1秒钟最多5个请求,我们可以看到第六6请求失败了。

http://localhost:8080/myredisluademo/myredislua/luaLimiter

image-20210731212039646.png

image-20210804183927488.png

3总结

3.1Lua脚本性能优化

lua脚本的好处借助redis单线程特性,将复杂的业务逻辑封装在lua脚本中发送给redis,且redis是原子性的,这样就保证了这段逻辑的原子性。

--lua脚本进行原子操作

-- 1.限流KEY,两点代表拼接
local key = "rate.limit:" .. KEYS[1]
-- 2.限流大小
local limit = tonumber(ARGV[1])

-- 3.获取当前访问流量
local current = tonumber(redis.call('get', key) or "0")

-- 4.流控逻辑判断
if current + 1 > limit then --如果超出限流大小,返回0,表示达到限流条件
  return 0
else

  -- 判断key是否存在,首次肯定不存在
  -- 0:不存在(Key为首次创建);1:存在
  local isExistKey = tonumber(redis.call('exists', key))

  -- 请求数 +1,不影响过期时间
  -- 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令
  redis.call("INCRBY", key,"1")


  -- 1.因为更新了,重写开始,并设置argv秒过期
  -- 缺点:Redis的值没有被重置,指定的秒内达到峰值以后,可能不会消失,因为最后一个合法的限流请求会加大过期时间
  -- 导致中间的ARGV[2]时间系统空转!
  -- redis.call("expire", key,"2")

  -- 2.脚本改进
  -- 到这里,key肯定有了,需要判断是否是首次。符合条件后,进行过期时间重置 add by kikop 20210801
  if isExistKey ==0 then -- Key为首次创建
    redis.call("expire", key,ARGV[2])
  end

  -- 每执行一次,加1,返回值即为执行次数
  return current + 1
end

3.2中文乱码问题

// 解决乱码问题,对方发送的编码方式为UTF-8,所以接收数据的时候也设置编码格式
@RequestMapping(value = "/luaLimiter", produces = {"application/json; charset=UTF-8"})

[图片上传失败...(image-359289-1628073817252)]

[图片上传失败...(image-3df46e-1628073817252)]

[图片上传失败...(image-dcbd08-1628073817252)]

参考

1Java高并发实战 - 使用Semaphore对单接口进行限流(单接口版)

//www.greatytc.com/p/57c33b330c83

https://juejin.cn/user/1961184476018359/posts

https://juejin.cn/post/6986823060418461726

2Java并发:分布式应用限流 Redis + Lua 实践

//www.greatytc.com/p/c1754b8c15da

3B站崩了,猜测了几个可能原因

https://juejin.cn/post/6984577649968414757

4聊聊高并发系统之限流特技(lua使用)

https://www.iteye.com/blog/jinnianshilongnian-2305117

5使用Redisson实现可重入分布式锁

//www.greatytc.com/p/7490a2a3be8d

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容