主流的四种限流策略,我都可以通过redis实现,干货仅此一篇

  • 所以我们的接口需要对流量进行限制。俗称的QPS也是对流量的一种描述

  • 针对限流现在大多应该是令牌桶算法,因为它能保证更多的吞吐量。除了令牌桶算法还有他的前身漏桶算法和简单的计数算法

  • 下面我们来看看这四种算法

固定时间窗口算法


  • 固定时间窗口算法也可以叫做简单计数算法。网上有很多都将计数算法单独抽离出来。但是笔者认为计数算法是一种思想,而固定时间窗口算法是他的一种实现

  • 包括下面滑动时间窗口算法也是计数算法的一种实现。因为计数如果不和时间进行绑定的话那么失去了限流的本质了。就变成了拒绝了

主流的四种限流策略,我都可以通过redis实现

优点

  • 在固定的时间内出现流量溢出可以立即做出限流。每个时间窗口不会相互影响

  • 在时间单元内保障系统的稳定。保障的时间单元内系统的吞吐量上限

缺点

  • 正如图示一样,他的最大问题就是临界状态。在临界状态最坏情况会受到两倍流量请求

  • 除了临界的情况,还有一种是在一个单元时间窗内前期如果很快地消耗完请求阈值。那么剩下的时间将会无法请求。这样就会因为一瞬间的流量导致一段时间内系统不可用。这在互联网高可用的系统中是不能接受的。

实现

  • 好了,关于原理介绍及优缺点我们已经了解了。下面我们动手实现它

  • 首先我们在实现这种计数时,采用redis是非常好的选择。这里我们通过redis实现

controller

@RequestMapping(value = “/start”,method = RequestMethod.GET)

public Map<String,Object> start(@RequestParam Map<String, Object> paramMap) {

return testService.startQps(paramMap);

}

service

@Override

public Map<String, Object> startQps(Map<String, Object> paramMap) {

//根据前端传递的qps上线

Integer times = 100;

if (paramMap.containsKey(“times”)) {

times = Integer.valueOf(paramMap.get(“times”).toString());

}

String redisKey = “redisQps”;

RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey,
redisTemplate.getConnectionFactory());

int no = redisAtomicInteger.getAndIncrement();

//设置时间固定时间窗口长度 1S

if (no == 0) {

redisAtomicInteger.expire(1, TimeUnit.SECONDS);

}

//判断是否超限 time=2 表示qps=3

if (no > times) {

throw new RuntimeException(“qps refuse request”);

}

//返回成功告知

Map<String, Object> map = new HashMap<>();

map.put(“success”, “success”);

return map;

}

结果测试

主流的四种限流策略,我都可以通过redis实现

  • 我们设置的qps=3 , 我们可以看到五个并发进来后前三个正常访问,后面两个就失败了。稍等一段时间我们在并发访问,前三个又可以正常访问。说明到了下一个时间窗口

主流的四种限流策略,我都可以通过redis实现

滑动时间窗口算法


  • 针对固定时间窗口的缺点–临界值出现双倍流量问题。 我们的滑动时间窗口就产生了。

  • 其实很好理解,就是针对固定时间窗口,将时间窗口统计从原来的固定间隔变成更加细度化的单元了。

  • 在上面我们固定时间窗口演示中我们设置的时间单元是1S 。 针对1S我们将1S拆成时间戳。

  • 固定时间窗口是统计单元随着时间的推移不断向后进行。而滑动时间窗口是我们认为的想象出一个时间单元按照相对论的思想将时间固定,我们的抽象时间单元自己移动。抽象的时间单元比实际的时间单元更小。

  • 读者可以看下下面的动图,就可以理解了。

主流的四种限流策略,我都可以通过redis实现

优点

  • 实质上就是固定时间窗口算法的改进。所以固定时间窗口的缺点就是他的优点。

  • 内部抽象一个滑动的时间窗,将时间更加小化。存在边界的问题更加小。客户感知更弱了。

缺点

  • 不管是固定时间窗口算法还是滑动时间窗口算法,他们都是基于计数器算法进行优化,但是他们对待限流的策略太粗暴了。

  • 为什么说粗暴呢,未限流他们正常放行。一旦达到限流后就会直接拒绝。这样我们会损失一部分请求。这对于一个产品来说不太友好

