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单机限流
单机限流,可以用到 Semaphore
、AtomicInteger
、RateLimiter
这些。
使用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
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