==========================================================
项目源码已提交到github:https://github.com/sunshinelyz/mykit-ratelimiter
本文是在《【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!》一文的基础上进行实现,有关项目的搭建可参见《【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!》一文的内容。小伙伴们可以关注【冰河技术】微信公众号来阅读上述文章。
前面介绍的限流方案有一个缺陷就是:它不是全局的,不是分布式的,无法很好的应对分布式场景下的大流量冲击。那么,接下来,我们就介绍下如何实现亿级流量下的分布式限流。
分布式限流的关键就是需要将限流服务做成全局的,统一的。可以采用Redis+Lua技术实现,通过这种技术可以实现高并发和高性能的限流。
Lua是一种轻量小巧的脚本编程语言,用标准的C语言编写的开源脚本,其设计的目的是为了嵌入到应用程序中,为应用程序提供灵活的扩展和定制功能。
我们可以使用Redia+Lua脚本的方式来对我们的分布式系统进行统一的全局限流,Redis+Lua实现的Lua脚本:
local key = KEYS[1] --限流KEY(一秒一个)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call(‘get’, key) or “0”)
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置2秒过期
redis.call(“INCRBY”, key, “1”)
redis.call(“expire”, key “2”)
return 1
end
我们可以按照如下的思路来理解上述Lua脚本代码。
(1)在Lua脚本中,有两个全局变量,用来接收Redis应用端传递的键和其他参数,分别为:KEYS、ARGV;
(2)在应用端传递KEYS时是一个数组列表,在Lua脚本中通过索引下标方式获取数组内的值。
(3)在应用端传递ARGV时参数比较灵活,可以是一个或多个独立的参数,但对应到Lua脚本中统一用ARGV这个数组接收,获取方式也是通过数组下标获取。
(4)以上操作是在一个Lua脚本中,又因为我当前使用的是Redis 5.0版本(Redis 6.0支持多线程),执行的请求是单线程的,因此,Redis+Lua的处理方式是线程安全的,并且具有原子性。
这里,需要注意一个知识点,那就是原子性操作:如果一个操作时不可分割的,是多线程安全的,我们就称为原子性操作。
接下来,我们可以使用如下Java代码来判断是否需要限流。
//List设置Lua的KEYS[1]
String key = “ip:” + System.currentTimeMillis() / 1000;
List keyList = Lists.newArrayList(key);
//List设置Lua的ARGV[1]
List argvList = Lists.newArrayList(String.valueOf(value));
//调用Lua脚本并执行
List result = stringRedisTemplate.execute(redisScript, keyList, argvList)
至此,我们简单的介绍了使用Redis+Lua脚本实现分布式限流的总体思路,并给出了Lua脚本的核心代码和Java程序调用Lua脚本的核心代码。接下来,我们就动手写一个使用Redis+Lua脚本实现的分布式限流案例。
这里,我们和在《【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!》一文中的实现方式类似,也是通过自定义注解的形式来实现分布式、大流量场景下的限流,只不过这里我们使用了Redis+Lua脚本的方式实现了全局统一的限流模式。接下来,我们就一起手动实现这个案例。
创建注解
首先,我们在项目中,定义个名称为MyRedisLimiter的注解,具体代码如下所示。
package io.mykit.limiter.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
-
@author binghe
-
@version 1.0.0
-
@description 自定义注解实现分布式限流
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyRedisLimiter {
@AliasFor(“limit”)
double value() default Double.MAX_VALUE;
double limit() default Double.MAX_VALUE;
}
在MyRedisLimiter注解内部,我们为value属性添加了别名limit,在我们真正使用@MyRedisLimiter注解时,即可以使用@MyRedisLimiter(10),也可以使用@MyRedisLimiter(value=10),还可以使用@MyRedisLimiter(limit=10)。
创建切面类
创建注解后,我们就来创建一个切面类MyRedisLimiterAspect,MyRedisLimiterAspect类的作用主要是解析@MyRedisLimiter注解,并且执行限流的规则。这样,就不需要我们在每个需要限流的方法中执行具体的限流逻辑了,只需要我们在需要限流的方法上添加@MyRedisLimiter注解即可,具体代码如下所示。
package io.mykit.limiter.aspect;
import com.google.common.collect.Lists;
import io.mykit.limiter.annotation.MyRedisLimiter;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.List;
/**
-
@author binghe
-
@version 1.0.0
-
@description MyRedisLimiter注解的切面类
*/
@Aspect
@Component
public class MyRedisLimiterAspect {
private final Logger logger = LoggerFactory.getLogger(MyRedisLimiter.class);
@Autowired
private HttpServletResponse response;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private DefaultRedisScript redisScript;
@PostConstruct
public void init(){
redisScript = new DefaultRedisScript();
redisScript.setResultType(List.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource((“limit.lua”))));
}
@Pointcut(“execution(public * io.mykit.limiter.controller..(…))”)
public void pointcut(){
}
@Around(“pointcut()”)
public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
//使用反射获取MyRedisLimiter注解
MyRedisLimiter myRedisLimiter = signature.getMethod().getDeclaredAnnotation(MyRedisLimiter.class);
if(myRedisLimiter == null){
//正常执行方法
return proceedingJoinPoint.proceed();
}
//获取注解上的参数,获取配置的速率
double value = myRedisLimiter.value();
//List设置Lua的KEYS[1]
String key = “ip:” + System.currentTimeMillis() / 1000;
List keyList = Lists.newArrayList(key);
//List设置Lua的ARGV[1]
List argvList = Lists.newArrayList(String.valueOf(value));
//调用Lua脚本并执行
List result = stringRedisTemplate.execute(redisScript, keyList, String.valueOf(value));
logger.info(“Lua脚本的执行结果:” + result);
//Lua脚本返回0,表示超出流量大小,返回1表示没有超出流量大小。
if(“0”.equals(result.get(0).toString())){
fullBack();
return null;
}
//获取到令牌,继续向下执行
return proceedingJoinPoint.proceed();
}
private void fullBack() {
response.setHeader(“Content-Type” ,“text/html;charset=UTF8”);
PrintWriter writer = null;
try{
writer = response.getWriter();
writer.println(“回退失败,请稍后阅读。。。”);
writer.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
if(writer != null){
writer.close();
}
}
}
}
上述代码会读取项目classpath目录下的limit.lua脚本文件来确定是否执行限流的操作,调用limit.lua文件执行的结果返回0则表示执行限流逻辑,否则不执行限流逻辑。既然,项目中需要使用Lua脚本,那么,接下来,我们就需要在项目中创建Lua脚本。
创建limit.lua脚本文件
在项目的classpath目录下创建limit.lua脚本文件,文件的内容如下所示。
local key = KEYS[1] --限流KEY(一秒一个)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call(‘get’, key) or “0”)
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置2秒过期
redis.call(“INCRBY”, key, “1”)
redis.call(“expire”, key “2”)
return 1
end
limit.lua脚本文件的内容比较简单,这里就不再赘述了。
接口添加注解
注解类、解析注解的切面类、Lua脚本文件都已经准备好。那么,接下来,我们在PayController类中在sendMessage2()方法上添加@MyRedisLimiter注解,并且将limit属性设置为10,如下所示。
@MyRedisLimiter(limit = 10)
@RequestMapping(“/boot/send/message2”)
public String sendMessage2(){
//记录返回接口
String result = “”;
boolean flag = messageService.sendMessage(“恭喜您成长值+1”);
if (flag){
result = “短信发送成功!”;
return result;
}
result = “哎呀,服务器开小差了,请再试一下吧”;
return result;
}
此处,我们限制了sendMessage2()方法,每秒钟最多只能处理10个请求。那么。接下来,我们就使用JMeter对sendMessage2()进行测试。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
面试题文档来啦,内容很多,485页!
由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。
1111道Java工程师必问面试题
MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:
Elasticsearch 24 题 +Memcached + Redis 40题:
Spring 26 题+ 微服务 27题+ Linux 45题:
Java面试题合集:
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
面只截取部分内容展示。**
1111道Java工程师必问面试题
[外链图片转存中…(img-vGLLa3rw-1713428367312)]
MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:
[外链图片转存中…(img-9BaSq6JS-1713428367312)]
Elasticsearch 24 题 +Memcached + Redis 40题:
[外链图片转存中…(img-CxBrmHo2-1713428367312)]
Spring 26 题+ 微服务 27题+ Linux 45题:
[外链图片转存中…(img-M09Dm0Bo-1713428367312)]
Java面试题合集:
[外链图片转存中…(img-RLdmIlF6-1713428367313)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!