实现

  • 滑动时间窗口是将时间更加细化,上面我们是通过redis#setnx实现的。这里我们就无法通过他统一记录了。我们应该加上更小的时间单元存储到一个集合汇总。然后根据集合的总量计算限流。redis的zsett数据结构就和符合我们的需求。

  • 为什么选择zset呢,因为redis的zset中除了值以外还有一个权重。会根据这个权重进行排序。如果我们将我们的时间单元及时间戳作为我们的权重,那么我们获取统计的时候只需要按照一个时间戳范围就可以了。

  • 因为zset内元素是唯一的,所以我们的值采用uuid或者雪花算法一类的id生成器

controller

@RequestMapping(value = “/startList”,method = RequestMethod.GET)

public Map<String,Object> startList(@RequestParam Map<String, Object> paramMap) {

return testService.startList(paramMap);

}

service

String redisKey = “qpsZset”;

Integer times = 100;

if (paramMap.containsKey(“times”)) {

times = Integer.valueOf(paramMap.get(“times”).toString());

}

long currentTimeMillis = System.currentTimeMillis();

long interMills = inter * 1000L;

Long count = redisTemplate.opsForZSet().count(redisKey, currentTimeMillis - interMills, currentTimeMillis);

if (count > times) {

throw new RuntimeException(“qps refuse request”);

}

redisTemplate.opsForZSet().add(redisKey, UUID.randomUUID().toString(), currentTimeMillis);

Map<String, Object> map = new HashMap<>();

map.put(“success”, “success”);

return map;

结果测试

主流的四种限流策略,我都可以通过redis实现

  • 和固定时间窗口采用相同的并发。为什么上面也会出现临界状况呢。因为在代码里时间单元间隔比固定时间间隔采用还要大 。 上面演示固定时间窗口时间单元是1S出现了最坏情况。而滑动时间窗口设计上就应该间隔更短。而我设置成10S 也没有出现坏的情况

  • 这里就说明滑动比固定的优处了。如果我们调更小应该更加不会出现临界问题,不过说到底他还是避免不了临界出现的问题

漏桶算法


  • 滑动时间窗口虽然可以极大程度地规避临界值问题,但是始终还是避免不了

  • 另外时间算法还有个致命的问题,他无法面对突如其来的大量流量,因为他在达到限流后直接就拒绝了其他额外流量

  • 针对这个问题我们继续优化我们的限流算法。 漏桶算法应运而生

主流的四种限流策略,我都可以通过redis实现

优点

  • 面对限流更加的柔性,不再粗暴的拒绝。

  • 增加了接口的接收性

  • 保证下流服务接收的稳定性。均匀下发

缺点

  • 我觉得没有缺点。非要鸡蛋里挑骨头那我只能说漏桶容量是个短板

实现

controller

@RequestMapping(value = “/startLoutong”,method = RequestMethod.GET)

public Map<String,Object> startLoutong(@RequestParam Map<String, Object> paramMap) {

return testService.startLoutong(paramMap);

}

service

  • 在service中我们通过redis的list的功能模拟出桶的效果。这里代码是实验室性质的。在真实使用中我们还需要考虑并发的问题

@Override

public Map<String, Object> startLoutong(Map<String, Object> paramMap) {

String redisKey = “qpsList”;

Integer times = 100;

if (paramMap.containsKey(“times”)) {

times = Integer.valueOf(paramMap.get(“times”).toString());

}

Long size = redisTemplate.opsForList().size(redisKey);

if (size >= times) {

throw new RuntimeException(“qps refuse request”);

}

Long aLong = redisTemplate.opsForList().rightPush(redisKey, paramMap);

if (aLong > times) {

//为了防止并发场景。这里添加完成之后也要验证。 即使这样本段代码在高并发也有问题。此处演示作用

redisTemplate.opsForList().trim(redisKey, 0, times-1);

throw new RuntimeException(“qps refuse request”);

}

Map<String, Object> map = new HashMap<>();

map.put(“success”, “success”);

return map;

}

  • 13
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值