目录
固定窗口计数法
优点:和令牌桶相比,这种算法不需要去等待令牌生成的时间,在新的时间窗口,可以立即处理大量的请求。
缺点:某 API 接口设置每分钟最多允许 100 次请求。若在 0:59 秒发起 100 次请求,1:00 秒后又立即发起 100 次请求,总流量瞬间达到 200 次,导致服务过载。
java实现的固定窗口计数法:
package common.flowLimit;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author: weijie
* @Date: 2020/9/23 18:02
* @Description:
* @url: https://blog.youkuaiyun.com/king0406/article/details/103129530?
*/
public class WindowLimiter {
Logger log = LoggerFactory.getLogger(WindowLimiter.class);
//本地缓存,以时间戳为key,以原子类计数器为value
private LoadingCache<Long, AtomicLong> counter =
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>() {
@Override
public AtomicLong load(Long seconds) throws Exception {
return new AtomicLong(0);
}
});
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//设置限流阈值为15
private long limit = 15;
/**
* 固定时间窗口
* 每隔5s,计算时间窗口内的请求数量,判断是否超出限流阈值
*/
@Test
public void run(){
while (true){
fixWindow();
}
}
private void fixWindow() {
scheduledExecutorService.scheduleWithFixedDelay(() -> {
try {
// time windows 5 s
long time = System.currentTimeMillis() / 5000;
//模拟每秒发送随机数量的接口请求
int reqCount = (int) (Math.random() * 5) + 1;
long num = counter.get(time).addAndGet(reqs);
log.info("time=" + time + ",num=" + num);
if (num > limit) {
log.info("限流了,num=" + num);
}
} catch (Exception e) {
log.error("fixWindow error", e);
} finally {
}
}, 0, 1000, TimeUnit.MILLISECONDS);
}
}
基于redis分布式固定窗口计数法:
package flowLimit;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Random;
/**
* @author: weijie
* @Date: 2020/9/23 18:11
* @url: https://blog.youkuaiyun.com/king0406/article/details/103130327
*/
public class WindowLimiterByRedisTest {
Logger logger = LoggerFactory.getLogger(WindowLimiterByRedisTest.class);
JedisPool jedisPool;
@Before
public void init(){
String host = "39.96.204.209";
int port = 6379;
jedisPool = new JedisPool(host, port);
}
@Test
public void run(){
/*
每次请求进来,查询一下当前的计数值,如果超出请求数阈值,则拒绝请求,返回系统繁忙提示
*/
Jedis redis = jedisPool.getResource();
redis.auth("123456");
long limit = 10;
while (true){
String request = "flow:api";
long count = 0;
try {
count = limitFlow(redis, request);
//超过限流
if (count > limit){
logger.error("当前访问过于频道,请稍后再试");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
logger.info("请求放行,执行业务处理");
}
}catch (Exception e){
e.printStackTrace();
}
}
}
private long limitFlow(Jedis jedis, String key) {
//Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功返回1,设置失败返回0
Long lng = jedis.setnx(key, "1");
if (lng == 1) {
//设置时间窗口,redis-key时效为10秒
jedis.expire(key, 10);
return 1L;
} else {
//Redis Incrby 命令将 key 中储存的数字加上指定的增量值。相当于放在redis中的计数器,每次请求到来计数器自增1
System.out.println("key: " + key);
String va = jedis.get(key);
System.out.println("value: " + va);
long val = jedis.incr(key);
System.out.println("result: " + val);
return val;
}
}
}
滑动窗口算法
- 原理:窗口随时间滑动,统计窗口内请求数。
- 优点:平滑控制流量,解决固定窗口的边界问题。
- 举例:社交平台限制用户每分钟最多发布 5 条动态。将 1 分钟划分为 6 个 10 秒的子窗口,每个子窗口最多允许 1 条动态,有效避免用户在窗口切换时的刷屏行为。
通过redis zset数据结构实现:
思想:score存储的是时间戳,value存储的是uuid,利用range方法统计滑动窗口内有多少次请求,超过请求数量则终止
缺点:存储uuid太多占用内存,可能导致出现大key的问题
public Response limitFlow(){
Long currentTime = new Date().getTime();
System.out.println(currentTime);
if(redisTemplate.hasKey("limit")) {
Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime - intervalTime, currentTime).size(); // intervalTime是限流的时间
System.out.println(count);
if (count != null && count > 5) {
return Response.ok("每分钟最多只能访问5次");
}
}
redisTemplate.opsForZSet().add("limit",UUID.randomUUID().toString(),currentTime);
return Response.ok("访问成功");
}
漏斗桶算法
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
- 原理:请求进入漏斗后以恒定速率流出
- 缺点:处理突发流量能力较弱。
- 举例:视频平台限制用户每秒最多上传 2MB 数据。即使某用户瞬间发起 10MB 的上传请求,系统仍按每秒 2MB 的速率处理,导致请求排队时间过长。
漏洞的容量是有限的,如果将漏嘴堵住,然后一直往里面灌水,它就会变满,直至再也装不进去。如果将漏嘴放开,水就会往下流,流走一部分之后,就又可以继续往里面灌水。如果漏嘴流水的速率大于灌水的速率,那么漏斗永远都装不满。如果漏嘴流水速率小于灌水的速率,那么一旦漏斗满了,灌水就需要暂停并等待漏斗腾空。
参考:高并发系统限流-漏桶算法和令牌桶算法 - 秦羽的思考 - 博客园
实现一:基于guava实现
package flowLimit;
import com.google.common.util.concurrent.RateLimiter;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
/**
* @author: weijie
* @Date: 2020/9/23 19:15
* @url:https://blog.youkuaiyun.com/cailianren1/article/details/85283044
*/
public class LeakyBucketLimitTest {
/**
* @param qps 平均qps 控制接口的响应速率,响应速率越快处理请求越多
* @param countOfReq 桶的大小,接受请求的最大值
* @return
*/
public RateLimiter createLeakyBucket(int qps, int countOfReq){
return RateLimiter.create(qps,countOfReq, TimeUnit.MILLISECONDS);
}
@Test
public void run(){
RateLimiter leakyBucket = createLeakyBucket(100, 1000);
long start = System.currentTimeMillis()/1000;
int countRequest = 200;
for (int i = 0; i < countRequest; i++){
// System.out.println("请求过来");
leakyBucket.acquire();
// System.out.println("业务处理");
}
long spend = System.currentTimeMillis()/1000 - start;
System.out.println("处理的请求数量:" + countRequest +"," +
""+"耗时:" + spend + "s " +",qps:" + leakyBucket.getRate()+",实际qps:"+
Math.ceil(countRequest/(spend)));
}
}
令牌桶算法
- 原理:以恒定速率生成令牌,请求需获取令牌才能通过,支持突发流量。
- 举例:电商秒杀活动中,系统设置令牌桶容量为 1000,每秒生成 100 个令牌。用户请求需获取令牌才能参与抢购,既能应对瞬时高并发,又能确保公平性
实现一:基于guava实现
guava的RateLimiter,用在处理请求时候,从桶中申请令牌,申请到了就成功响应,申请不到时直接返回失败;
package common.guava;
import com.google.common.util.concurrent.RateLimiter;
import org.junit.Test;
public class RateLimitTest {
@Test
public void use1(){
RateLimiter rateLimiter = RateLimiter.create(5.0);
for (int i = 0 ; i < 20; i++){
//尝试获取令牌
if (rateLimiter.tryAcquire()){
System.out.println("获取令牌成功");
//模拟业务执行
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}else {
System.out.println("获取令牌失败");
}
}
}
@Test
public void test2(){
RateLimiter rateLimiter = RateLimiter.create(5);
long start = System.currentTimeMillis()/1000;
for (int i = 0 ; i < 10; i++){
System.out.println("----start----");
//阻塞式放行
rateLimiter.acquire();
System.out.println("放行");
System.out.println("-----end-----");
}
long end = System.currentTimeMillis() / 1000;
System.out.println(String.format("耗时:%d s", (end - start)));
}
}
实现二:基于redis的list实现
思想:令牌桶算法,限流触发的时机是输出速率>输入速率,就触发限流了,此时我们可以用list的lpop、rpush方式,输入端通过@schedule java代码定时通过rpush去写uuid,请求过来的时候输出端通过lpop获取令牌,如果拿到令牌正常访问,拿不到则触发限流
第一步:依靠List的leftPop来获取令牌
// 输出令牌
public Response limitFlow2(Long id){
Object result = redisTemplate.opsForList().leftPop("limit_list");
if(result == null){
return Response.ok("当前令牌桶中无令牌");
}
return Response.ok(articleDescription2);
}
第二步:
再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成
// 10S的速率往令牌桶中添加UUID,只为保证唯一性
@Scheduled(fixedDelay = 10_000,initialDelay = 0)
public void setIntervalTimeTask(){
redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
}
参考文章: