Springboot 小巧简便的限流器使用 RateLimiter

简介: Springboot 小巧简便的限流器使用 RateLimiter

正文



image.png


向技术致敬的最佳方案: 给予技术分享传播者一个点赞、收藏 。

(方案不是很成熟,但是可以尝试)


开搞:


① 引入相关依赖,pom.xml  :


        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


② (其实就是使用guava的,简单做了一层业务包装)MyRateLimiter.java :


vimport com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;
@Component
public class MyRateLimiter {
    /**
     * 账号注册限流器 每秒只发出5个令牌
     */
    private RateLimiter accountRegisterRateLimiter = RateLimiter.create(5.0);
    /**
     * 短信发送限流器    每秒只发出3个令牌
     */
    private RateLimiter smsSendRateLimiter = RateLimiter.create(3.0);
    /**
     * 尝试获取令牌,返回尝试结果
     *
     * @return
     */
    public boolean tryAccountRegisterAcquire() {
        return accountRegisterRateLimiter.tryAcquire();
    }
    /**
     * 取令牌,暂时取不到会一直去尝试
     * @return
     */
    public double accountRegisterAcquire() {
        return accountRegisterRateLimiter.acquire();
    }
    /**
     * 尝试获取令牌
     *
     * @return
     */
    public boolean trySmsSendAcquire() {
        return smsSendRateLimiter.tryAcquire();
    }
}

代码简析:


image.png


源码解析:


用的简单,但是我们需要简单看看源码,方便我们可以根据业务场景做相关调整。


image.png


简单翻译一下两个用的比较多的create函数里面的注释:


方法 一


public static RateLimiter create(double permitsPerSecon);


咱大白话翻译(其实源码上有注释,还有举例):


保证每秒处理不超过 permitsPerSecond个请求。

如果每秒请求数爆炸,超过我们设置的permitsPerSecond 数量,会慢慢处理。

如果每秒请求书很少,这个permitsPerSecond相当于令牌,会囤积起来,最多囤积permitsPerSecond个。


方法 二


public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) ;


咱大白话翻译(其实源码上有注释,还有举例):


保证了平均每秒不超过permitsPerSecond个请求。

但是这个创建出来的限流器有一个热身期(warmup period)。

热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率。

同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态。

玩过阿里的那个 Sentinel组件的话,应该对这种热身限流策略不会陌生,其实限流策略都是这几种,万变不离其宗。
设计这个的意图是为了满足那种资源提供方需要热身时间,
而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间。


再来简单看看 尝试获取令牌的 tryAcquire函数


//尝试获取一个令牌,立即返回尝试结果


public boolean tryAcquire();


//尝试获取 permits 个令牌,立即返回尝试结果

public boolean tryAcquire(int permits);


//尝试获取一个令牌,带超时时间传参

public boolean tryAcquire(long timeout, TimeUnit unit);


//尝试获取permits个令牌,带超时时间传参

public boolean tryAcquire(int permits, long timeout, TimeUnit unit);


再看看 获取令牌的 acquire函数


//默认 permits是 1 ,也就是默认拿1个令牌

public double acquire();

//令牌自己定,权重大一点的业务,也许需要拿3个令牌才能执行一次(举例)

public double acquire(int permits);

image.png


可以看到返回值是个dubbo ,其实这是一个等待的时间:


rateLimiter.acquire()该方法会阻塞线程,直到令牌桶中能取到令牌为止才继续向下执行,并返回等待的时间。


好了,开始结合实际案例玩一把 。


模拟场景,我们提供一个 账号注册接口,注册接口需要限流。


然后我们再模拟一个并发接口,多线程去调度 注册接口,模拟出 注册接口被短时间并发调用的场景,看看限流器RateLimiter 玩出来的效果。


首先写个简单的HTTP GET 请求调用函数:


HttpUtil.java


