❤ 作者主页:李奕赫揍小邰的博客
❀ 个人介绍:大家好,我是李奕赫!( ̄▽ ̄)~*
🍊 记得点赞、收藏、评论⭐️⭐️⭐️
📣 认真学习!!!🎉🎉
文章目录
- 限流的算法
- 漏桶算法
- 令牌桶算法
- 限流粒度
- 限流实现构思
- 1.本地限流(单机限流)
- 2.分布式限流(多机限流)
- Redisson 限流实现
- 1.先引入依赖
- 2.创建 RedissonConfig 配置类,用于初始化 RedissonClient 对象单例;
- 3.编写 RedisLimiterManager
- 4.在项目引用
最近在写一个项目,需要控制用户使用系统的次数,以避免超支,比如给不同等级的用户分配不同的调用次数,防止用户过度使用系统造成破产。现在要做一个解决方案,就是限流,比如说限制单个用户在每秒只能使用一次,那这里我们怎么去思考这个限流的阈值是多少?多少合适呢?
参考正常用户的使用,比如限制单个用户在每秒只能使用 1 次。
限流的算法
限流的算法有很多,常用的有两个漏桶算法和令牌桶算法,接下来会讲一下这两个算法的用法。
漏桶算法
它的原理很简单,可以认为就是注水漏水的过程。往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。
1.流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的。
2.桶的容量一般表示系统所能处理的请求数。
3.如果桶的容量满了,就达到限流的阀值,就会丢弃水滴(拒绝请求)
4.流出的水滴,是恒定过滤的,对应服务按照固定的速率处理请求。
伪代码如下:
/**
* 每秒处理数(出水率)
*/
private long rate;
/**
* 当前剩余水量
*/
private long currentWater;
/**
* 最后刷新时间
*/
private long refreshTime;
/**
* 桶容量
*/
private long capacity;
/**
* 漏桶算法
* @return
*/
boolean leakybucketLimitTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long outWater = (currentTime - refreshTime) / 1000 * rate; //流出的水量 =(当前时间-上次刷新时间)* 出水率
long currentWater = Math.max(0, currentWater - outWater); // 当前水量 = 之前的桶内水量-流出的水量
refreshTime = currentTime; // 刷新时间
// 当前剩余水量还是小于桶的容量,则请求放行
if (currentWater < capacity) {
currentWater++;
return true;
}
// 当前剩余水量大于等于桶的容量,限流
return false;
}
没有办法迅速处理一批请求,面对突发流量的时候,漏桶算法还是循规蹈矩地处理请求,按照固定速率处理请求,这不是我们想要的结果。
令牌桶算法
面对突发流量的时候,我们可以使用令牌桶算法限流。
令牌桶算法原理:
1.有一个令牌管理员,根据限流大小,定速往令牌桶里放令牌。
2.如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
3.系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑;
4.如果拿不到令牌,就直接拒绝这个请求。
伪代码如下:
/**
* 每秒处理数(放入令牌数量)
*/
private long putTokenRate;
/**
* 最后刷新时间
*/
private long refreshTime;
/**
* 令牌桶容量
*/
private long capacity;
/**
* 当前桶内令牌数
*/
private long currentToken = 0L;
/**
* 漏桶算法
* @return
*/
boolean tokenBucketTryAcquire() {
long currentTime = System.currentTimeMillis(); //获取系统当前时间
long generateToken = (currentTime - refreshTime) / 1000 * putTokenRate; //生成的令牌 =(当前时间-上次刷新时间)* 放入令牌的速率
currentToken = Math.min(capacity, generateToken + currentToken); // 当前令牌数量 = 之前的桶内令牌数量+放入的令牌数量
refreshTime = currentTime; // 刷新时间
//桶里面还有令牌,请求正常处理
if (currentToken > 0) {
currentToken--; //令牌数量-1
return true;
}
return false;
}
如果令牌发放的策略正确,这个系统即不会被拖垮,也能提高机器的利用率。Guava的RateLimiter限流组件,就是基于令牌桶算法实现的
限流粒度
1.针对某个方法限流,即单位时间内最多允许同时 XX 个操作使用这个方法
2.针对某个用户限流,比如单个用户单位时间内最多执行 XX 次操作
3.针对某个用户 x 方法限流,比如单个用户单位时间内最多执行 XX 次这个方法
限流实现构思
1.本地限流(单机限流)
每个服务器单独限流,一般适用于单体项目,就是你的项目只有一个服务器 。
在 Java 中,有很多第三方库可以用来实现单机限流:Guava RateLimiter:这是谷歌 Guava 库提供的限流工具,可以对单位时间内的请求数量进行限制。
import com.google.common.util.concurrent.RateLimiter;
public static void main(String[] args) {
// 每秒限流5个请求
RateLimiter limiter = RateLimiter.create(5.0);
while (true) {
if (limiter.tryAcquire()) {
// 处理请求
} else {
// 超过流量限制,需要做何处理
}
}
}
2.分布式限流(多机限流)
如果你的项目有多个服务器,比如微服务,那么建议使用分布式限流。
1.把用户的使用频率等数据放到一个集中的存储进行统计;
比如 Redis,这样无论用户的请求落到了哪台服务器,都以集中存储中的数据为准。
(Redisson – 是一个操作 Redis 的工具库)
2.在网关集中进行限流和统计(比如 Sentinel、Spring Cloud Gateway)
//redission
import org.redisson.Redisson;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
public static void main(String[] args) {
// 创建RedissonClient
RedissonClient redisson = Redisson.create();
// 获取限流器
RSemaphore semaphore = redisson.getSemaphore("mySemaphore");
// 尝试获取许可证
boolean result = semaphore.tryAcquire();
if (result) {
// 处理请求
} else {
// 超过流量限制,需要做何处理
}
}
Redisson 限流实现
Redisson 内置了一个限流工具类,可以帮助你利用 Redis 来存储、来统计。
1.先引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.21.3</version>
</dependency>
2.创建 RedissonConfig 配置类,用于初始化 RedissonClient 对象单例;
application.yml文件
RedissonConfig 配置类
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {
private Integer database;
private String host;
private Integer port;
private String password;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setDatabase(database)
.setAddress("redis://" + host + ":" + port);
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
3.编写 RedisLimiterManager
Manager是专门提供 RedisLimiter 限流基础服务的(提供了通用的能力,可以放到任何一个项目里)。
@Service
public class RedisLimiterManager {
@Resource
private RedissonClient redissonClient;
/**
* 限流操作
*
* @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
*/
public void doRateLimit(String key) {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
// 限流器的统计规则(每秒2个请求;连续的请求,最多只能有2个请求被允许通过)
// RateType.OVERALL表示速率限制作用于整个令牌桶,即限制所有请求的速率
rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
// 每当一个操作来了后,请求一个令牌
boolean canOp = rateLimiter.tryAcquire(1);
// 如果没有令牌,还想执行操作,就抛出异常
if (!canOp) {
throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
}
}
}
之后进行一下测试即可。
4.在项目引用
伪代码,只需要引入RedisLimiterManager 类,直接在需要限流的地方加上即可,例如给用户加上限流,就是每个用户每秒只允许请求两次
@Resource
private RedisLimiterManager redisLimiterManager;
User loginUser = userService.getLoginUser(request);
// 限流判断,每个用户一个限流器
redisLimiterManager.doRateLimit("user_" + loginUser.getId());