【高并发】亿级流量场景下如何实现分布式限流?看完我彻底懂了!!(1)

1200页Java架构面试专题及答案

小编整理不易,对这份1200页Java架构面试专题及答案感兴趣劳烦帮忙转发/点赞

百度、字节、美团等大厂常见面试题

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

理论篇:《【高并发】如何实现亿级流量下的分布式限流?这些理论你必须掌握!!

==========================================================

算法篇:《【高并发】如何实现亿级流量下的分布式限流?这些算法你必须掌握!!

==========================================================

项目源码已提交到github:https://github.com/sunshinelyz/mykit-ratelimiter

本文是在《【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!》一文的基础上进行实现,有关项目的搭建可参见《【高并发】亿级流量场景下如何为HTTP接口限流?看完我懂了!!》一文的内容。小伙伴们可以关注【冰河技术】微信公众号来阅读上述文章。

前面介绍的限流方案有一个缺陷就是:它不是全局的,不是分布式的,无法很好的应对分布式场景下的大流量冲击。那么,接下来,我们就介绍下如何实现亿级流量下的分布式限流。

分布式限流的关键就是需要将限流服务做成全局的,统一的。可以采用Redis+Lua技术实现,通过这种技术可以实现高并发和高性能的限流。

Lua是一种轻量小巧的脚本编程语言,用标准的C语言编写的开源脚本,其设计的目的是为了嵌入到应用程序中,为应用程序提供灵活的扩展和定制功能。

Redis+Lua脚本实现分布式限流思路


我们可以使用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脚本实现的分布式限流案例。

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”)

最后

金三银四到了,送上一个小福利!

image.png

image.png

专题+大厂.jpg

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

–如果超出限流大小

return 0

else --请求数+1,并设置2秒过期

redis.call(“INCRBY”, key, “1”)

最后

金三银四到了,送上一个小福利!

[外链图片转存中…(img-UDi8gw3b-1715489712878)]

[外链图片转存中…(img-jg5ftImT-1715489712878)]

[外链图片转存中…(img-crAmEG3f-1715489712878)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值