一 背景
在系統(tǒng)高可用設(shè)計中,接口限流是一個非常重要環(huán)節(jié),一方面是出于對自身服務(wù)器資源的保護,另一方面也是對依賴資源的一種保護措施。比如對于 Web 應(yīng)用,我限制單機只能處理每秒 1000 次的請求,超過的部分直接返回錯誤給客戶端。雖然這種做法損害了用戶的使用體驗,但是它是在極端并發(fā)下的無奈之舉,是短暫的行為,因此是可以接受的。
二 設(shè)計思路
常見的限流有2種思路
-
第一種是限制總量,也就是限制某個指標的累積上限,常見的是限制當(dāng)前系統(tǒng)服務(wù)的用戶總量,例如:某個搶購活動商品數(shù)量只有 100 個,限制參與搶購的用戶上限為 1 萬個,1 萬以后的用戶直接拒絕。
-
第二種是限制時間量,也就是限制一段時間內(nèi)某個指標的上限,例如 1 分鐘內(nèi)只允許 10000 個用戶訪問;每秒請求峰值最高為 10 萬。
三 限流算法
目前實現(xiàn)限流算法主要分為3類,這里不詳細展開介紹:
1)時間窗口
固定時間窗口算法是最簡單的限流算法,它的實現(xiàn)原理就是控制單位時間內(nèi)請求的數(shù)量,但是這個算法有個缺點就是臨界值問題。
為了解決臨界值的問題,又推出滑動時間窗口算法,其實現(xiàn)原理大致上是將時間分為一個一個小格子,在統(tǒng)計請求數(shù)量的時候,是通過統(tǒng)計滑動時間周期內(nèi)的請求數(shù)量。
2)漏斗算法
漏斗算法的核心是控制總量,請求流入的速率不確定,超過流量部分益出,該算法比較適用于針對突發(fā)流量,想要盡可能的接收全部請求的場景。其缺點也比較明顯,這個總量怎么評估,大小怎么配置,而且一旦初始化也沒法動態(tài)調(diào)整。
3)令牌桶算法
令牌桶算法的核心是控制速率,令牌產(chǎn)生的速度是關(guān)鍵,不斷的請求獲取令牌,獲取不到就丟棄。該算法比較適用于針對突發(fā)流量,以保護自身服務(wù)資源以及依賴資源為主,支持動態(tài)調(diào)整速率。缺點的話實現(xiàn)比較復(fù)雜,而且會丟棄很多請求。
四 實現(xiàn)步驟
我們自定義的這套限流組件有是基于guava RateLimiter封裝的,采用令牌桶算法以控制速率為主,支持DUCC動態(tài)配置,同時支持限流后的降級措施。接下來看一下整體實現(xiàn)方案
1、自定義RateLimiter Annotation標簽
這里主要對限流相關(guān)屬性的一個定義,包括每秒產(chǎn)生的令牌數(shù)、獲取令牌超時時間、降級邏輯實現(xiàn)以及限流開關(guān)等內(nèi)容
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SysRateLimit {
/**
* 每秒產(chǎn)生的令牌數(shù) 默認500
*
* @return
*/
double permitsPerSecond() default 500D;
/**
* 獲取令牌超時時間 默認100
*
* @return
*/
long timeout() default 100;
/**
* 獲取令牌超時時間單位 默認毫秒
*
* @return
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* 服務(wù)降級方法名稱 Spring bean id
*
* @return
*/
String fallbackBeanId() default "";
/**
* 限流key 唯一
*
* @return
*/
String limitKey() default "";
}
2、基于Spring Aspect 構(gòu)造切面
首先就是我們需要構(gòu)造一個Aspect切面用于掃描我們自定義的SysRateLimit標簽
@Slf4j
@EnableAspectJAutoProxy
@Aspect
public class SysRateLimitAspect {
/**
* 自定義切入點
*/
@Pointcut("@annotation(com.jd.smb.service.ratelimiter.annotation.SysRateLimit)")
public void pointCut() {
}
/**
* 方法前執(zhí)行限流方案
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 如果未獲取到對象,直接執(zhí)行方法
if (signature == null) {
return joinPoint.proceed();
}
try {
Method method = joinPoint.getTarget().getClass().getDeclaredMethod(signature.getName(), signature.getMethod().getParameterTypes());
// 獲取注解對象
SysRateLimit sysRateLimit = method.getAnnotation(SysRateLimit.class);
if (sysRateLimit == null) {
return joinPoint.proceed();
}
} catch (Exception e) {
// todo log
}
return joinPoint.proceed();
}
}
獲取自定義SysRateLimit標簽的各種屬性
// 限流key
String limitKey = sysRateLimit.limitKey();
if (StringUtils.isBlank(limitKey)) {
return joinPoint.proceed();
}
// 令牌桶數(shù)量
double permitsPerSecond = sysRateLimit.permitsPerSecond();
// 獲取令牌超時時間
long timeout = sysRateLimit.timeout();
// 獲取令牌超時時間單位
TimeUnit timeUnit = sysRateLimit.timeUnit();
將我們自定義的SysRateLimiter 和 Guava RateLimiter 進行整合
- 首先我們需要構(gòu)造一個全局Map,用于存儲我們開啟限流的方法,key就是我們定義的limitKey, value就是我們轉(zhuǎn)換后的Guava RateLimiter
/**
* 存儲RateLimiter(key: limitKey value:RateLimiter )
*/
private static final Map<String, RateLimiter> LIMITER_MAP = new ConcurrentHashMap<>();
- 接著就是核心邏輯:這里首先從我們創(chuàng)建的Map中獲取Guava RateLimiter,獲取不到就創(chuàng)建RateLimiter.create(permitsPerSecond) ;然后調(diào)用RateLimiter.tryAcquire()嘗試獲取令牌桶,獲取成功則執(zhí)行后續(xù)的邏輯,這里重點獲取失敗后,我們需要執(zhí)行我們的降級方法。(注意:Guava RateLimiter 有很多API,這里我們不展開討論,后續(xù)會針對Guava限流的源碼進行詳細的解析)
RateLimiter rateLimiter;
// Map中是否存在 存在直接獲取
if (LIMITER_MAP.containsKey(limitKey)) {
rateLimiter = LIMITER_MAP.get(limitKey);
} else {
// 不存在創(chuàng)建后放到Map中
rateLimiter = RateLimiter.create(permitsPerSecond);
LIMITER_MAP.put(limitKey, rateLimiter);
}
// 嘗試獲取令牌
if (!rateLimiter.tryAcquire(timeout, timeUnit)) {
// todo 限流后降級措施
return this.fallBack(sysRateLimit, joinPoint, signature);
}
降級方案執(zhí)行
上面我們在獲取令牌桶超時后,需要執(zhí)行我們的降級邏輯,怎么做呢?也很簡單,我們在定義SysRateLimiter的時候有個fallBackBeanId,這個就是我們執(zhí)行降級邏輯的bean對象Id,需要我們提前進行創(chuàng)建。接著我們看一下是怎么實現(xiàn)的。
/**
* 執(zhí)行降級邏輯
*
* @param sysRateLimit
* @param joinPoint
* @param signature
* @return
*/
private Object fallBack(SysRateLimit sysRateLimit, ProceedingJoinPoint joinPoint, MethodSignature signature) {
String fallbackBeanId = sysRateLimit.fallbackBeanId();
// 當(dāng)沒有配置具體的降級實現(xiàn)方案的時候 可以結(jié)合業(yè)務(wù)世紀情況設(shè)置限流錯誤碼
if (StringUtils.isBlank(fallbackBeanId)) {
// 自定義的 可以結(jié)合自己系統(tǒng)里的進行設(shè)置
return ApiResult.error(ResultCode.REACH_RATE_LIMIT);
}
try {
// SpringContext中通過BeanId獲取對象 SpringUtils只是獲取bean對象的工具類 有多種實現(xiàn)方式 可自行百度
Object bean = SpringUtils.getBean(fallbackBeanId);
Method method = bean.getClass().getMethod(signature.getName(), signature.getParameterTypes());
// 執(zhí)行對應(yīng)的方法
return method.invoke(bean, joinPoint.getArgs());
} catch (Exception e) {
// todo error log
}
return ApiResult.error(ResultCode.REACH_RATE_LIMIT);
}
這樣我們大概的一個架子就弄好了。 接下來我們看看實際該如何使用
3、具體應(yīng)用
在方法入口引入SysRateLimiter標簽
@Slf4j
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserQueryController extends AbstractController {
/**
* 查詢用戶信息
*
* @param request
* @return
*/
@GetMapping("/info/{id}")
@SysRateLimit(permitsPerSecond = 500, limitKey = "UserQueryController.info", fallbackBeanId = "userQueryControllerFallBack",
timeout = 100, timeUnit = TimeUnit.MILLISECONDS)
public ApiResult<UserInfo> info(@PathVariable Long id, HttpServletRequest request) {
// todo 業(yè)務(wù)邏輯查詢 這里不展開
return ApiResult.success();
}
}
設(shè)置降級方法
@Service
public class UserQueryControllerFallBack {
/**
* 降級后執(zhí)行的邏輯
*
* @param request
* @return
*/
public ApiResult<UserInfo> info(Long id, HttpServletRequest request) {
// todo 編寫限流降級后的邏輯 可以是降級碼 也可以是默認對象
return ApiResult.success(null);
}
}
當(dāng)請求進來的時候,會結(jié)合我們設(shè)置的閾值進行令牌桶的獲取,獲取失敗后會執(zhí)行限流,這里我們進行了限流后的降級處理。其實到這里我們完成限流組件的簡單封裝和使用,但是仍有一些點需要我們進行處理,例如如何動態(tài)設(shè)置令牌的數(shù)量,接下來我們就看一下如何實現(xiàn)令牌的動態(tài)設(shè)置。
4、動態(tài)設(shè)置令牌數(shù)量
通過DUCC配置令牌數(shù)量 我們需要定義一個DUCC配置,這里面內(nèi)容很簡單,配置我們設(shè)置limitKey的令牌數(shù)量
@Data
@Slf4j
@Component
public class RateLimitConfig {
/**
* 配置config key: limitKey value: 數(shù)量
*/
private Map<String, Integer> limitConfig;
/**
* 監(jiān)聽ducc配置
*
* @param json
*/
@LafValue(key = "rate.limit.conf")
public void setConfig(String json) {
if (StringUtils.isBlank(json)) {
return;
}
Map<String, Integer> map = JsonModelUtils.getModel(json, Map.class, null);
if (map != null) {
Wrapper.wrapperBean(map, this, true);
}
}
}
通過DUCC配置獲取指定limitKey的令牌數(shù)量,獲取失敗則采用方法設(shè)置默認數(shù)量,這樣我們后面設(shè)置令牌數(shù)量就可以通過DUCC動態(tài)的配置了
/**
* 獲取令牌桶數(shù)量
*
* @param sysRateLimit
* @return
*/
private double getPermitsPerSecond(SysRateLimit sysRateLimit) {
// 方法默認令牌數(shù)量
double defaultValue = sysRateLimit.permitsPerSecond();
if (rateLimitConfig == null || rateLimitConfig.getLimitConfig() == null) {
return defaultValue;
}
// 配置的令牌數(shù)量
Integer value = rateLimitConfig.getLimitConfig().get(sysRateLimit.limitKey());
if (value == null) {
return defaultValue;
}
return value;
}
5、后續(xù)其他配置
其實后續(xù)我們的其他屬性都可以通過DUCC動態(tài)化的來配置,這里呢因為和令牌桶數(shù)量類似,就不再展開描述了。感興趣的小伙伴可以自行設(shè)置,根據(jù)我們的使用,使用默認配置即可。
作者:京東零售 王磊文章來源:http://www.zghlxwxcb.cn/news/detail-494839.html
來源:京東云開發(fā)者社區(qū)文章來源地址http://www.zghlxwxcb.cn/news/detail-494839.html
到了這里,關(guān)于從0到1構(gòu)造自定義限流組件 | 京東云技術(shù)團隊的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!