本文介绍了四种常见的限流算法及其实现方式。固定窗口限流简单但存在临界突刺问题;滑动窗口通过时间片滑动解决突刺问题,但滑动单位选择困难;漏桶算法以固定速率处理请求,能削峰缓冲但不够灵活;令牌桶算法允许突发流量,并发性能更好但时间单位选择仍需考量。实现层面,单机限流可使用Guava的RateLimiter,分布式限流推荐Redisson或网关层工具如Sentinel。提供了Redisson的配置示例和两种限流设置方式,建议采用基于Duration的新API实现更简洁的限流控制。
限流算法介绍
1、固定窗口限流
在单位时间内允许部分操作。
就比如,单位时间(窗口)为1小时,一小时内只能有100个用户能访问,如果在一小时内就已经达到了次数上限100,那么这时不论来多少请求都会被拒绝掉,也就是说1小时内设定了多少次就是只能有多少次,只有到1小时才会重置次数。但是它有一个突刺问题,例如在前59分钟都没有一个用户进行访问,在59分钟的时候100个用户同时访问,然后1小时01分时候又有100个用户都访问了,那就是2分钟有200个用户同时访问,可能瞬间通过200个请求,在两个窗口的临界突刺,服务器可能就会有流量高峰危险。
优点:实现简单
缺点:突刺问题
2、滑动窗口限流
在单位时间内允许部分操作,不过这个单位时间是滑动的,需要指定一个滑动时间单位(时间片)。
**滑动窗口解决了固定窗口的突刺问题。**滑动窗口是持续滑动,逐时间片淘汰旧数据的过程,它不会像固定窗口一样在固定的时间点重置次数,而是持续淘汰旧数据,因此能更精确地控制任意连续内的请求速率。
比如,单位时间(窗口)是1小时,指定滑动单位是1分钟,那么在59分钟时它的限流窗口是59分~1小时59分,经过2分钟它的限流窗口就是1小时01分~2小时01分,这个时间段还是就只接收100个请求,如果有超过第100个的请求访问,超过的访问请求都会被拒绝掉;如果每2分钟都有1个旧的请求数据淘汰,经过2分钟这时只有99个数据就可以允许有1个新的请求进入。
优点:解决突刺问题
缺点:滑动窗口的限流效果与滑动单位有关,滑动单位小效果好,不过往往很难选到一个合适的滑动单位
3、漏桶算法限流(推荐)
请求桶大小固定,以固定的速率处理请求,当请求桶里面满了之后会拒绝请求。
漏桶算法限流的基本原理为:水(对应请求)从进水口进入到漏桶里,漏桶以一定的速度出水(请求放行),当水流入速度过大,桶内的总水量大于桶容量会直接溢出,请求被拒绝。
大致的漏桶限流规则如下:
1、进水口(对应客户端请求)以任意速率流入进入漏桶。
2、漏桶的容量是固定的,出水(放行)速率也是固定的。
3、漏桶容量是不变的,如果处理速度太慢,桶内水量会超出了桶的容量,则后面流入的水滴会溢出,表示请求拒绝。
以一定速率处理请求,有大量流量进入时,会发生溢出,从而限流保护服务可用(也就是削峰),不至于直接请求到服务器,缓冲压力(也就是缓冲)。
优点:一定程度上应对流量突刺,以恒定的速率处理请求,保证服务器安全
缺点:漏桶出口的速度固定,不能灵活的应对后端能力提升。比如,通过动态扩容,后端流量从100QPS提升到1000QPS,漏桶没有办法。
4、令牌桶算法限流(推荐)
管理员提前生成一批令牌,比如每秒 10 个令牌,当用户要操作前,先去拿到一个令牌,有令牌的人就有资格执行操作、能同时执行操作,而拿不到令牌的就拒绝请求得等着。当然了令牌的数量也是有上限的,令牌的数量与时间和发放速率强相关,时间流逝的时间越长,会不断往桶里加入越多的令牌,如果令牌发放的速度比申请速度快,令牌桶会放满令牌,直到令牌占满整个令牌桶。
令牌桶的大小固定,令牌的产生速度固定可以自己设置,但是消耗令牌(即请求)速度不固定(可以应对一些某些时间请求过多的情况);每个请求都会从令牌桶中取出令牌,如果没有令牌则丢弃该次请求。
令牌桶限流大致的规则如下:
1、进水口按照某个速度,向桶中放入令牌。
2、令牌的容量是固定的,但是放行的速度不是固定的,只要桶中还有剩余令牌,一旦请求过来就能申请成功,然后放行。
3、如果令牌的发放速度,慢于请求到来速度,桶内就无牌可领,请求就会被拒绝。
优点:比漏桶更高效,可以方便地应对突发出口流量,能够并发处理同时的请求,并发性能会更高
缺点 :还是时间单位选取的问题,令牌的产生速度、消耗令牌的速度对应时间选多少合适
限流实现
单机限流
使用谷歌提供的限流工具Guava RateLimiter第三方库实现,对单位时间的请求数量进行限制。
示例代码:
import com.google.common.util.concurrent.RateLimiter;
public static void main(String[] args) {
// 每秒限流10个请求
RateLimiter limiter = RateLimiter.create(10.0);
while (true) {
if (limiter.tryAcquire()) {
// 处理请求
} else {
// 超过流量限制,需要做何处理
}
}
}
多机限流
1、使用操作 Redis 的工具库Redisson内置了一个限流工具,分布式限流实现,把用户的使用频率等数据放到一个集中的存储进行统计,这样无论用户的请求落到了哪台服务器,都以集中存储中的数据为准。
2、在网关层集中进行限流和统计,如 Sentinel、Spring Cloud Gateway
Redisson限流实现
参考官方文档:https://github.com/redisson/redisson
首先引入Redisson依赖:
<!-- redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>4.1.0</version>
</dependency>
还需要有redis的依赖和application.yml配置
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
# Redis 配置
redis:
database: 0
host: localhost
port: 6379
timeout: 5000
# password: 123456
然后写个RedissonConfig配置类:
/**
* Redisson 配置(初始化RedissonClient对象单例)
*/
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
private Integer database;
private String host;
private Integer port;
// 如果你的redis默认没有设置密码则不用写
private String password;
@Bean
public RedissonClient getRedissonClient() {
// 1.创建配置对象
Config config = new Config();
config.useSingleServer()
.setDatabase(database)
.setAddress("redis://" + host + ":" + port);
// 2.创建Redisson实例
return Redisson.create(config);
}
}
再写个通用的RedissonManager类,便于使用redisson限流功能:
查看源码发现trySetRate方法的图中参数用法在新版的Redisson中是被弃用的,现在推荐使用RateIntervalUnit的替代方案org.redisson.api.RateType和Duration