import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
public class HttpUtil {
    /**
     * get请求
     *
     * @param realUrl
     * @return
     */
    public static String sendGet(URL realUrl) {
        String result = "";
        BufferedReader in = null;
        try {
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            // 建立实际的连接
            connection.connect();
            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(
                    connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }
}


然后写个模拟的注册接口:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Controller
public class UserController {
    @Autowired
    MyRateLimiter myRateLimiter;
    @RequestMapping("/userRegister")
    @ResponseBody
    public String userRegister() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        //尝试获取令牌
        boolean acquire = myRateLimiter.tryAccountRegisterAcquire();
        System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+acquire);
        if (acquire) {
            try {
                //模拟业务执行500毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" +  dtf.format(LocalDateTime.now()) + "]";
        } else {
            return Thread.currentThread().getName()+"userRegister 被 limit 限制了 [" +  dtf.format(LocalDateTime.now())+ "]";
        }
    }
}

可以看到这个接口里面,我们当前只 玩了一下 尝试获取令牌函数 tryAcquire   。


OK,我们来 写个多线程调用接口,来看看这时候限流的效果:


import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import static com.example.dotest.util.HttpUtil.sendGet;
import static java.util.concurrent.Executors.*;
@RestController
public class TestController {
    ExecutorService fixedThreadPool = newFixedThreadPool(10);
    @RequestMapping("/test")
    public void test() throws MalformedURLException, InterruptedException {
        final URL url = new URL("http://localhost:8696/userRegister");
        for(int i=0;i<10;i++) {
            fixedThreadPool.submit(() -> System.out.println(sendGet(url)));
        }
        fixedThreadPool.shutdown();
        fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }
}


简析:


image.png


可以看到效果:


image.png



可以看到我们的代码,目前用的这个 tryAcquire 函数 ,返回boolean值 ,只要是拿不到令牌我们就直接不做处理了。


这时候其实也可以考虑这么使用,拿不到的 做一些降级、熔断操作或者重试等待啥的。


那么,我们如果想,尝试拿的时候拿不到,让线程自己去帮我们继续自旋去等待持续获取令牌呢?


这时候我们就需要用的是   acquire  函数    。


改造一下刚才的模拟接口:


    @RequestMapping("/userRegister")
    @ResponseBody
    public String userRegister() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
        //尝试获取令牌
        boolean tryAcquire = myRateLimiter.tryAccountRegisterAcquire();
        System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+tryAcquire);
        double registerAcquireWaitTime = myRateLimiter.accountRegisterAcquire();
        System.out.println(Thread.currentThread().getName() + "  坚持  获取令牌,被限制的时间是"+registerAcquireWaitTime);
        if (tryAcquire) {
            //模拟业务执行500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" +  dtf.format(LocalDateTime.now()) + "]";
        } else {
            return Thread.currentThread().getName()+"userRegister拿到令牌不是很顺利被 limit 过,但是还是拿到了 [" +  dtf.format(LocalDateTime.now())+ "]";
        }
    }
}

简析:


image.png


继续调用一下接口,看看这时候限流器的效果:


image.png


限流效果:


image.png

相关文章
|
18天前
|
缓存 Java Sentinel
Springboot 中使用 Redisson+AOP+自定义注解 实现访问限流与黑名单拦截
Springboot 中使用 Redisson+AOP+自定义注解 实现访问限流与黑名单拦截
|
8月前
|
Java
springboot实现自定义注解限流
springboot实现自定义注解限流
108 1
|
3月前
|
算法 NoSQL Java
springboot整合redis及lua脚本实现接口限流
springboot整合redis及lua脚本实现接口限流
91 0
|
2月前
|
前端开发 NoSQL Java
【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流
【SpringBoot】秒杀业务:redis+拦截器+自定义注解+验证码简单实现限流
|
3月前
|
存储 算法 Java
Spring Boot 通用限流方案
Spring Boot 通用限流方案
138 0
|
5月前
|
算法 NoSQL Java
8. 「Java大师」教你如何用Spring Boot轻松实现高效「限流」!
8. 「Java大师」教你如何用Spring Boot轻松实现高效「限流」!
129 0
|
监控 Java API
SpringBoot 2.0 + 阿里巴巴 Sentinel 动态限流实战
前言 在从0到1构建分布式秒杀系统和打造十万博文系统中,限流是不可缺少的一个环节,在系统能承受的范围内既能减少资源开销又能防御恶意攻击。 在前面的文章中,我们使用了开源工具包 Guava 提供的限流工具类 RateLimiter 和 OpenResty 的 Lua 脚本分别进行 API 和应用层面的限流。
2746 0
|
8月前
|
NoSQL Java Redis
springboot高级教程基于 redis 通过注解实现限流
springboot高级教程基于 redis 通过注解实现限流
102 0
|
10月前
|
NoSQL 前端开发 Java
Springboot搭配Redis实现接口限流
介绍 限流的需求出现在许多常见的场景中: 秒杀活动,有人使用软件恶意刷单抢货,需要限流防止机器参与活动 某 api 被各式各样系统广泛调用,严重消耗网络、内存等资源,需要合理限流 淘宝获取 ip 所在城市接口、微信公众号识别微信用户等开发接口,免费提供给用户时需要限流,更 具有实时性和准确性的接口需要付费。
114 0
|
11月前
|
NoSQL Java Redis
SpringBoot中如何实现限流,这种方式才叫优雅!
SpringBoot中如何实现限流,这种方式才叫优雅!
231 0