互联网高并发之限流
RPC远程调用 可以跨平台 一般采用HTTP协议 底层使用socket技术 只要语言支持socket技术,就可以进行通讯
开放平台一般采用http协议,因为支持更多的语言
本地调用只支持Java语言与Java语言开发使用虚拟机与虚拟机之间通讯 rmi
高并发限流解决方案
为啥要限流?
秒杀 双十一 服务安全(流量攻击 DDOS) 雪崩效应
限流为了保护服务
高并发限流解决方案限流算法(令牌桶、漏桶、计数器)、应用层解决限流(Nginx)
限流算法
常见的限流算法有:令牌桶、漏桶。计数器也可以进行粗暴限流实现。
计数器
它是限流算法中最简单最容易的一种算法,比如我们要求某一个接口,1分钟内的请求不能超过10次,我们可以在开始时设置一个计数器,每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多,如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器
public class LimitService { private int limtCount = 60;// 限制最大访问的容量 AtomicInteger atomicInteger = new AtomicInteger(0); // 每秒钟 实际请求的数量 private long start = System.currentTimeMillis();// 获取当前系统时间 private int interval = 60;// 间隔时间60秒 public boolean acquire() { long newTime = System.currentTimeMillis(); if (newTime > (start + interval)) { // 判断是否是一个周期 start = newTime; atomicInteger.set(0); // 清理为0 return true; } atomicInteger.incrementAndGet();// i++; return atomicInteger.get() <= limtCount; } static LimitService limitService = new LimitService(); public static void main(String[] args) { ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); for (int i = 1; i < 100; i++) { final int tempI = i; newCachedThreadPool.execute(new Runnable() { public void run() { if (limitService.acquire()) { System.out.println("你没有被限流,可以正常访问逻辑 i:" + tempI); } else { System.out.println("你已经被限流呢 i:" + tempI); } } }); } } }
滑动窗口计数
滑动窗口计数有很多使用场景,比如说限流防止系统雪崩。相比计数实现,滑动窗口实现会更加平滑,能自动消除毛刺。
滑动窗口原理是在每次有访问进来时,先判断前 N 个单位时间内的总访问量是否超过了设置的阈值,并对当前时间片上的请求数 +1。
相对于传统的 对于临界值的打破 传统计数器缺点:临界问题 可能违背定义固定速率原则
每隔10s移动一次
令牌桶算法
Token guava限流 令牌算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
令牌桶算法(Token)
令牌桶分为两个动作
动作1 固定速录往桶中存入令牌
动作2 客户端如果想访问请求,先从桶中获取token
客户端只有拿到令牌 才可以 访问服务器端 这是关键哦,获取不到就走降级
桶中的令牌也会满了哦,满了就不在装了。不能无限装的,有容量。
一个存 一个取
使用RateLimiter实现令牌桶限流
RateLimiter是guava提供的基于令牌桶算法的实现类,可以非常简单的完成限流特技,并且根据系统的实际情况来调整生成token的速率。
通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。
pom:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.toov5</groupId>
<artifactId>springboot-guava</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
</dependencies>
</project>
service
package com.toov5.service; import org.springframework.stereotype.Service; @Service public class OrderService { public boolean addOrder() { System.out.println("db...正在操作订单表数据库"); return true; } }
controller
package com.toov5.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.google.common.util.concurrent.RateLimiter; import com.toov5.service.OrderService; @RestController public class IndexController { @Autowired private OrderService orderService; //create方法中传入一个参数 以秒为单位固定的速率值 1r/s 往桶中存入一个令牌 RateLimiter rateLimiter = RateLimiter.create(1); //独立线程!它自己是个线程 //相当于接口每秒只能接受一个客户端请求 @RequestMapping("/addOrder") public String addOrder() { //限流放在网关 获取到当前 客户端从桶中获取对应的令牌 结果表示从桶中拿到令牌等待时间 //如果获取不到令牌 就一直等待 double acquire = rateLimiter.acquire(); System.out.println("从桶中获取令牌等待时间"+acquire); //业务逻辑处理 boolean addOrderResult = orderService.addOrder(); if (addOrderResult) { return "恭喜抢购成功!"; } return "抢购失败!"; } }
启动:
package com.toov5; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
运行访问:
加速点击 后面就有等待了 存放到token 不够用了就得等待了
这样一直等待也不好 设置服务降级处理
规定时间内没有获取到临牌 走降级
修改添加:
package com.toov5.controller; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.google.common.util.concurrent.RateLimiter; import com.toov5.service.OrderService; @RestController public class IndexController { @Autowired private OrderService orderService; //create方法中传入一个参数 以秒为单位固定的速率值 1r/s 往桶中存入一个令牌 RateLimiter rateLimiter = RateLimiter.create(1); //独立线程!它自己是个线程 //相当于接口每秒只能接受一个客户端请求 @RequestMapping("/addOrder") public String addOrder() { //限流放在网关 获取到当前 客户端从桶中获取对应的令牌 结果表示从桶中拿到令牌等待时间 //如果获取不到令牌 就一直等待 double acquire = rateLimiter.acquire(); System.out.println("从桶中获取令牌等待时间"+acquire); boolean tryAcquire=rateLimiter.tryAcquire(500,TimeUnit.MILLISECONDS); //如果在500sms没有获取到令牌 直接走降级 if (!tryAcquire) { System.out.println("别抢了,等等吧!"); return "别抢了,等等吧!"; } //业务逻辑处理 boolean addOrderResult = orderService.addOrder(); if (addOrderResult) { System.out.println("恭喜抢购成功!"); return "恭喜抢购成功!"; } return "抢购失败!"; } }
运行结果:
获取后就从桶中删除了token
漏桶算法
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可以用于流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:
一个固定容量的漏桶,按照常量固定速率流出水滴;
如果桶是空的,则不需流出水滴;
可以以任意速率流入水滴到漏桶;
如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
令牌桶和漏桶对比:
令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;
漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;
漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;
令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。
另外有时候我们还使用计数器来进行限流,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数;只要全局总请求数或者一定时间段的总请求数设定的阀值则进行限流,是简单粗暴的总数量限流,而不是平均速率限流。
应用层可以 Nginx http_limit 应用层限流 (这一种运维解决,前三种 网关里面加代码)
以固定的速率从桶中流出水滴 以任意的速率往桶中放水滴 桶中的容量大小是不会发生改变的
令牌桶算法以平均速率访问,漏桶算法平滑访问。