# limit-spring-boot-starter
**Repository Path**: javacoo/limit-spring-boot-starter
## Basic Information
- **Project Name**: limit-spring-boot-starter
- **Description**: 限流组件
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 8
- **Forks**: 4
- **Created**: 2021-05-06
- **Last Updated**: 2024-09-29
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# limit-spring-boot-starter
> limit-spring-boot-starter是一个基于springboot starter机制,结合SPI 接口设计思想,内部集成:redis+lua实现分布式限流(令牌桶),以及限流回退默认实现,支持注解方式/配置文件方式接入限流,扩展方便,集成,使用简单的分布式限流组件。
### 背景介绍
##### 业务背景
1、随着业务的快速发展,对接的第三方合作机构越来越多,对外提供服务API访问量成倍增加,导致服务器压力也不断增加,而服务器资源是有限的,当请求量达到设计的极限时,如果不采取措施,轻则导致服务响应时间变长,重则可能造成整个系统瘫痪。
##### 生产环境背景
1、账单日批量业务接口访问量暴增,特别是某个时间段
2、业务方调用接口的速度未知,QPS可能达到400/s,600/s,或者更高
3、对外服务API性能上限是 QPS 300/s
4、已经出现服务不可用,应用崩溃的事故
##### 需求分析
1、鉴于业务方对接口的调用频率未知,而我方的接口服务有上限,为保证服务的可用性,业务层需要对接口调用方的流量进行限制—–接口限流。
2、尽量少改或者不改造已有功能:少侵入或者0侵入式开发。
3、扩展方便,集成简单,开发速率高,使用简单。
### 设计思路
##### 主流思路
- 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。
- 常见的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
- 如果是单节点我们可以使用google为我们提供的guava包下的RateLimiter进行限流,它使用的是令牌桶算法,分布式场景下也可以使用网关进行限流,如Spring Clound Gateway,其实还有很多开源的限流框架如阿里的Sentinel,甚至我们可以利用redis+lua脚本自己来实现限流。
##### 前面的话
在实际应用时也不要太纠结算法问题,因为一些限流算法实现是一样的只是描述不一样;具体使用哪种限流技术还是要根据实际场景来选择,不要一味去找最佳模式,白猫黑猫能解决问题的就是好猫 :)
##### 我的思路
组件基于springboot starter机制,结合SPI 接口设计思想,内部集成默认实现:redis+lua实现分布式限流,以及限流回退,支持注解方式/配置文件方式接入限流,主要分为以下部分:
- 新建 springboot starter工程
- 基于SPI思想设计扩展接口
- redis+lua实现分布式限流
- 支持接口添加注解方式限流
- 支持配置文件方式限流
- 支持限流回退
- ...
具体实现见 实施步骤 一节,由于实施步骤较多,故放在后面章节,我们先来看看如何集成及使用。
### 集成及使用
##### 集成
```xml
com.javacoo
limit-spring-boot-starter
1.0.0
```
##### 使用
- 配置文件接入限流
主要配置说明,详见 LimitConfig 限流配置,覆盖原则:有方法级独立配置则使用独立配置,否则使用全局配置,当配置了 limit.expression限流表达式则激活了配置文件接入限流,注解方式失效。

