limit-spring-boot-starter
limit-spring-boot-starter是一個基于springboot starter機制,結合SPI 接口設計思想(內部集成:Redis+Lua實現限流算法(令牌桶,固定窗口,滑動窗口)以及限流回退默認實現),支持注解方式/配置文件方式接入限流,擴展方便,集成使用簡單的分布式限流組件。
開源地址:https://gitee.com/javacoo/limit-spring-boot-starter
背景介紹
業務背景
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實現分布式限流
- 支持接口添加注解方式限流
- 支持配置文件方式限流
- 支持限流回退
- ...
具體實現見 實施步驟 一節,由于實施步驟較多,故放在后面章節,我們先來看看如何集成及使用。
集成及使用
集成
<dependency>
<groupId>com.javacoo</groupId>
<artifactId>limit-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
使用
-
配置文件接入限流
主要配置說明,詳見 LimitConfig 限流配置,覆蓋原則:有方法級獨立配置則使用獨立配置,否則使用全局配置,當配置了 limit.expression限流表達式則激活了配置文件接入限流,注解方式失效。
config.png
#全局配置
#限流表達式:攔截 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
-
使用注解方式接入限流:默認配置情況
//60秒內,允許訪問1次 @Limit(period = 60,count = 1) public Optional<ExampleDto> 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); }
限流效果:正常訪問
2021-05-29 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-29 15:13:38.553 INFO 16452 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易請求],嘗試獲取執行權限成功,開始執行目標方法 ...
限流效果:降級策略:FAIL_FAST
2021-05-29 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-29 15:14:14.414 INFO 16192 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易請求],嘗試獲取執行權限失敗,服務降級處理,降級策略:FAIL_FAST 2021-05-29 15:14:14.416 ERROR 16192 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流服務執行異常] com.javacoo.limit.client.exception.LimitException: 訪問過于頻繁,超出訪問限制 ...
-
使用注解接入限流:回退策略情況
//60秒內,允許訪問1次,回退策略,指定回退處理類 @Limit(period = 60,count = 1,fallbackStrategy = FallbackStrategy.FALLBACK,fallbackImpl = "getExampleInfoFallback") public Optional<ExampleDto> 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); }
限流效果:正常訪問
2021-05-29 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-29 15:22:44.614 INFO 16720 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易請求],嘗試獲取執行權限成功,開始執行目標方法 ...
限流效果:降級策略:FALLBACK
2021-05-29 15:23:09.497 INFO 8592 --- [ main] c.j.l.c.handler.AnnotationLimitHandler : [AnnotationLimitHandler限流交易請求],嘗試獲取方法:com.javacoo.service.example.service.impl.ExampleServiceImpl.getExampleInfo,執行權限
2021-05-29 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-29 15:23:09.666 INFO 8592 --- [ main] c.j.l.c.handler.AbstractLimitHandler : [限流交易請求],嘗試獲取執行權限失敗,服務降級處理,降級策略:FALLBACK
2021-05-29 15:23:09.668 INFO 8592 --- [ main] c.j.s.e.fallback.GetExampleInfoFallback : getExampleInfo方法降級處理
...
-
限流回退擴展實現
基于xkernel 提供的SPI機制(詳見:https://gitee.com/javacoo/xkernel
),擴展非常方便,大致步驟如下:實現限流回退接口:如 com.javacoo.service.example.fallback.GetExampleInfoFallback,com.javacoo.service.example.fallback.GlobalFallback
-
配置限流回退接口:
在項目resource目錄新建包->META-INF->services
-
創建com.javacoo.limit.client.api.Fallback文件,文件內容:實現類的全局限定名,如:
globalFallback=com.javacoo.service.example.fallback.GlobalFallback getExampleInfoFallback=com.javacoo.service.example.fallback.GetExampleInfoFallback
fallback.png
- 修改配置文件,添加如下內容:
```properties
#全局配置
limit.limit-rule.fallback-impl=globalFallback
#方法級配置
limit.limit-method-map[getExampleInfo].fallbackImpl = getExampleInfoFallback
```
全局實現
/**
* 全局方法降級處理接口實現
* <li></li>
*
* @author: duanyong@jccfc.com
* @since: 2021/5/29 14:35
*/
@Slf4j
public class GlobalFallback implements Fallback<Object> {
/**
* 服務降級處理
* <li></li>
*
* @author duanyong@jccfc.com
* @date 2021/5/29 10:25
* @return: R 返回對象
*/
@Override
public Object getFallback() {
log.info("全局降級處理");
return null;
}
}
指定方法實現
/**
* getExampleInfo方法降級處理接口實現
* <li></li>
*
* @author: duanyong@jccfc.com
* @since: 2021/5/29 11:20
*/
@Slf4j
public class GetExampleInfoFallback implements Fallback<Optional<ExampleDto>> {
/**
* 服務降級處理
* <li></li>
*
* @author duanyong@jccfc.com
* @date 2021/5/29 10:25
* @return: java.lang.Object 返回對象
*/
@Override
public Optional<ExampleDto> getFallback() {
log.info("getExampleInfo方法降級處理");
ExampleDto exampleDto = new ExampleDto();
exampleDto.setData("請求過多,請稍后再試");
return Optional.ofNullable(exampleDto);
}
}
實施步驟
1,新建limit-spring-boot-starter工程
- 工程結構
- 類結構圖
-
項目結構
limit-spring-boot-starter └── src ├── main │ ├── java │ │ └── com.javacoo │ │ ├────── limit │ │ │ ├──────client │ │ │ │ ├── api │ │ │ │ │ ├── Fallback 服務降級回退處理接口 │ │ │ │ │ └── RateLimiter 限流接口 │ │ │ │ ├── annotation │ │ │ │ │ └── Limit 限流注解 │ │ │ │ ├── config │ │ │ │ │ ├── LimitRule 限流規則配置 │ │ │ │ │ └── LimitConfig 限流配置 │ │ │ │ ├── enums │ │ │ │ │ ├── FallbackStrategy 降級策略 │ │ │ │ │ └── LimitType 限流類型 │ │ │ │ ├── exception │ │ │ │ │ └── LimitException 限流異常 │ │ │ │ ├── util │ │ │ │ │ └── WebUtil 工具類 │ │ │ │ ├── handler │ │ │ │ │ ├── AbstractLimitHandler 抽象限流處理器 │ │ │ │ │ ├── AnnotationLimitHandler 限流注解處理器 │ │ │ │ │ ├── ConfigLimitHandler 限流配置處理器 │ │ │ │ │ └── LimitPointcutAdvisor 限流切面Advisor │ │ │ │ └── internal 接口內部實現 │ │ │ │ ├── redis │ │ │ │ │ ├── DefaultFallback 服務降級回退處理接口默認實現 │ │ │ │ └── redis │ │ │ │ ├── LimitRedisConfig RedisTemplate配置類 │ │ │ │ ├── AbstractRateLimiter 抽象限流接口實現 │ │ │ │ ├── FixedWindowRateLimiter 固定窗口算法實現類 │ │ │ │ ├── LeakyBucketRateLimiter 漏桶算法實現類 │ │ │ │ ├── SlidingWindowRateLimiter 滑動窗口算法實現類 │ │ │ │ └── TokenBucketRateLimiter 令牌桶算法實現類 │ │ │ └──────starter │ │ │ ├── LimitAutoConfiguration 自動配置類 │ │ │ └── RateLimiterHolder 分布式限流接口對象持有者 │ └── resource │ ├── META-INF │ │ ├── spring.factories │ │ └── ext │ │ └── internal │ │ ├── com.javacoo.limit.client.api.Fallback │ │ └── com.javacoo.limit.client.api.RateLimiter │ └── script │ ├── FixedWindow.lua │ ├── LeakyBucket.lua │ ├── SlidingWindow.lua │ └── TokenBucket.lua └── test 測試
2,基于SPI思想設計擴展接口
-
限流接口->com.javacoo.limit.client.api.RateLimiter
/** * 限流接口 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/6 22:25 */ @Spi(LimitConfig.DEFAULT_IMPL) public interface RateLimiter { /** 默認時間單位:秒 */ TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS; /** 默認限流時間范圍:1 */ int DEFAULT_PERIOD = 1; /** 默認限流數量: 10 */ int DEFAULT_LIMIT_COUNT = 10; /** * 嘗試獲取 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/6 23:09 * @param keys key * @param count 限制數量 * @return boolean 是否成功 */ boolean tryAcquire(ImmutableList<String> keys ,int count); /** * 嘗試獲取 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 23:09 * @param keys key * @param count 限制數量 * @param period 時間周期 * @return boolean 是否成功 */ boolean tryAcquire(ImmutableList<String> keys ,int count,int period); /** * 嘗試獲取 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 23:09 * @param keys key * @param count 限制數量 * @param period 時間周期 * @param timeUnit 時間周期單位 * @return boolean 是否成功 */ boolean tryAcquire(ImmutableList<String> keys ,int count,int period,TimeUnit timeUnit); }
-
服務降級回退處理接口->com.javacoo.limit.client.api.Fallback
/** * 服務降級回退處理接口 * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 10:20 */ @Spi(LimitConfig.DEFAULT_IMPL) public interface Fallback<R> { /** * 服務降級處理 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 10:25 * @return: R 返回對象 */ default R getFallback(){ return null;} }
3,redis+lua實現分布式限流(內部擴展實現)
-
RedisTemplate配置類
/** * RedisTemplate配置類 * <li></li> * * @author: duanyong * @since: 2020/6/22 10:41 */ @Slf4j @Configuration public class LimitRedisConfig { private static final String DATA_FORMAT = "yyyy-MM-dd HH:mm:ss"; @Bean public RedisTemplate<String, Serializable> limitRedisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); FastJsonRedisSerializer<Serializable> fastJsonRedisSerializer = new FastJsonRedisSerializer(Serializable.class); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setDateFormat(DATA_FORMAT); fastJsonRedisSerializer.setFastJsonConfig(fastJsonConfig); redisTemplate.setValueSerializer(fastJsonRedisSerializer); redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); // 設置鍵(key)的序列化采用StringRedisSerializer。 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
-
抽象限流接口實現
/** * 抽象限流接口實現 * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 9:35 */ @Slf4j public abstract class AbstractRateLimiter implements RateLimiter { /** * RedisScript */ protected RedisScript<Long> redisLuaScript; /** * RedisTemplate */ @Autowired protected RedisTemplate<String, Serializable> redisTemplate; @PostConstruct public void initLUA() { redisLuaScript = new DefaultRedisScript<>(buildLuaScript(), Long.class); } /** * 嘗試獲取 * <p>說明:</p> * <li></li> * * @param keys key * @param count 限制數量 * @return boolean 是否成功 * @author duanyong * @date 2021/5/6 23:09 */ @Override public boolean tryAcquire(ImmutableList<String> keys , int count) { return tryAcquire(keys,count,RateLimiter.DEFAULT_PERIOD); } /** * 嘗試獲取 * <p>說明:</p> * <li></li> * * @param keys key * @param count 限制數量 * @param period 時間周期 * @return boolean 是否成功 * @author duanyong * @date 2021/5/7 23:09 */ @Override public boolean tryAcquire(ImmutableList<String> keys ,int count, int period) { return tryAcquire(keys,count,period,RateLimiter.DEFAULT_TIME_UNIT); } /** * 嘗試獲取 * <p>說明:</p> * <li></li> * * @param keys key * @param count 限制數量 * @param period 時間周期 * @param timeUnit 時間周期單位 * @return boolean 是否成功 * @author duanyong * @date 2021/5/7 23:09 */ @Override public boolean tryAcquire(ImmutableList<String> keys ,int count, int period, TimeUnit timeUnit) { return acquire(keys,count,period,timeUnit); } /** * 構建lua腳本 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 9:43 * @return: java.lang.String */ protected abstract String buildLuaScript(); /** * 嘗試獲取 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 22:21 * * @param keys key * @param limitCount 限制數量 * @param limitPeriod 時間周期 * @param timeUnit 時間周期單位 * @return boolean 是否成功 */ protected abstract boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit); /** * 加載lua腳本 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 10:47 * @param path: * @return: java.lang.String */ protected String loadLuaScript(String path){ try { PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(this.getClass().getClassLoader()); Resource[] resource = resolver.getResources(path); String luaScriptContent = StreamUtils.copyToString(resource[0].getInputStream(), StandardCharsets.UTF_8); log.info("完成加載Lua腳本:{}",luaScriptContent); return luaScriptContent; } catch (IOException ioException) { ioException.printStackTrace(); throw new LimitException("加載Lua腳本異常",ioException); } } }
-
固定窗口算法實現類
** * 分布式限流 固定窗口算法實現類 * <p>說明:</p> * <li>固定窗口,一般來說,如非時間緊迫,不建議選擇這個方案,太過生硬。但是,為了能快速止損眼前的問題可以作為臨時應急的方案</li> * @author duanyong * @date 2021/5/6 22:49 */ @Slf4j public class FixedWindowRateLimiter extends AbstractRateLimiter { /** * 構建lua腳本 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 9:43 * @return: java.lang.String */ @Override protected String buildLuaScript(){ return loadLuaScript( "classpath:script/FixedWindow.lua"); } /** * 嘗試獲取 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 22:21 * * @param keys key * @param limitCount 限制數量 * @param limitPeriod 時間周期 * @param timeUnit 時間周期單位 * @return boolean 是否成功 */ @Override protected boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit){ Long count = redisTemplate.execute(redisLuaScript, keys,limitCount,limitPeriod); log.info("[固定窗口限流交易請求],key:{},返回:{},{}秒內,限制次數:{}",keys,count,limitPeriod,limitCount); if (count != null && count.intValue() == 1) { return true; } else { return false; } } }
FixedWindow.lua
--限流KEY local key = KEYS[1] --限流大小 local limit = tonumber(ARGV[1]) --時間周期 local period = tonumber(ARGV[2]) local current = tonumber(redis.call('get', key) or "0") --如果超出限流大小 if current + 1 > limit then return 0 else redis.call("incr", key) if current == 1 then redis.call("expire", key,period) end return 1 end
漏桶算法實現類
-
滑動窗口算法實現類
/** * 分布式限流 滑動窗口算法實現類 * <p>說明:</p> * <li>滑動窗口。這個方案適用于對異常結果「高容忍」的場景,畢竟相比“兩窗”少了一個緩沖區。但是,勝在實現簡單</li> * @author duanyong * @date 2021/5/29 22:49 */ @Slf4j public class SlidingWindowRateLimiter extends AbstractRateLimiter { /** * 構建lua腳本 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 9:43 * @return: java.lang.String */ @Override protected String buildLuaScript(){ return loadLuaScript( "classpath:script/SlidingWindow.lua"); } /** * 嘗試獲取 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 22:21 * * @param keys key * @param limitCount 限制數量 * @param limitPeriod 時間周期 * @param timeUnit 時間周期單位 * @return boolean 是否成功 */ @Override protected boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit){ Long count = redisTemplate.execute(redisLuaScript, keys,"1",limitCount,limitPeriod); log.info("[滑動窗口限流交易請求],key:{},{}秒內,返回數量:{},{}秒內,限制次數:{}",keys,limitPeriod,count,limitPeriod,limitCount); if (count != null && count.intValue() > 0) { return true; } else { return false; } } }
SlidingWindow.lua
local function addToQueue(x,time) local count=0 for i=1,x,1 do redis.call('lpush',KEYS[1],time) count=count+1 end return count end --返回 local result=0 --限流KEY local key = KEYS[1] --申請數 local applyCount = tonumber(ARGV[1]) --閥值數量 local limit = tonumber(ARGV[2]) --閥值時間 local period = tonumber(ARGV[3]) redis.replicate_commands() local now = redis.call('time')[1] redis.call('SET','now',now); --當前時間 local current_time = now local timeBase = redis.call('lindex',key, limit - applyCount) if (timeBase == false) or (tonumber(current_time) - tonumber(timeBase) > period) then result = result + addToQueue(applyCount,tonumber(current_time)) end if (timeBase ~= false) then redis.call('ltrim',key,0,limit) end return result
-
令牌桶算法實現類
/** * 分布式限流 令牌桶算法實現類 * <p>說明:</p> * <li>令牌桶。當你需要盡可能的壓榨程序的性能(此時桶的最大容量必然會大于等于程序的最大并發能力),并且所處的場景流量進入波動不是很大(不至于一瞬間取完令牌,壓垮后端系統)</li> * @author duanyong * @date 2021/5/29 22:49 */ @Slf4j public class TokenBucketRateLimiter extends AbstractRateLimiter { /** * 構建lua腳本 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 9:43 * @return: java.lang.String */ @Override protected String buildLuaScript(){ return loadLuaScript( "classpath:script/TokenBucket.lua"); } /** * 嘗試獲取 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 22:21 * * @param keys key * @param limitCount 限制數量 * @param limitPeriod 時間周期 * @param timeUnit 時間周期單位 * @return boolean 是否成功 */ @Override protected boolean acquire(ImmutableList<String> keys ,int limitCount, int limitPeriod, TimeUnit timeUnit){ Long count = redisTemplate.execute(redisLuaScript, keys,limitCount,limitPeriod); log.info("[令牌桶限流交易請求],key:{},返回:{},{}秒內,限制次數:{}",keys,count,limitPeriod,limitCount); if (count != null && count.intValue() == 1) { return true; } else { return false; } } }
TokenBucket.lua
--利用redis的hash結構,存儲key所對應令牌桶的上次獲取時間和上次獲取后桶中令牌數量 local ratelimit_info = redis.pcall('HMGET',KEYS[1],'last_time','current_token_num') local last_time = ratelimit_info[1] local current_token_num = tonumber(ratelimit_info[2]) redis.replicate_commands() local now = redis.call('time')[1] redis.call('SET','now',now); --tonumber是將value轉換為數字,此步是取出桶中最大令牌數、生成令牌的速率(每秒生成多少個)、當前時間 local max_token_num = tonumber(ARGV[1]) local token_rate = tonumber(ARGV[2]) --local current_time = tonumber(ARGV[3]) local current_time = now --reverse_time 即多少毫秒生成一個令牌 local reverse_time = 1000/token_rate --如果current_token_num不存在則說明令牌桶首次獲取或已過期,即說明它是滿的 if current_token_num == nil then current_token_num = max_token_num last_time = current_time else --計算出距上次獲取已過去多長時間 local past_time = current_time-last_time --在這一段時間內可產生多少令牌 local reverse_token_num = math.floor(past_time/reverse_time) current_token_num = current_token_num +reverse_token_num last_time = reverse_time * reverse_token_num + last_time if current_token_num > max_token_num then current_token_num = max_token_num end end local result = 0 if(current_token_num > 0) then result = 1 current_token_num = current_token_num - 1 end --將最新得出的令牌獲取時間和當前令牌數量進行存儲,并設置過期時間 redis.call('HMSET',KEYS[1],'last_time',last_time,'current_token_num',current_token_num) redis.call('pexpire',KEYS[1],math.ceil(reverse_time*(max_token_num - current_token_num)+(current_time-last_time))) return result
4,支持接口添加注解方式限流
-
自定義限流注解
/** * 自定義限流注解 * <p>說明:</p> * <li></li> * * @author duanyong * @date 2021/5/6 21:47 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Limit { /** * 名字 */ String name() default ""; /** * key */ String key() default ""; /** * Key的前綴 */ String prefix() default ""; /** * 給定的時間范圍 單位(秒) */ int period(); /** * 一定時間內最多訪問次數 */ int count(); /** * 限流的類型(用戶自定義key 或者 請求ip) */ LimitType limitType() default LimitType.CUSTOMER; /** * 降級策略:默認快速失敗 */ FallbackStrategy fallbackStrategy() default FallbackStrategy.FAIL_FAST; /** * 當降級策略為:回退 時回退處理接口實現名稱 */ String fallbackImpl() default ""; }
-
限流注解處理器
/** * 限流注解處理器 * <li>基于aspectj</li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 17:04 */ @Slf4j @Aspect @Component public class AnnotationLimitHandler extends AbstractLimitHandler<Object>{ @Around("@annotation(limit)") public Object around(ProceedingJoinPoint joinPoint, Limit limit) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 獲取被攔截的方法 Method method = signature.getMethod(); // 獲取被攔截的類名 String className = signature.getDeclaringType().getName(); // 獲取被攔截的方法名 String methodName = method.getName(); String defaultKey = StringUtils.join(className,".", methodName); log.info("[AnnotationLimitHandler限流交易請求],嘗試獲取方法:{},執行權限", defaultKey); //組裝限流規則 LimitRule limitRule = LimitRule.builder() .defaultKey(defaultKey) .count(limit.count()) .key(limit.key()) .limitType(limit.limitType()) .period(limit.period()) .prefix(limit.prefix()) .fallbackStrategy(limit.fallbackStrategy()) .fallbackImpl(limit.fallbackImpl()) .build(); return handle(limitRule, () -> { try { return joinPoint.proceed(); } catch (Throwable throwable) { log.error("[AnnotationLimitHandler限流交易請求],方法執行異常",throwable); } return null; }); } }
5,支持配置文件方式限流
-
接口限流配置
/** * 接口限流配置 * @author duanyong * @date 2021/3/4 15:15 */ @ConfigurationProperties(prefix = LimitConfig.PREFIX) public class LimitConfig { /** 前綴 */ public static final String PREFIX = "limit"; /** 前綴 */ public static final String EXP = "expression"; /** limit是否可用,默認值*/ public static final String ENABLED = "enabled"; /** 默認實現,默認值*/ public static final String DEFAULT_IMPL= "default"; /** PointCut表達式,默認值*/ public static final String DEFAULT_EXP= ""; /** limit是否可用*/ private String enabled = ENABLED; /**實現*/ private String impl = DEFAULT_IMPL; /**PointCut表達式*/ private String expression = DEFAULT_EXP; /**全局限流規則配置*/ @NestedConfigurationProperty private LimitRule limitRule = new LimitRule().init(); /**特殊接口限流規則配置Map*/ private Map<String, LimitRule> limitMethodMap = new HashMap<>(5); public String getEnabled() { return enabled; } public void setEnabled(String enabled) { this.enabled = enabled; } public String getImpl() { return impl; } public void setImpl(String impl) { this.impl = impl; } public String getExpression() { return expression; } public void setExpression(String expression) { this.expression = expression; } public LimitRule getLimitRule() { return limitRule; } public void setLimitRule(LimitRule limitRule) { this.limitRule = limitRule; } public Map<String, LimitRule> getLimitMethodMap() { return limitMethodMap; } public void setLimitMethodMap(Map<String, LimitRule> limitMethodMap) { this.limitMethodMap = limitMethodMap; } }
-
限流規則配置
/** * 限流規則配置 * <li>單個接口方法限流規則配置</li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 15:31 */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class LimitRule { /** 給定的時間范圍 單位(秒),默認值*/ public static final int DEFAULT_PERIOD = 1; /** 一定時間內最多訪問次數,默認值*/ public static final int DEFAULT_COUNT = 50; /** * defaultKey */ private String defaultKey = ""; /** * key */ private String key; /** * Key的前綴 */ private String prefix; /** * 給定的時間范圍 單位(秒) */ private int period; /** * 一定時間內最多訪問次數 */ private int count; /** * 限流類型 */ private LimitType limitType; /** * 降級策略:默認快速失敗 */ private FallbackStrategy fallbackStrategy; /** * 當降級策略為:回退 時回退處理接口實現名稱 */ private String fallbackImpl; /** * 初始化 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 14:06 * @return: void */ public LimitRule init(){ this.setPeriod(DEFAULT_PERIOD); this.setCount(DEFAULT_COUNT); this.setLimitType(LimitType.CUSTOMER); this.setFallbackStrategy(FallbackStrategy.FAIL_FAST); this.setFallbackImpl(LimitConfig.DEFAULT_IMPL); return this; } }
-
限流配置處理器
/** * 限流配置處理器 * <li>處理配置方式限流</li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 17:06 */ @Slf4j public class ConfigLimitHandler extends AbstractLimitHandler<Object> implements MethodInterceptor { @Nullable @Override public Object invoke(@Nonnull MethodInvocation methodInvocation) throws Throwable { // 獲取被攔截的類名 String className = methodInvocation.getClass().getName(); // 獲取被攔截的方法名 String methodName = methodInvocation.getMethod().getName(); String defaultKey = StringUtils.join(className,".", methodName); //獲取配置文件中的限流規則 LimitRule limitRule = lockConfig.getLimitRule(); //如果需要特殊處理 if(lockConfig.getLimitMethodMap().containsKey(methodName)){ limitRule = lockConfig.getLimitMethodMap().get(methodName); } limitRule.setDefaultKey(defaultKey); log.info("[ConfigLimitHandler限流交易請求],嘗試獲取方法:{},執行權限", defaultKey); return handle(limitRule, () -> { try { return methodInvocation.proceed(); } catch (Throwable throwable) { log.error("[ConfigLimitHandler限流交易請求],方法執行異常",throwable); } return null; }); } }
-
限流切面Advisor
/** * 限流切面Advisor * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 10:52 */ @Slf4j public class LimitPointcutAdvisor extends AbstractBeanFactoryPointcutAdvisor { @Autowired private LimitConfig lockConfig; @Override public Pointcut getPointcut() { AspectJExpressionPointcut adapterPointcut = new AspectJExpressionPointcut(); //從配置文件中獲取PointCut表達式 adapterPointcut.setExpression(lockConfig.getExpression()); return adapterPointcut; } }
6,支持限流回退
-
服務降級回退處理接口默認實現
/** * 服務降級回退處理接口默認實現 * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 11:00 */ public class DefaultFallback implements Fallback<Object> { /** * 服務降級處理 * <li></li> * * @author duanyong@jccfc.com * @date 2021/5/29 10:25 * @return: java.lang.Object 返回對象 */ @Override public Object getFallback() { return null; } }
7,其他輔助類
-
降級策略枚舉類
/** * 降級策略 * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 10:32 */ public enum FallbackStrategy { /** * 快速失敗 */ FAIL_FAST, /** * 回退 */ FALLBACK; }
-
限流類型枚舉類
/** * 限流類型 * <p>說明:</p> * <li></li> * * @author duanyong * @date 2021/5/6 21:47 */ public enum LimitType { /** * 自定義key */ CUSTOMER, /** * 請求者IP */ IP; }
-
限流異常類
/** * 限流異常 * <li></li> * * @author: duanyong * @since: 2021/4/29 11:20 */ public class LimitException extends RuntimeException{ /**錯誤碼*/ protected String code; public LimitException() { } public LimitException(Throwable ex) { super(ex); } public LimitException(String message) { super(message); } public LimitException(String code, String message) { super(message); this.code = code; } public LimitException(String message, Throwable ex) { super(message, ex); } }
-
抽象限流處理器
/** * 抽象限流處理器 * <p>說明:</p> * <li></li> * * @author duanyong@jccfc.com * @date 2021/5/29 21:48 */ @Slf4j public abstract class AbstractLimitHandler<R> { @Autowired protected LimitConfig lockConfig; @Autowired private RateLimiter rateLimiter; /** * 限流處理 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 9:34 * @param limitRule: 限流規則 * @param function: 結果提供函數 * @return: R */ protected R handle(LimitRule limitRule, Supplier<R> function){ //規則整理 handleLimitRule(limitRule); ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitRule.getPrefix(), limitRule.getKey())); try { if (rateLimiter.tryAcquire(keys,limitRule.getCount(), limitRule.getPeriod())) { log.info("[限流交易請求],嘗試獲取執行權限成功,開始執行目標方法"); return function.get(); }else{ log.info("[限流交易請求],嘗試獲取執行權限失敗,服務降級處理,降級策略:{}",limitRule.getFallbackStrategy()); if(FallbackStrategy.FAIL_FAST.equals(limitRule.getFallbackStrategy())){ throw new LimitException("訪問過于頻繁,超出訪問限制"); } //獲取回退處理接口實現:如果為空則使用默認實現 Fallback<R> fallback = ExtensionLoader.getExtensionLoader(Fallback.class).getExtension(limitRule.getFallbackImpl()); fallback = fallback != null ? fallback : ExtensionLoader.getExtensionLoader(Fallback.class).getDefaultExtension(); return fallback.getFallback(); } } catch (Throwable e) { log.error("[限流服務執行異常]",e); if (e instanceof LimitException) { throw e; } throw new LimitException("限流服務執行異常",e); } } /** * 限流規則整理 * <li></li> * @author duanyong@jccfc.com * @date 2021/5/29 14:15 * @param limitRule: 限流規則 * @return: void */ private void handleLimitRule(LimitRule limitRule){ //限流規則整理:如果為空則使用全局配置 limitRule.setKey(StringUtils.isBlank(limitRule.getKey()) ? lockConfig.getLimitRule().getKey() : limitRule.getKey()); limitRule.setCount(limitRule.getCount() == 0 ? lockConfig.getLimitRule().getCount() : limitRule.getCount()); limitRule.setFallbackImpl(StringUtils.isBlank(limitRule.getFallbackImpl()) ? lockConfig.getLimitRule().getFallbackImpl() : limitRule.getFallbackImpl()); limitRule.setFallbackStrategy(limitRule.getFallbackStrategy() == null ? lockConfig.getLimitRule().getFallbackStrategy() : limitRule.getFallbackStrategy()); limitRule.setLimitType(limitRule.getLimitType() == null ? lockConfig.getLimitRule().getLimitType() : limitRule.getLimitType()); limitRule.setPeriod(limitRule.getPeriod() == 0 ? lockConfig.getLimitRule().getPeriod() : limitRule.getPeriod()); limitRule.setPrefix(StringUtils.isBlank(limitRule.getPrefix()) ? lockConfig.getLimitRule().getPrefix() : limitRule.getPrefix()); String key = limitRule.getKey(); //根據限流類型獲取不同的key ,如果不傳我們會以方法名作為key switch (limitRule.getLimitType()) { case IP: key = WebUtil.getIpAddress(); break; case CUSTOMER: key = StringUtils.isBlank(key) ? StringUtils.upperCase(limitRule.getDefaultKey()) : key; break; default: key = StringUtils.upperCase(limitRule.getDefaultKey()); } limitRule.setKey(key); } }
-
工具類
/** * 工具類 * <li></li> * * @author: duanyong@jccfc.com * @since: 2021/5/29 15:58 */ public class WebUtil { private static final String UNKNOWN = "unknown"; /** * 獲取IP * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 22:35 */ public static String getIpAddress() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
-
自動配置類
/** * 自動配置類 * <li></li> * @author duanyong * @date 2021/3/5 9:50 */ @Slf4j @Configuration @EnableConfigurationProperties(value = LimitConfig.class) @ConditionalOnClass(RateLimiter.class) @ConditionalOnProperty(prefix = LimitConfig.PREFIX, value = LimitConfig.ENABLED, matchIfMissing = true) public class LimitAutoConfiguration { @Autowired private LimitConfig lockConfig; @Bean @ConditionalOnMissingBean(RateLimiter.class) public RateLimiter createRateLimiter() { log.info("初始化分布式限流對象,實現類名稱:{}",lockConfig.getImpl()); RateLimiterHolder.rateLimiter = ExtensionLoader.getExtensionLoader(RateLimiter.class).getExtension(lockConfig.getImpl()); log.info("初始化分布式限流對象成功,實現類:{}", RateLimiterHolder.rateLimiter); return RateLimiterHolder.rateLimiter; } @Bean @ConditionalOnProperty(prefix = LimitConfig.PREFIX, value = LimitConfig.EXP) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public LimitPointcutAdvisor adapterServiceAdvisor() { LimitPointcutAdvisor advisor = new LimitPointcutAdvisor(); advisor.setAdviceBeanName("limitPointcutAdvisor"); advisor.setAdvice(createConfigLimitHandler()); advisor.setOrder(Ordered.HIGHEST_PRECEDENCE); return advisor; } @Bean public ConfigLimitHandler createConfigLimitHandler() { return new ConfigLimitHandler(); } @Bean @ConditionalOnMissingBean(LimitPointcutAdvisor.class) public AnnotationLimitHandler createAnnotationLimitHandler() { return new AnnotationLimitHandler(); } }
-
限流接口對象持有者
/** * 限流接口對象持有者 * <p>說明:</p> * <li></li> * @author duanyong * @date 2021/5/7 22:27 */ public class RateLimiterHolder { /** 限流對象*/ static RateLimiter rateLimiter; public static Optional<RateLimiter> getRateLimiter() { return Optional.ofNullable(rateLimiter); } }
8,資源文件
-
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.javacoo.limit.client.internal.redis.LimitRedisConfig,\ com.javacoo.limit.starter.LimitAutoConfiguration
-
com.javacoo.limit.client.api.Fallback
default=com.javacoo.limit.client.internal.fallback.DefaultFallback
-
com.javacoo.limit.client.api.RateLimiter
default=com.javacoo.limit.client.internal.redis.TokenBucketRateLimiter fixedWindow=com.javacoo.limit.client.internal.redis.FixedWindowRateLimiter slidingWindow=com.javacoo.limit.client.internal.redis.SlidingWindowRateLimiter
問題及局限性
問題
緊經過小規模生產驗證,雖滿足業務需求,但是還不算成熟,任需更多測試驗證。
僅供學習參考,如需用于生產,請謹慎,并多測試。
-
推薦使用Spring Clound Gateway,Sentinel等專業流量防護組件
局限性
限流組件保證了高可用,犧牲了性能,增加了一層 IO 環節的開銷,單機限流在本地,分布式限流還要通過網絡協議。
限流組件保證了高可用,犧牲了一致性,在大流量的情況下,請求的處理會出現延遲的情況,這種場景便無法保證強一致性。特殊情況下,還無法保證最終一致性,部分請求直接被拋棄。
限流組件擁有流控權,若限流組件掛了,會引起雪崩效應,導致請求與業務的大批量失敗。
引入限流組件,增加系統的復雜程度,開發難度增加,限流中間件的設計本身就是一個復雜的體系,需要綜合業務與技術去思考與權衡,同時還要確保限流組件本身的高可用與性能,極大增加工作量,甚至需要一個團隊去專門開發。
一些信息
路漫漫其修遠兮,吾將上下而求索
碼云:https://gitee.com/javacoo
QQ:164863067
作者/微信:javacoo
郵箱:xihuady@126.com