第一种是:
rateLimiter.trySetRate(RateType.OVERALL, 2, Duration.ofSeconds(1));
第二种是:
rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.valueOf("SECONDS"));
推荐使用第一种方式,因为:
1、使用 Duration****更符合 Java 8+ 的日期时间 API 设计理念
2、代码更简洁明了
3、这是 Redisson 官方推荐的新用法
因此,最终代码为
/**
* 提供RedissonLimiter限流基础服务
*/
@Service
public class RedissonManager {
@Resource
private RedissonClient redissonClient;
public void doRateLimit(String key){
// 创建一个限流器
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// 设置限流器速率为每秒2个请求
rateLimiter.trySetRate(RateType.OVERALL, 2, Duration.ofSeconds(1));
// 每当一个请求过来,尝试获取一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
if (!canOp){
//TOO_MANY_REQUEST_ERROR(42900, "请求过于频繁")
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST_ERROR, "操作太频繁,请稍后再试");
}
}
}
最后在controller中使用它:
/**
* 获取当前登录用户
*
* @param request
* @return
*/
@Override
public User getLoginUser(HttpServletRequest request) {
// 先判断是否已登录
// 用户登录态键String USER_LOGIN_STATE = "user_login";
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
User currentUser = (User) userObj;
if (currentUser == null || currentUser.getId() == null) {
//NOT_LOGIN_ERROR(40100, "未登录")
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
// 从数据库查询(追求性能的话可以注释,直接走缓存)
long userId = currentUser.getId();
currentUser = this.getById(userId);
if (currentUser == null) {
throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
}
return currentUser;
}
@Resource
private RedissonManager redissonManager;
User loginUser = userService.getLoginUser(request);
String id = String.valueOf(loginUser.getId());
redissonManager.doRateLimit("generateByAi_" + id);