概况
在用户并发量比较大的情况下,服务会发生雪崩效应:用户同时请求堆积在一个接口,导致其它的接口服务无法访问,这种效果给到用户体验不好。本章分别描述常见的限流算法。
常见限流
单机版本限流:
计数器限流:AtomicInteger、Semaphore信号量、Semaphore控制并发量;
滑动窗口限流算法;
Guava令牌桶限流;
漏桶限流;
微服务限流方式:
Alibaba Sentinel限流(底层采用滑动窗口算法);
SpringCloud Hystrix限流(底层采用滑动窗口算法);
网关统一设置限流;
分布式限流:
Redis + Lua分布式限流;
应用层限流
Nginx 设置限流;
SpringBoot 配置Tomcat参数限流;
简单计数器限流算法
通过线程安全类,在指定时间内对客户端限流。通过计数器不需要引入任何其它第三方插件、非常简单。
缺点:对限流无法更精细控制控制。
在Java中有常见的两种计数器限流算法:AtomicInteger、Semaphore信号量
AtomicInteger实现计数器限流算法
需求:控制一分钟只能访问十次。
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* 计数器限流
* @author terry
* @version 1.0
* @date 2022/6/4 14:27
*/
@RestController
@RequestMapping("/counter")
public class CounterCtrl {
// 控制一分钟只能访问十次
public static AtomicInteger atomicCount = new AtomicInteger(0);
// 时间搓
public static AtomicLong atomicTime = new AtomicLong(System.currentTimeMillis());
@RequestMapping("/testAtomic")
public String testAtomic(){
// 换算成间隔多少秒
long second = (System.currentTimeMillis() - atomicTime.get()) / 1000;
// 如果时间大于1分钟 清零
if (second > 60) {
atomicCount.set(0);
atomicTime.set(System.currentTimeMillis());
}
atomicCount.incrementAndGet();
if (atomicCount.get() > 10) {
return "返回失败!" + second + "秒,访问次数" + atomicCount.get();
}
return "返回成功!" + second + "秒,访问次数" + atomicCount.get();
}
}
打印如下:
返回成功!5秒,访问次数7
返回失败!17秒,访问次数11
等待一分钟后访问:
返回成功!2秒,访问次数2
Semaphore实现计数器限流算法
Semaphore信号量是Java AQS下的并发组件。
/**
* 计数器限流
* @author terry
* @version 1.0
* @date 2022/6/4 14:27
*/
@RestController
@RequestMapping("/counter")
public class CounterCtrl {
// 控制一分钟只能访问十次
public static AtomicInteger atomicCount = new AtomicInteger(0);
// 时间搓
public static AtomicLong atomicTime = new AtomicLong(System.currentTimeMillis());
@RequestMapping("/testAtomic")
public String testAtomic(){
// 换算成间隔多少秒
long second = (System.currentTimeMillis() - atomicTime.get()) / 1000;
// 如果时间大于1分钟 清零
if (second > 60) {
atomicCount.set(0);
atomicTime.set(System.currentTimeMillis());
}
atomicCount.incrementAndGet();
if (atomicCount.get() > 10) {
return "返回失败!" + second + "秒,访问次数" + atomicCount.get();
}
return "返回成功!" + second + "秒,访问次数" + atomicCount.get();
}
// 信号量:控制一分钟只能访问十次
public static Semaphore semaphore = new Semaphore(10);
// 时间搓
public static AtomicLong semaphoreTime = new AtomicLong(System.currentTimeMillis());
@RequestMapping("/testSemaphore")
public String testSemaphore(){
// 换算成间隔多少秒
long second = (System.currentTimeMillis() - semaphoreTime.get()) / 1000;
// 如果时间大于1分钟 清零
if (second > 60) {
semaphore.release(10);
semaphoreTime.set(System.currentTimeMillis());
}
// 非阻塞tryAcquire
if (!semaphore.tryAcquire()) {
return "返回失败!" + second + "秒,剩余次数" + semaphore.availablePermits();
}
return "返回成功!" + second + "秒,剩余次数" + semaphore.availablePermits();
}
}
打印如下:
返回成功!3秒,剩余次数9
返回成功!7秒,剩余次数7
返回失败!24秒,剩余次数0
等待一分钟后访问:
返回成功!2秒,剩余次数8
Semaphore控制接口并发量
上面讲到Semaphore计数器限流算法的实现,现在单纯控制接口并发量。
假设:并发量控制4,接口请求需要10秒,代码实现。
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* 计数器限流
* @author terry
* @version 1.0
* @date 2022/6/4 14:27
*/
@RestController
@RequestMapping("/counter")
public class CounterCtrl {
public static Semaphore testSemaphoreQPS = new Semaphore(2);
/**
* 信号量:控制并发量为2
* @return
* @throws InterruptedException
*/
@RequestMapping("/testSemaphoreQPS")
public String testSemaphoreQPS() throws InterruptedException {
// 非阻塞tryAcquire
if (!testSemaphoreQPS.tryAcquire()) {
return "接口请求过于频繁!当前剩余人数" + testSemaphoreQPS.availablePermits();
}
// 模拟业务处理10秒
Thread.sleep(1000 * 10);
// 处理完释放
testSemaphoreQPS.release();
return "返回成功!" + testSemaphoreQPS.availablePermits();
}
}
开3个谷歌窗口打印如下:
返回成功!2
返回成功!1
接口请求过于频繁!当前剩余人数0
等待10秒后访问
返回成功!2
滑动窗口计数器限流算法
计数器限流算法存在一个问题,比如60秒内控制10个并发,如果用户在59秒、61秒 都请求了10次,那么实则是2秒钟请求了20次,就会无法精准控制限流的问题。
滑动窗口就是解决这个问题:如滑动窗口算法在60秒控制10个请求,会提前分割号10个单元格,每个单元格就是6秒钟,每隔6秒就会淘汰前面的单元格。
缺点:实现相对复杂。
令牌桶算法
令牌桶算法原理是每隔一段时间生成一个资源,客户端去拿资源。如果能够获取令牌则操作业务,没有则限制访问。
Java中令牌桶开源框架guava。
guava实现限流
pom.xml
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
代码实现
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* guava令牌桶限流
* @author terry
* @version 1.0
* @date 2022/6/4 16:19
*/
@RestController
@RequestMapping("/rateLimit")
public class RateLimitCtrl {
// 每秒2个令牌
static RateLimiter rateLimiter = RateLimiter.create(2);
/**
* 控制QPS 2
* @return
*/
@RequestMapping("/test")
public String test(){
// 非阻塞,如果获取不到则直接返回
if (!rateLimiter.tryAcquire()) {
return "请稍后重试!";
}
return "返回成功!";
}
}
测试:http://localhost:8080/rateLimit/test
返回成功!
返回成功!
请稍后重试!
小提示:可以封装一个RateLimiter注解,这样更轻量级实现。
Alibaba Sentinel限流
相对于单机版本的,Alibaba Sentinel有控制台更加方便管理,除控制台外还可以通过注解,详细参考往期博客: