1、核心概念
1.1、流量控制
流量控制是為了 防止系統被過多的請求壓垮,確保資源合理分配并保持服務的可用性,比如對請求數量的限制。
流量控制的 3 個主要優勢:
- 防止過載:當瞬間涌入的請求量超出系統處理能力時,會導致資源枯竭,如 CPU和內存耗盡。流量控制通過限制系統能處理的請求數,確保不會發生過載。
- 避免雪崩效應:高負載下某個服務崩潰可能引發其他依賴服務的崩潰,形成連鎖反應。流量控制可以有效預防這種連鎖故障,避免系統雪崩。
- 優化用戶體驗:即便部分請求被拒絕或延遲處理,流量控制也能確保大部分用戶的請求能夠正常響應,避免全局響應時間過長的情況。
常見的實現流量控制方法有 2 種:
- 限流:通過固定窗口、令牌桶或漏桶等算法限制單位時間內的請求數量。
- 排隊:當請求量超出處理能力時,部分請求進入等待隊列,防止立即超載。
1.2、熔斷機制
熔斷機制的目的是 避免當下游服務發生異常時,整個系統繼續耗費資源重復發起失敗請求,從而防止連鎖故障。
工作機制:
- 監控服務健康狀態:系統會實時監控服務的調用情況,例如請求成功率、響應時間等,判斷服務的健康狀況。
- 進入熔斷狀態:當某個服務的錯誤率達到設定閾值(如響應時間過長或出錯率過高)時,系統會激活熔斷器,暫時停止對該服務的調用,避免消耗不必要的資源和讓錯誤進一步擴散。
- 快速失敗:在熔斷狀態下,系統不會再等待超時,而是直接返回失敗響應,減少系統資源占用,并避免因長時間等待導致用戶體驗的惡化。(也可以降級處理)
- 熔斷恢復機制:熔斷并非永久狀態。在一段時間后,熔斷器會進入半開狀態,允許少量請求測試服務的健康情況。如果恢復正常,熔斷器將關閉,恢復正常服務調用;如果仍有問題,則繼續保持熔斷。
1.3、降級策略
降級的目的是在某個服務的響應能力下降、或該服務不可用時,提供簡化版的功能或返回默認值作為 兜底,保持系統的部分功能可用,確保用戶體驗的連續性,避免系統頻繁報錯。
降級可以是手?動配置,也可以根據系統負?載自動觸發。系統可能由于?多種原因(如高負載、外部?依賴不可用等)觸發降級,?返回簡化的響應或默認值。
降級機制的好處:
- 優雅地處理故障:在降級狀態下,系統不會直接返回錯誤信息,而是提供一個替代方案。例如,某個數據查詢服務不可用時,系統可以返回緩存數據,確保用戶看到的是有效信息,而非錯誤頁面。
- 降低服務壓力:降級有助于減輕系統對非核心服務的依賴,確保核心功能的穩定運行。例如,當推薦系統或廣告服務出現故障時,降級可以減少對這些服務的調用,保護系統的整體穩定性。
1.4、熔斷和降級的區別
具體來說:
- 熔斷是當服務健康狀況惡化時,通過 切斷調用 避免系統資源浪費或服務間故障擴散。
- 降級是在系統壓力過大或某個服務不可用時,通過提供簡化的替代方案 ,保持系統的可用性和用戶體驗。
2、需求分析
2.1、對單個接口整體限流
目的:控制對?耗時較長的、經常訪問的接?口的請求頻率,防止過多請?求導致系過載
限流規則:
- 策略:整個接口每秒鐘不超過 10 次請求
- 阻塞操作:提示“系統壓力過大,請耐心等待”
熔斷規則:
- 熔斷條件:如果接口異常率超過 10%,或者慢調用(響應時長 > 3 秒)的比例大于 20%,觸發 60 秒熔斷。
- 熔斷操作:直接返回本地數據(緩存或空數據)
2.2、對單個 IP 訪問單個接口限流
限流規則:
- 策略:每個 IP 地址每分鐘允許查看題目列表的次數不能超過 60 次。
- 阻塞操作:提示“訪問過于頻繁,請稍后再試”
熔斷規則:
- 熔斷條件:如果接口異常率超過 10%,或者慢調用(響應時長 > 3 秒)的比例大于 20%,觸發 60 秒熔斷。
- 熔斷操作:直接返回本地數據(緩存或空數據)
3、Sentinel 介紹
Sentinel是阿里巴巴開源的限流、熔斷、降級組件,旨在為分布式系統提供可靠的保護機制。它設計用于解決高并發流量下的穩定性問題,并且支持與 Dubbo、Spring Cloud 等多種框架集成。
詳細內容可查看 -> 官方文檔
它的功能:
- 限流:支持基于 QPS、并發數量等條件的限流,支持滑動窗口、預熱、漏桶等算法。
- 熔斷降級:支持失敗率、慢調用比例等指標觸發熔斷,并提供自動恢復機制。
- 熱點參數限流:可以基于特定的參數進行限流,如限制特定用戶 ID 的請求頻率。
- 系統負載保護:可以根據系統的實際負載(如 CPU、內存)動態調整流量。
- 豐富的規則配置:通過配置中心或控制臺動態調整限流和熔斷規則。
優勢:功能?豐富、提供控制臺、?更新較頻繁、社區活?躍、文檔清晰,能夠?快速入門上手。
3.1、使用方式
使用 Sentinel 來進行資源保護,主要分為幾個步驟:
- 定義資源
- 定義規則
- 檢驗規則是否生效
3.2、架構設計
在 Sentinel 里面,所有的資源都對應一個資源名稱以及一個 Entry。Entry 可以通過對主流框架的適配自動創建,也可以通過注解的方式或調用 API 顯式創建;每一個 Entry 創建的時候,同時也會創建一系列功能插槽(slot chain)。這些插槽有不同的職責,例如:
3.2.1、NodeSelectorSlot
NodeSelectorSlot 的本質作用:Sentinel 是通過一條一條資源“鏈路”來判斷是否限流/降級的,而 它 就是用來維護這些資源鏈路的結構樹。
具體干這幾件事:
1、把資源“掛”到調用上下文樹上
比如你訪問 nodeA,它就會:
- 看你當前上下文是哪個入口(entrance1)
- 在 entrance1 節點下面查:有沒有掛過 nodeA
- 沒有就新建一個 DefaultNode(nodeA),掛上去!
2、同一個資源,但不同入口,樹結構是獨立的!
ContextUtil.enter("entrance1", "appA");
SphU.entry("nodeA"); // 第一次
ContextUtil.exit();ContextUtil.enter("entrance2", "appA");
SphU.entry("nodeA"); // 第二次
ContextUtil.exit();
這相當于你從 兩個不同入口玩了同一個關卡,Sentinel 會建立兩條路徑記錄:
machine-root├── EntranceNode(entrance1)│ └── DefaultNode(nodeA)└── EntranceNode(entrance2)└── DefaultNode(nodeA)
為什么這么設計?
因為在真實業務中:
- 你可能有多個入口(比如首頁、搜索頁都能訪問同一個資源)
- 你想控制每條鏈路的 QPS(比如“從搜索頁過來的 QPS 限制更嚴格”)
所以 Sentinel 就必須要分得清:
“你是從哪兒來的,然后才訪問的這個資源”
3.2.2、ClusterBuilderSlot
ClusterBuilderSlot 則用于存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用作為多維度限流,降級的依據;
ClusterBuilderSlot 的作用
1、調用方級別限流!
- 比如限制 “微信小程序” 每秒最多訪問 100 次,其他 origin 不受影響。
2、監控更細粒度!
- 哪個來源調用最多?
- 哪個來源的調用最慢、最容易出錯?
比如:是不是 Web 前端的調用出異常多,是不是他們傳參老有問題?
配合熱點參數限流、來源限流做更精細的控制。
3.3.3、StatisticSlot
StatisticSlot 是 Sentinel 的核心功能插槽之一,用于統計實時的調用數據。
- clusterNode:資源唯一標識的 ClusterNode 的 runtime 統計
- origin:根據來自不同調用者的統計信息
- defaultnode: 根據上下文條目名稱和資源 ID 的 runtime 統計入口的統計
Sentinel 底層采用高性能的滑動窗口數據結構 LeapArray 來統計實時的秒級指標數據,可以很好地支撐寫多于讀的高并發場景。
3.3.4、LeapArray底層原理
LeapArray 底層是一個固定長度的環形數組,每個元素是一個帶有時間戳的窗口格子 WindowWrap。
它支持在高并發場景下以時間為索引安全讀寫,保證統計的是“最近一段時間”的窗口數據,用于 Sentinel 的實時監控與限流、降級判斷。
結構總覽:
public class LeapArray<T> {protected final int sampleCount; // 窗口格子數,比如10protected final int intervalInMs; // 窗口總時長,比如1000ms(1s)private final WindowWrap<T>[] array; // 固定長度數組,環形復用
}
每個窗口格子是這樣一個類:
public class WindowWrap<T> {private final long windowStart; // 窗口開始時間private final long windowLength; // 每個格子的長度(interval / sampleCount)private final T value; // 存放統計信息的對象,比如 MetricBucket
}
它是怎么定位時間屬于哪個窗口的?
比如:
總窗口時間是 1000ms,格子數是 10
每個格子負責 100ms
現在時間是 10:00:01.450(即 1450ms)
① 算出這個時間屬于哪個格子:
int idx = (timestamp / windowLength) % sampleCount;
// 1450 / 100 = 14 -> 14 % 10 = 4,落在索引為4的格子
② 再判斷這個格子的起始時間是不是還有效:
//當前時間戳 ? 除以窗口長度 ? 計算出當前時間該落在哪個格子 ? 比較該格子的起始時間
//? 如果不同 ? 就滑窗、清理、重建!
window.windowStart == timestamp - (timestamp % windowLength)
如果有效:直接返回當前格子
如果無效(已經過期了):用 CAS 創建一個新的 WindowWrap(復用原來的槽位)
3.3.4.1、它是怎么滑動更新的?
核心點:每次請求到來時,才會“懶更新”窗口格子
流程如下:
1、當前時間戳算出對應窗口索引(如上)
2、取出格子對象,檢查是否是當前時間的窗口
- 是:直接用
- 否:CAS 替換為新窗口 + 清空舊統計值(滑動來了)
3、更新格子里的統計數據
它并不需要定時器或后臺線程維護窗口,而是靠訪問觸發滑動,非常節省資源。
3.3.4.2、并發怎么處理?
多線程訪問時可能并發寫同一個窗口:
- 用的是 AtomicReferenceArray + Compare-And-Swap (CAS)
- 每個格子內部統計信息用 LongAdder 等并發友好結構
3.3.4.3、總結:
LeapArray 是一個時間輪+懶更新+CAS構建的滑動窗口數組,專為高并發下限流降級而生。
3.3.5、FlowSlot
這個 slot 主要根據預設的資源的統計信息,按照固定的次序,依次生效。如果一個資源對應兩條或者多條流控規則,則會根據如下次序依次檢驗,直到全部通過或者有一個規則生效為止:
- 指定應用生效的規則,即針對調用方限流的;
- 調用方為 other 的規則;
- 調用方為 default 的規則。
3.3.6、DegradeSlot
這個 slot 主要針對資源的平均響應時間(RT)以及異常比率,來決定資源是否在接下來的時間被自動熔斷掉。
4、代碼實戰
4.1、查看題庫列表接口限流熔斷
/*** 分頁獲取題庫列表(封裝類)** @param questionBankQueryRequest* @param request* @return*/@PostMapping("/list/page/vo")@SentinelResource(value = "listQuestionBankVOByPage",blockHandler = "handleBlockException",fallback = "handleFallback")public BaseResponse<Page<QuestionBankVO>> listQuestionBankVOByPage(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,HttpServletRequest request) {long current = questionBankQueryRequest.getCurrent();long size = questionBankQueryRequest.getPageSize();// 限制爬蟲ThrowUtils.throwIf(size > 200, ErrorCode.PARAMS_ERROR);// 查詢數據庫Page<QuestionBank> questionBankPage = questionBankService.page(new Page<>(current, size),questionBankService.getQueryWrapper(questionBankQueryRequest));// 獲取封裝類return ResultUtils.success(questionBankService.getQuestionBankVOPage(questionBankPage, request));}
熔斷降級操作,處理 異常 / 熔斷場景下的兜底邏輯,避免用戶看到系統崩潰。
/*** listQuestionBankVOByPage 降級操作:直接返回本地數據*/public BaseResponse<Page<QuestionBankVO>> handleFallback(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,HttpServletRequest request, Throwable ex) {// 可以返回本地數據或空數據return ResultUtils.success(null);}
限流操作
/*** listQuestionBankVOByPage 流控操作* 限流:提示“系統壓力過大,請耐心等待”* 熔斷:執行降級操作*/public BaseResponse<Page<QuestionBankVO>> handleBlockException(@RequestBody QuestionBankQueryRequest questionBankQueryRequest,HttpServletRequest request, BlockException ex) {// 降級操作if (ex instanceof DegradeException) {return handleFallback(questionBankQueryRequest, request, ex);}// 限流操作return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系統壓力過大,請耐心等待");}
小Tips💡:為什么需要專門寫降級操作判斷?
// 降級操作if (ex instanceof DegradeException) {return handleFallback(questionBankQueryRequest, request, ex);}
Sentinel 中“熔斷觸發”其實本質上也是一種“被限流”行為,會先走 blockHandler,而不是直接走 fallback。
具體來說
情況 | 觸發邏輯 | 進入哪個方法? |
---|---|---|
QPS 超限 | Sentinel 流控規則 | blockHandler() |
異常率 > 50% 熔斷開啟 | Sentinel 熔斷規則 | 也進入 blockHandler() ! |
方法內空指針等異常 | Java 拋異常 | fallback() |
這時候你在 blockHandler() 里接收到的是 BlockException,它可能是:
- FlowException(QPS 控制)
- DegradeException(熔斷控制)
- ParamFlowException(熱點參數)
- AuthorityException(授權規則)
- SystemBlockException(系統保護)
你得根據 ex instanceof DegradeException
去區分熔斷的 case。
總結: 雖然 Sentinel 有 fallback 專門處理 異常 / 熔斷,但 在觸發熔斷時默認走的是 blockHandler,你必須自己判斷 ex 類型來決定要不要轉交給 fallback 處理。也可以理解為:只有業務?異常(比如請求參數錯誤、或?者數據庫操作失敗等問題),?才會算到熔斷條件中,限流熔?斷本身的異常 BlockE?xception 是不計算的
4.2、單 IP 查看題目列表限流熔斷
對于這個需求,需要對每個用戶進一步精細化限流,而不是整體接口限流,可以采用 熱點參數限流機制,允許根據參數控制限流觸發條件,例如:將 IP 地址作為熱點參數。
代碼如下:
/*** 分頁獲取題目列表(封裝類) - 限流版** @param questionQueryRequest* @param request* @return*/@PostMapping("/list/page/vo/sentinel")public BaseResponse<Page<QuestionVO>> listQuestionVOByPageSentinel(@RequestBody QuestionQueryRequest questionQueryRequest,HttpServletRequest request) {long current = questionQueryRequest.getCurrent();long size = questionQueryRequest.getPageSize();// 限制爬蟲ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);// 基于 IP 限流String remoteAddr = request.getRemoteAddr();Entry entry = null;try {entry = SphU.entry("listQuestionVOByPage", EntryType.IN, 1, remoteAddr);// 被保護的業務邏輯// 查詢數據庫Page<Question> questionPage = questionService.listQuestionByPage(questionQueryRequest);// 獲取封裝類return ResultUtils.success(questionService.getQuestionVOPage(questionPage, request));} catch (Throwable t) {// 自定義業務異常if (!BlockException.isBlockException(t)) {Tracer.trace(t);return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系統異常");}// 資源訪問阻止,被限流或被降級if (t instanceof DegradeException) {return handleFallback(questionQueryRequest, request, t);}// 限流操作return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "訪問過于頻繁,請稍后再試");} finally {if (entry != null) {entry.exit(1, remoteAddr);}}}/*** listQuestionVOByPageSentinel 降級操作:直接返回本地數據*/public BaseResponse<Page<QuestionVO>> handleFallback(@RequestBody QuestionQueryRequest questionQueryRequest,HttpServletRequest request, Throwable ex) {// 可以返回本地數據或空數據return ResultUtils.success(null);}
String remoteAddr = request.getRemoteAddr()
參數 remoteAddr 的作用是獲取用戶 IP,作為限流參數維度,防止一個 IP 亂刷。
entry = SphU.entry("listQuestionVOByPage", EntryType.IN, 1, remoteAddr)
作用是將 listQuestionVOByPage 作為資源注冊到 Sentinel 的上下文中,開始對該資源進行限流、熔斷、熱點參數等規則的監控與統計。
異常處理判斷邏輯:
情況 | 處理方式 |
---|---|
不是 Sentinel 異常(業務異常、系統異常) | 打點上報(Tracer.trace(t) ),提示“系統異常” |
是 Sentinel 熔斷異常(DegradeException ) | 走自定義的降級處理邏輯:handleFallback |
是 Sentinel 限流異常(FlowException 等) | 返回:“訪問過于頻繁,請稍后再試” |
重點💡
1、若 entry 的時候傳入了熱點參數,那么 exit 的時候也一定要帶上對應的參數(exit(count, args)),否則可能會有統計錯誤。
2、SphU.entry(xxx) 需要與 entry.exit() 方法成對出現,匹配調用,否則會導致調用鏈記錄異常,拋出 ErrorEntryFreeException 異常。
3、注意 Sentinel 的降級僅針對業務異常,對 Sentinel 限流降級本身的異常 BlockException 不生效。為了統計異常比例或異常數,需要手動通過 Tracer.trace(ex) 記錄業務異常。
4.3、定義規則
新建 sentinel 包并定義一個單獨的 Manager 作為 Bean,利用 @PostConstruct 注解,在 Bean 加載后創建規則。
@Component
public class SentinelRulesManager {@PostConstructpublic void initRules() throws Exception {initFlowRules();initDegradeRules();}// 限流規則public void initFlowRules() {// 單 IP 查看題目列表限流規則ParamFlowRule rule = new ParamFlowRule("listQuestionVOByPage").setParamIdx(0) // 對第 0 個參數限流,即 IP 地址.setCount(60) // 每分鐘最多 60 次.setDurationInSec(60); // 規則的統計周期為 60 秒ParamFlowRuleManager.loadRules(Collections.singletonList(rule));}// 降級規則public void initDegradeRules() {// 單 IP 查看題目列表熔斷規則DegradeRule slowCallRule = new DegradeRule("listQuestionVOByPage").setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType()).setCount(0.2) // 慢調用比例大于 20%.setTimeWindow(60) // 熔斷持續時間 60 秒.setStatIntervalMs(30 * 1000) // 統計時長 30 秒.setMinRequestAmount(10) // 最小請求數.setSlowRatioThreshold(3); // 響應時間超過 3 秒DegradeRule errorRateRule = new DegradeRule("listQuestionVOByPage").setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType()).setCount(0.1) // 異常率大于 10%.setTimeWindow(60) // 熔斷持續時間 60 秒.setStatIntervalMs(30 * 1000) // 統計時長 30 秒.setMinRequestAmount(10); // 最小請求數// 加載規則DegradeRuleManager.loadRules(Arrays.asList(slowCallRule, errorRateRule));}
}
本地化配置,讓 Sentinel 的限流和降級規則支持本地文件持久化(可讀可寫)
/*** 持久化配置為本地文件*/
public void listenRules() throws Exception {//獲取項目根目錄并創建 sentinel/ 文件夾String rootPath = System.getProperty("user.dir");File sentinelDir = new File(rootPath, "sentinel");if (!FileUtil.exist(sentinelDir)) {FileUtil.mkdir(sentinelDir);}// 設置規則文件路徑String flowRulePath = new File(sentinelDir, "FlowRule.json").getAbsolutePath();String degradeRulePath = new File(sentinelDir, "DegradeRule.json").getAbsolutePath();// 初始化 限流規則讀取器(拉模式)ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new FileRefreshableDataSource<>(flowRulePath, flowRuleListParser);FlowRuleManager.register2Property(flowRuleDataSource.getProperty());//注冊 限流規則寫入器WritableDataSource<List<FlowRule>> flowWds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);WritableDataSourceRegistry.registerFlowDataSource(flowWds);// 降級規則讀取器FileRefreshableDataSource<List<DegradeRule>> degradeRuleDataSource= new FileRefreshableDataSource<>(degradeRulePath, degradeRuleListParser);DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());WritableDataSource<List<DegradeRule>> degradeWds = new FileWritableDataSource<>(degradeRulePath, this::encodeJson);// Register to writable data source registry so that rules can be updated to fileWritableDataSourceRegistry.registerDegradeDataSource(degradeWds);
}//JSON字符串 -> 規則對象
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(source,new TypeReference<List<FlowRule>>() {});
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(source,new TypeReference<List<DegradeRule>>() {});// 規則對象 -> JSON字符串
private <T> String encodeJson(T t) {return JSON.toJSONString(t);
}
注意:在初始化時加入 listenRules()
方法
@PostConstructpublic void initRules() throws Exception {initFlowRules();initDegradeRules();listenRules();}
5、總結
Sentinel 的規則配置 + 接口中的限流/熔斷操作 的聯動協同
5.1、規則配置部分(限流 & 熔斷)
①在內存中加載規則(程序啟動時)
如對 listQuestionVOByPage 添加:
- 限流:每個 IP 每分鐘訪問不超 60 次
- 熔斷:慢調用比例超 20%,觸發熔斷;異常率超 10%,也觸發熔斷
②文件持久化監聽
調用 listenRules() 做兩件事:監聽本地文件變化,自動刷新配置
將規則寫回文件(支持動態修改)
5.2、接口限流/熔斷的具體操作(怎么用規則)
①注解方式(簡潔明了)
具體來說:
應規則名就是 value 指定的 “listQuestionBankVOByPage”:
- 如果被限流了 ? 走 handleBlockException()
- 如果被熔斷了 ? 也會走 handleBlockException(),但你可以判斷異常類型 DegradeException
- 如果業務異常了(比如系統異常)? 走 handleFallback()
②編程方式(可擴展性強)
entry = SphU.entry("listQuestionVOByPage", EntryType.IN, 1, remoteAddr);
- 參數 remoteAddr 作為限流 key(這是為了實現 ParamFlowRule)
- try-catch 中寫核心業務邏輯
- catch 中判斷是否是限流/熔斷異常:
對于 對單個接口整體限流 我們使用 @SentinelResource 注解方式 做限流操作,這時注解會監控所有參數。
對于 單個IP查看題目列表限流熔斷 用 編程式 做限流操作,是因為我們只需要對 用戶IP 這一個參數監控,不需要對傳入的所有參數做監控。