```properties
#全局配置
#限流表达式:拦截 com.javacoo.service.example.service 包及子包下所有方法
limit.expression=execution(* com.javacoo.service.example.service..*.*(..))
#全局限流回退实现名称
limit.limit-rule.fallback-impl=globalFallback
#方法级配置:针对getExampleInfo方法 配置独立的限流规则
#给定的时间范围 单位(秒)
limit.limit-method-map[getExampleInfo].period = 60
#一定时间内最多访问次数
limit.limit-method-map[getExampleInfo].count = 5
#限流类型
limit.limit-method-map[getExampleInfo].limitType = CUSTOMER
#降级策略
limit.limit-method-map[getExampleInfo].fallbackStrategy = FALLBACK
#当降级策略为:回退 时回退处理接口实现名称
limit.limit-method-map[getExampleInfo].fallbackImpl = getExampleInfoFallback
```
- 使用注解方式接入限流:默认配置情况
```java
//60秒内,允许访问1次
@Limit(period = 60,count = 1)
public Optional getExampleInfo(String id) {
AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID);
ExampleDto exampleDto = new ExampleDto();
exampleDto.setId("1");
exampleDto.setData("正常数据");
return Optional.ofNullable(exampleDto);
}
```
限流效果:正常访问
```properties
2021-05-27 15:13:38.553 INFO 16452 --- [ main] c.j.l.c.i.redis.RedisLuaRateLimiter : [限流交易请求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒内,已访问次数:1,60秒内,限制次数:1
2021-05-27 15:13:38.553 INFO 16452 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易请求],尝试获取执行权限成功,开始执行目标方法
...
```
限流效果:降级策略:FAIL_FAST
```properties
2021-05-27 15:14:14.413 INFO 16192 --- [ main] c.j.l.c.i.redis.RedisLuaRateLimiter : [限流交易请求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒内,已访问次数:2,60秒内,限制次数:1
2021-05-27 15:14:14.414 INFO 16192 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易请求],尝试获取执行权限失败,服务降级处理,降级策略:FAIL_FAST
2021-05-27 15:14:14.416 ERROR 16192 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流服务执行异常]
com.javacoo.limit.client.exception.LimitException: 访问过于频繁,超出访问限制
...
```
- 使用注解接入限流:回退策略情况
```java
//60秒内,允许访问1次,回退策略,指定回退处理类
@Limit(period = 60,count = 1,fallbackStrategy = FallbackStrategy.FALLBACK,fallbackImpl = "getExampleInfoFallback")
public Optional getExampleInfo(String id) {
AbstractAssert.isNotBlank(id, ErrorCodeConstants.SERVICE_GET_EXAMPLE_INFO_ID);
ExampleDto exampleDto = new ExampleDto();
exampleDto.setId("1");
exampleDto.setData("正常数据");
return Optional.ofNullable(exampleDto);
}
```
限流效果:正常访问
```properties
2021-05-27 15:22:44.613 INFO 16720 --- [ main] c.j.l.c.i.redis.RedisLuaRateLimiter : [限流交易请求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒内,已访问次数:1,60秒内,限制次数:1
2021-05-27 15:22:44.614 INFO 16720 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易请求],尝试获取执行权限成功,开始执行目标方法
...
```
限流效果:降级策略:FALLBACK
```properties
2021-05-27 15:23:09.497 INFO 8592 --- [ main] c.j.l.c.handler.AnnotationLimitHandler : [AnnotationLimitHandler限流交易请求],尝试获取方法:com.javacoo.service.example.service.impl.ExampleServiceImpl.getExampleInfo,执行权限
2021-05-27 15:23:09.666 INFO 8592 --- [ main] c.j.l.c.i.redis.RedisLuaRateLimiter : [限流交易请求],key:[COM.JAVACOO.SERVICE.EXAMPLE.SERVICE.IMPL.EXAMPLESERVICEIMPL.GETEXAMPLEINFO],60秒内,已访问次数:2,60秒内,限制次数:1
2021-05-27 15:23:09.666 INFO 8592 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易请求],尝试获取执行权限失败,服务降级处理,降级策略:FALLBACK
2021-05-27 15:23:09.668 INFO 8592 --- [ main] c.j.s.e.fallback.GetExampleInfoFallback : getExampleInfo方法降级处理
...
```
- 限流回退扩展实现
基于xkernel 提供的SPI机制,扩展非常方便,大致步骤如下:
1. 实现限流回退接口:如 com.javacoo.service.example.fallback.GetExampleInfoFallback,com.javacoo.service.example.fallback.GlobalFallback
2. 配置限流回退接口:
- 在项目resource目录新建包->META-INF->services
- 创建com.javacoo.limit.client.api.Fallback文件,文件内容:实现类的全局限定名,如:
```properties
globalFallback=com.javacoo.service.example.fallback.GlobalFallback
getExampleInfoFallback=com.javacoo.service.example.fallback.GetExampleInfoFallback
```

- 修改配置文件,添加如下内容:
```properties
#全局配置
limit.limit-rule.fallback-impl=globalFallback
#方法级配置
limit.limit-method-map[getExampleInfo].fallbackImpl = getExampleInfoFallback
```
全局实现
```java
/**
* 全局方法降级处理接口实现
*
*
* @author: duanyong@jccfc.com
* @since: 2021/5/22 14:35
*/
@Slf4j
public class GlobalFallback implements Fallback