限流和单元测试的例子

677 阅读2分钟

前言

最近在看限流的一些知识,并想写下单元测试。

springboot单元测试注意事项

image.png

1.注意单元测试要写的位置,项目搭建后会自动有test的包,只有在test下面才能使用测试类;
2.注解中的@SpringBootTest(classes = DemoApplication.class)这里要写启动类

限流

为什么限流

防止恶意请求浪费服务器资源
防止数据请求量过大造成服务器宕机

限流的方法

看了一些资料后限流主要分为:
  漏桶算法 就是流入桶的速率大于流出的速度,这样才能保证桶里面有数据
         严格限制速率的,超过的速率会被打回的
         
  令牌桶法 一定时间内分配好令牌个数,然后再这段时间内消费这些令牌
        桶中有令牌就可以提供服务,无令牌则拒绝
        
  借助redis的请求数量控制

例子

单机版方式 RateLimiter

这里可以写成注解的方式放到路由上控制访问量,再过滤器中获取令牌看是否要拒绝请求
jar包
<dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>31.0-jre</version>
</dependency>
测试使用
public class RateGuavaTest1 implements Runnable {
    //每秒中产生5个令牌
    private static RateLimiter limiter = RateLimiter.create(5);
	 
    public static void exec() {
        /**这里还有有个预消费机制 
         * limiter.acquire(10);
         * 请求数量大于产生的数量会慢慢还上的
         */
        limiter.acquire(1);
        System.out.println("--" + System.currentTimeMillis() / 1000);
    }
    
    @Override
	public void run() {
	   limiter.acquire(1);
	   System.out.println(Thread.currentThread().getName()+":" + System.currentTimeMillis() / 1000);
	}
    
    public static void main(String[] args) {
//    	for(int i=0;i<100;i++) {
//		 RateGuavaTest1.exec();
//    	}
    	
    	for(int i=0;i<100;i++) {
    		Thread thread=new Thread(new RateGuavaTest1());
    		thread.start();
    	}
	}
}

image.png

方法摘要
修饰符和类型方法和描述
doubleacquire() 从RateLimiter获取一个许可,该方法会被阻塞直到获取到请求
doubleacquire(int permits) 从RateLimiter获取指定许可数,该方法会被阻塞直到获取到请求
static RateLimitercreate(double permitsPerSecond) 根据指定的稳定吞吐率创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少查询)
static RateLimitercreate(double permitsPerSecond, long warmupPeriod, TimeUnit unit) 根据指定的稳定吞吐率和预热期来创建RateLimiter,这里的吞吐率是指每秒多少许可数(通常是指QPS,每秒多少个请求量),在这段预热时间内,RateLimiter每秒分配的许可数会平稳地增长直到预热期结束时达到其最大速率。(只要存在足够请求数来使其饱和)
doublegetRate() 返回RateLimiter 配置中的稳定速率,该速率单位是每秒多少许可数
voidsetRate(double permitsPerSecond) 更新RateLimite的稳定速率,参数permitsPerSecond 由构造RateLimiter的工厂方法提供。
StringtoString() 返回对象的字符表现形式
booleantryAcquire() 从RateLimiter 获取许可,如果该许可可以在无延迟下的情况下立即获取得到的话
booleantryAcquire(int permits) 从RateLimiter 获取许可数,如果该许可数可以在无延迟下的情况下立即获取得到的话
booleantryAcquire(int permits, long timeout, TimeUnit unit) 从RateLimiter 获取指定许可数如果该许可数可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可数的话,那么立即返回false (无需等待)
booleantryAcquire(long timeout, TimeUnit unit) 从RateLimiter 获取许可如果该许可可以在不超过timeout的时间内获取得到的话,或者如果无法在timeout 过期之前获取得到许可的话,那么立即返回false(无需等待)

集群借助redis的实现方式

踩坑

注意事项:
        这里用了redis的事务,所以再代码中返回的数据是提交执行后统一返回的。
        如果再事务执行中写了返回值
        Boolea addResut=zset.add(key, currentTime+":"+Thread.currentThread().getName(), currentTime);
        会报空指针异常的

/**
	 * @param key 限流
	 * @param maxAllow 最大允许
	 * @param time 间隔时间毫秒
	 * @return
	 */
	public boolean isRateAllow(String key,int maxAllow,int time) {
		redisTemplateString.multi();
		long currentTime=System.currentTimeMillis();
		ZSetOperations<String, Object> zset=redisTemplateString.opsForZSet();
                //添加请求
		zset.add(key, currentTime+":"+Thread.currentThread().getName(), currentTime);
            //移除到这段时间差的请求   
	    zset.removeRangeByScore(key, 0,currentTime-time);
            //统计当前的请求数量
	    zset.zCard(key);
	    List<Object> result=redisTemplateString.exec();
	    Long size=(Long)result.get(2);
	    System.out.println(result+"   "+Thread.currentThread().getName()+":"+size);
		return (Long)result.get(2)<=maxAllow;
	}

Redis-Cell

Redis4.0提供来一个限流Redis模块 —— Redis-Cell。该模块使用来漏斗算法,并提供了原子的限流指令。

该模块只有1条指令cl.throttle,它的参数和返回值都略显复杂,接下来让我们来看看这个指令具体该如何使用。

> cl.throttle laoqian:reply 15 30 60 1
                      ▲     ▲  ▲  ▲  ▲
                      |     |  |  |  └───── need 1 quota (可选参数,默认值也是1)
                      |     |  └──┴─────── 30 operations / 60 seconds 这是漏水速率
                      |     └───────────── 15 capacity 这是漏斗容量
                      └─────────────────── key laoqian

上面这个指令的意思是允许「用户老钱回复行为」的频率为每 60s 最多 30 次(漏水速率),漏斗的初始容量为 15,也就是说一开始可以连续回复 15 个帖子,然后才开始受漏水速率的影响。我们看到这个指令中漏水速率变成了 2 个参数,替代了之前的单个浮点数。用两个参数相除的结果来表达漏水速率相对单个浮点数要更加直观一些。

> cl.throttle laoqian:reply 15 30 60
1) (integer) 0   # 0 表示允许,1表示拒绝
2) (integer) 15  # 漏斗容量capacity
3) (integer) 14  # 漏斗剩余空间left_quota
4) (integer) -1  # 如果拒绝了,需要多长时间后再试(漏斗有空间了,单位秒)
5) (integer) 2   # 多长时间后,漏斗完全空出来(left_quota==capacity,单位秒)