博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
基于 Hystrix 高并发服务限流第 6 篇 —— 服务限流,基于 RateLimiter 实现
阅读量:2094 次
发布时间:2019-04-29

本文共 7622 字,大约阅读时间需要 25 分钟。

查看之前的博客可以点击顶部的【分类专栏】

 

什么是服务限流?

服务限流就是对接口访问进行限制,常用服务限流算法令牌桶、漏桶。计数器也可以进行粗暴限流实现。更多详情可以查看博客:

 

本篇博客主要讲解令牌桶算法的限流。

在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。

 

使用 RateLimiter 实现令牌桶限流

RateLimiter是 guava (Google)提供的基于令牌桶算法的实现类,可以非常简单的完成接口限流,并且根据系统的实际情况来调整生成token的速率。通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。

 

我们创建一个 limit-server 项目

pom.xml:

4.0.0
com.study
limit-server
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.2.2.RELEASE
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
com.google.guava
guava
30.0-jre
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR2
pom
import

我们创建一个 controller,代码如下:

package com.study.controller;import com.google.common.util.concurrent.RateLimiter;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.TimeUnit;/** * @author biandan * @description * @signature 让天下没有难写的代码 * @create 2021-06-21 下午 12:59 */@RestControllerpublic class LimitController {    //表示每秒钟生成1个令牌存入令牌桶中    RateLimiter rateLimiter = RateLimiter.create(1.0);    //模拟秒杀下单场景    @GetMapping("/order")    public String order() {        String result = "";        //限制在 500 毫秒内获取令牌,无法获取返回 false        boolean flag = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);        if (flag) {            result = "恭喜您,抢到了一个惊喜!";            //然后实现业务逻辑        } else {            result = "没抢到,等会再试试!";        }        System.out.println(result);        return result;    }}

bootstrap.yml

server:  port: 80spring:  application:    name: limit-server# 将SpringBoot项目作为单实例部署调试时,不需要注册到注册中心eureka:  client:    fetch-registry: false    register-with-eureka: false

 

编写启动类。启动测试:

控制台输出:

 

然后不断的刷新前端页面,测试:

 

这就是令牌桶算法的结果。

 

封装 RateLimiter 令牌桶算法

OK,我们尝试封装一下令牌桶算法,只需要在方法里添加一个注解即可。然后令牌桶的存入桶的速率可以配置。我们的令牌桶是针对某一个接口而制定的,也就是说添加了我们自定义的注解之后,就为该接口开辟了令牌桶算法。

我们用到 AOP,因此在 pom.xml 增加依赖:

org.springframework.boot
spring-boot-starter-aop

 

步骤1:自定义注解

package com.study.annotation;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;/** * @author biandan * @description * @signature 让天下没有难写的代码 * @create 2021-06-21 下午 3:18 */@Target(value = ElementType.METHOD)//表示此注解用在方法上@Retention(RetentionPolicy.RUNTIME)//注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在public @interface ExtRateLimiter {    /**     * 每秒添加的数量     * @return     */    double value();    /**     * 限制等待时间     * @return     */    long limitTime();}

 

步骤2:编写 AOP 切面

package com.study.aop;import com.google.common.util.concurrent.RateLimiter;import com.study.annotation.ExtRateLimiter;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.springframework.stereotype.Component;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.TimeUnit;/** * @author biandan * @description * @signature 让天下没有难写的代码 * @create 2021-06-21 下午 3:24 */@Aspect@Componentpublic class RateLimiterAOP {    //某个接口对应的令牌桶    private static ConcurrentHashMap
rateLimiterMap = new ConcurrentHashMap<>(); //定义切面,拦截 controller 层 @Pointcut("execution(public * com.study.controller.*.*(..))") public void myPointCut() { } //环绕通知(目前主要用于抢单的方法上) @Around("myPointCut()") public Object aroundAOP(ProceedingJoinPoint joinPoint) throws Throwable { //判断方法上是否有注解 @ExistsApiToken MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); ExtRateLimiter extRateLimiter = methodSignature.getMethod().getDeclaredAnnotation(ExtRateLimiter.class); //如果不包含该注解,正常往下执行即可 if (extRateLimiter != null) { //获取注解上的参数,配置固定速率 double value = extRateLimiter.value(); long limitTime = extRateLimiter.limitTime(); RateLimiter rateLimiter = getRateLimiter(value); //判断令牌桶获取 token 是否超时 boolean acquire = rateLimiter.tryAcquire(limitTime, TimeUnit.MILLISECONDS); //如果超时 if (!acquire) { serviceFallBack(); return null; } } return joinPoint.proceed(); } //获取 RateLimiter 对象 private RateLimiter getRateLimiter(double value) { //获取当前URL ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String requestURI = request.getRequestURI(); RateLimiter rateLimiter = null; if (!rateLimiterMap.containsKey(requestURI)) { //开启令牌限流 rateLimiter = RateLimiter.create(value);//独立线程 rateLimiterMap.put(requestURI, rateLimiter); } else { rateLimiter = rateLimiterMap.get(requestURI); } return rateLimiter; } //超时,服务降级 private void serviceFallBack() throws IOException { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletResponse response = attributes.getResponse(); response.setHeader("Content-type", "text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); String msg = "目前活动太火爆,抢购人数较多,您未能抢到,请稍后重试~"; System.out.println(msg); try { writer.println(msg); } catch (Exception e) { e.printStackTrace(); } finally { writer.close(); } }}

 

然后 controller  的代码就简洁了,如下:

package com.study.controller;import com.study.annotation.ExtRateLimiter;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;/** * @author biandan * @description * @signature 让天下没有难写的代码 * @create 2021-06-21 下午 12:59 */@RestControllerpublic class LimitController {    //模拟秒杀下单场景    @GetMapping("/order")    @ExtRateLimiter(value = 2, limitTime = 500)    public String order() {        String result = "恭喜您,抢到了一个惊喜!";        System.out.println(result);        return result;    }}

重启,测试:

 

OK,关于高并发服务限流讲解到这。

 

转载地址:http://dkuhf.baihongyu.com/

你可能感兴趣的文章
Redis运维利器 -- RedisManager
查看>>
分布式之REDIS复习精讲
查看>>
分布式之数据库和缓存双写一致性方案解析
查看>>
Redis集群
查看>>
Oracle 查看和扩展表空间
查看>>
记一次线上Java程序导致服务器CPU占用率过高的问题排除过程
查看>>
Java 内存溢出(java.lang.OutOfMemoryError)的常见情况和处理方式总结
查看>>
从cpu和内存来理解为什么数组比链表查询快
查看>>
CentOS7下使用YUM安装MySQL5.6
查看>>
JVM内存空间
查看>>
Docker 守护进程+远程连接+安全访问+启动冲突解决办法 (完整收藏版)
查看>>
从零写分布式RPC框架 系列 2.0 (4)使用BeanPostProcessor实现自定义@RpcReference注解注入
查看>>
Java 设计模式 轻读汇总版
查看>>
Paxos学习笔记及图解
查看>>
深入解析Spring使用枚举接收参数和返回值机制并提供自定义最佳实践
查看>>
数据序列化框架——Kryo
查看>>
布隆过滤器(BloomFilter)——应用(三)
查看>>
MPP架构数据库优化总结——华为LibrA(MPPDB、GuassDB)与GreenPlum
查看>>
Spark代码可读性与性能优化——示例七(构建聚合器,以用于复杂聚合)
查看>>
Spark代码可读性与性能优化——示例八(一个业务逻辑,多种解决方式)
查看>>