【 限流技術 | 從四大限流算法到Redisson令牌桶實踐 】

引言:為什么需要限流?

在現代分布式系統中,服務的穩定性是至關重要的。在遇到突發的請求量激增,惡意的用戶訪問,亦或是請求頻率過高給下游服務帶來較大壓力時,我們常常需要通過緩存、限流、熔斷降級、負載均衡等多種方式保證服務的穩定性。其中限流是不可或缺的一環,這篇文章介紹限流相關知識。

限流(Rate Limiting)?正是為了解決這一問題而生的關鍵技術。它通過控制請求的速率,將流量限制在系統所能處理的合理范圍內,從而:

  • 保障服務可用性:防止系統被過量請求拖垮。

  • 實現公平使用:保護資源不被少數用戶或惡意請求獨占。

  • 應對突發流量:通過平滑處理請求,避免后端服務被沖垮。

四種常見的限流算法

限流方式核心原理實現復雜度
固定窗口限流算法將時間劃分為固定窗口(如 1 秒),統計窗口內請求數,超過閾值則限流
滑動窗口限流算法將固定窗口劃分為多個小時間片,隨時間滑動計算窗口內請求數
漏桶算法請求像水一樣進入固定容量的桶,桶以固定速率出水,溢出則限流
令牌桶算法系統以固定速率向桶中放令牌,請求需獲取令牌才能通過,桶可累積令牌

1.固定窗口限流算法

1.1 什么是固定窗口算法

固定窗口算法又叫計數器算法,是一種簡單方便的限流算法。他的原理是:維護一個計數器,將固定單位時間段當作一個窗口,計數器會記錄這個窗口接收請求的次數。

當一個新的請求到達時,系統會檢查當前窗口內的計數器值。

1.如果計數器未達到預設的閾值,允許請求訪問,計數器自增。
2.當計數器已達到或大于限流閾值,拒絕該請求,觸發限流規則。
3.直到進入下一個時間窗口,此時計數器會被重置為零,重新開始計數

假設單位時間是60秒,限流閥值為10。在單位時間60秒內,每來一個請求,計數器就加1,如果計數器累加的次數超過限流閥值10,后續的請求全部會被拒絕。直到進入下一個時間窗口,此時計數器會被重置為零,重新開始計數。

1.2 偽代碼實現
   public static Integer counter = 0;  //統計請求數public static long lastAcquireTime =  0L;public static final Long windowUnit = 1000L ; //假設固定時間窗口是1000mspublic static final Integer threshold = 10; // 窗口閥值是10public synchronized boolean fixedWindowsTryAcquire() {long currentTime = System.currentTimeMillis();  //獲取系統當前時間if (currentTime - lastAcquireTime > windowUnit) {  //檢查是否在時間窗口內counter = 0;  // 計數器清0lastAcquireTime = currentTime;  //開啟新的時間窗口}if (counter < threshold) {  // 小于閥值counter++;  //計數統計器加1return true;}return false;}

1.2 固定窗口算法的優缺點

優點:實現簡單,資源消耗低
缺點:存在臨界問題,可能在窗口切換時出現雙倍流量峰值

舉例:窗口大小為 1 秒,閾值為 10 次請求。第 1 個窗口的最后 100ms 內涌入 10 次請求(未超限)。第 2 個窗口的前 100ms 內又涌入 10 次請求(未超限)。實際 200ms 內總請求數達到 20 次,遠超 “1 秒 10 次” 的限制,可能瞬間壓垮系統。

2.?滑動窗口限流算法

2.1 什么是滑動窗口限流算法

滑動窗口算法可以看作是固定窗口算法的一種改進和優化。它不再將時間劃分為離散的、獨立的窗口,而是將時間視為一個連續的整體。它將單位時間周期分為n個小周期,分別記錄每個小周期內接口的訪問次數,并且根據時間滑動刪除過期的小周期。

2.2 滑動窗口解決邊界問題

如圖:在 0.8-1.0 秒來了 5 個請求后,1.0-1.2 秒再發 5 個請求,固定窗口會認為是兩個窗口而全部允許,滑動窗口則因這 10 個請求都在 0.2-1.2 秒的滑動區間內而限流后 5 個,解決了邊界流量突增問題。

場景固定窗口處理邏輯滑動窗口處理邏輯
邊界點前后請求分屬兩個窗口,分別計數同屬 “最近 1 秒” 窗口,合并計數
1000~1200ms 請求數允許 5 個(總 10 個)全部限流(總請求數超 5)
核心原因窗口固定,邊界點前后割裂窗口動態滑動,覆蓋邊界點前后區間

2.3 代碼實現
?
public class SimpleSlidingWindow {// 窗口配置private static final long WINDOW_MS = 1000;    // 總窗口1秒private static final int SUB_WINDOWS = 5;      // 5個小窗口private static final long SUB_WINDOW_MS = WINDOW_MS / SUB_WINDOWS;  // 每個200msprivate static final int THRESHOLD = 5;        // 限流閾值// 存儲每個小窗口的請求數private final int[] counts = new int[SUB_WINDOWS];private int totalRequests;  // 當前窗口總請求數private final Lock lock = new ReentrantLock();public boolean tryAcquire() {long now = System.currentTimeMillis();lock.lock();try {// 1. 計算當前小窗口索引int currentIdx = (int) (now / SUB_WINDOW_MS % SUB_WINDOWS);// 2. 清理過期的小窗口long windowStart = now - WINDOW_MS;for (int i = 0; i < SUB_WINDOWS; i++) {long subWindowStart = (i * SUB_WINDOW_MS);if (subWindowStart < windowStart) {totalRequests -= counts[i];counts[i] = 0;}}// 3. 判斷是否超過閾值if (totalRequests >= THRESHOLD) {return false;}// 4. 記錄當前請求counts[currentIdx]++;totalRequests++;return true;} finally {lock.unlock();}}?
2.4 滑動窗口算法的優缺點

優點:能平滑流量,解決固定窗口臨界問題,避免窗口切換時流量翻倍。

缺點:實現復雜度高,需維護動態數據結構,增加內存和計算開銷。

3. 漏桶限流算法

3.1 什么是漏桶限流算法

漏桶限流算法的核心思想是將請求視為水滴,進入一個固定容量的(隊列)中。桶的底部有一個小孔,水會以一個恒定的速率從桶中流出,這個速率代表了系統能夠穩定處理請求的速率。當請求(水滴)的流入速率小于或等于桶的流出速率時,請求可以被及時處理,不會發生積壓。

當請求的流入速率小于或等于桶的流出速率時,請求可以被及時處理,不會發生積壓。

當請求的流入速率超過流出速率時,桶中的水會逐漸增多,桶中的未處理請求會增加。

如果桶被裝滿,那么后續新進入的請求就會被直接丟棄或拒絕,直到桶中有足夠的空間為止。

3.2 代碼實現
public class LeakyBucketRateLimiter {private final long capacity; // 桶的容量private long water; // 當前桶中的水量(請求數)private final long rate; // 漏出速率 (請求/秒)private long lastLeakTimestamp; // 上次漏水時間戳public LeakyBucketRateLimiter(long capacity, long rate) {this.capacity = capacity;this.rate = rate;this.water = 0;this.lastLeakTimestamp = System.currentTimeMillis();}public synchronized boolean tryAcquire() {long now = System.currentTimeMillis();// 計算這段時間內應該漏掉的水量long leakedWater = (now - lastLeakTimestamp) * rate / 1000;// 更新桶中的水量,但不能小于0water = Math.max(0, water - leakedWater);lastLeakTimestamp = now;// 判斷桶是否還有空間if (water < capacity) {water++; // 新請求加入桶中return true; // 請求被接受} else {return false; // 桶滿了,拒絕請求}}
}

3.3?漏桶算法的優缺點

優點:漏桶算法最突出的優點是能提供絕對平滑、恒定的請求處理速率(水流穩定輸出)

缺點:無法應對突發流量,即便系統有空閑資源,超出自設流出速率的請求也會被拒;當請求速率持續高于流出速率時,桶會快速填滿,導致后續請求被阻或拒,增加平均延遲甚至超時。

4. 令牌桶限流算法(非常推薦)

4.1?什么是令牌桶限流算法

令牌桶算法的核心思想是,系統會以一個恒定的速率向一個固定容量的桶中放入令牌。每個請求在到達時,都需要先從桶中獲取一個令牌。如果桶中有可用的令牌,請求就可以立即被處理,并消耗掉一個令牌。如果桶中沒有令牌了,那么請求就會被拒絕或阻塞,直到有新的令牌被放入桶中。

解決突發流量問題:令牌桶在應對突發流量的時候,桶內假如有 10?個令牌,那么這 10 個令牌可以馬上被取走,而不像漏桶那樣勻速的消費。所以在應對突發流量的時候令牌桶表現的更佳

4.2 偽代碼實現
public class TokenBucket {private final double capacity; // 令牌桶容量private final double rate; // 令牌生成速率 (令牌/毫秒)private double tokens; // 當前桶中的令牌數private long lastRefillTimestamp; // 上次填充令牌的時間戳public TokenBucket(double capacity, double rate) {this.capacity = capacity;this.rate = rate;this.tokens = capacity; // 初始時桶是滿的this.lastRefillTimestamp = System.currentTimeMillis();}public synchronized boolean acquire(int numTokens) {long now = System.currentTimeMillis();// 計算這段時間內應該生成的令牌數double newTokens = (now - lastRefillTimestamp) * rate;// 更新桶中的令牌數,但不能超過容量tokens = Math.min(capacity, tokens + newTokens);lastRefillTimestamp = now;// 判斷桶中是否有足夠的令牌if (tokens >= numTokens) {tokens -= numTokens; // 消耗令牌return true; // 請求被允許} else {return false; // 令牌不足,請求被拒絕}}
}

在這個實現中,tryAcquire()方法首先根據時間差計算出應該“漏掉”的請求數量,并更新桶中的當前水量。然后,它檢查桶是否已滿。如果未滿,則將新請求加入桶中(water++),并返回true;否則,返回false,表示請求被拒絕。這種實現方式確保了請求的處理速率被嚴格限制在rate所定義的范圍內,從而實現了流量的平滑輸出。

4.3?漏桶算法的優缺點

優點:支持突發流量,低負載時積累的令牌可應對突發請求,適用于秒殺、突發熱點等場景;靈活性高,可通過調整令牌生成速率和桶容量平衡系統穩定性與資源利用率。

缺點:實現較復雜,需精確處理令牌生成與消費及并發一致性。(復雜度較高)

5. 基于Redisson的令牌桶限流實戰

RRateLimiter+AOP:基于令牌桶算法的分布式限流器RRateLimiter結合Spring框架的AOP(面向切面編程)技術,將限流邏輯應用到業務代碼中,實現注解式的分布式限流。

5.1 自定義限流注解@RateLimiter

首先,我們需要定義一個自定義注解@RateLimiter,用于標記需要進行限流的方法。這個注解可以包含一些配置屬性,如限流的key、限流速率、時間單位、限流類型(按IP、按用戶ID等)以及被限流時的提示信息

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {// 限流key的前綴String key() default "";// 限流速率(單位時間內允許的請求數)int count() default 10;// 限流時間單位(秒)int time() default 60;// 限流類型(IP、用戶等)LimitType limitType() default LimitType.USER;// 被限流時的提示信息String message() default "請求頻繁,請稍后重試";
}enum LimitType {IP,USER,API
}
5.2 定義限流切面RateLimiterAspect

接下來,我們需要定義一個AOP切面?RateLimiterAspect?,它負責攔截所有帶有?@RateLimiter?注解的方法,并在方法執行前執行限流邏輯。這個切面會解析注解中的配置生成唯一的限流key,然后調用Redisson的RRateLimiter來判斷當前請求是否應該被允許。

@Aspect
@Component
@Slf4j
public class RateLimitAspect {@Resourceprivate RedissonClient redissonClient;@Resourceprivate UserService userService;@Before("@annotation(rateLimit)")public void doBefore(JoinPoint point, RateLimit rateLimit) {String key = generateRateLimitKey(point, rateLimit);// 使用Redisson的分布式限流器RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);rateLimiter.expire(Duration.ofHours(1)); // 1 小時后過期// 設置限流器參數:每個時間窗口允許的請求數和時間窗口rateLimiter.trySetRate(RateType.OVERALL, rateLimit.rate(), rateLimit.rateInterval(), RateIntervalUnit.SECONDS);// 嘗試獲取令牌,如果獲取失敗則限流if (!rateLimiter.tryAcquire(1)) {throw new BusinessException(ErrorCode.TOO_MANY_REQUEST, rateLimit.message());}}private String generateRateLimitKey(JoinPoint point, RateLimit rateLimit) {StringBuilder keyBuilder = new StringBuilder();keyBuilder.append("rate_limit:");// 添加自定義前綴if (!rateLimit.key().isEmpty()) {keyBuilder.append(rateLimit.key()).append(":");}// 根據限流類型生成不同的keyswitch (rateLimit.limitType()) {case API:// 接口級別:方法名MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();keyBuilder.append("api:").append(method.getDeclaringClass().getSimpleName()).append(".").append(method.getName());break;case USER:// 用戶級別:用戶IDtry {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {HttpServletRequest request = attributes.getRequest();User loginUser = userService.getLoginUser(request);keyBuilder.append("user:").append(loginUser.getId());} else {// 無法獲取請求上下文,使用IP限流keyBuilder.append("ip:").append(getClientIP());}} catch (BusinessException e) {// 未登錄用戶使用IP限流keyBuilder.append("ip:").append(getClientIP());}break;case IP:// IP級別:客戶端IPkeyBuilder.append("ip:").append(getClientIP());break;default:throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的限流類型");}return keyBuilder.toString();}private String getClientIP() {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return "unknown";}HttpServletRequest request = attributes.getRequest();String ip = request.getHeader("X-Forwarded-For");if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("X-Real-IP");}if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}// 處理多級代理的情況if (ip != null && ip.contains(",")) {ip = ip.split(",")[0].trim();}return ip != null ? ip : "unknown";}}

3.2.3 在業務方法上應用限流注解

只需在需要進行限流控制的方法上添加@RateLimiter注解,配置實際要求即可。

//@GetMapping(value = "/chat/gen/code", produces = MediaType.TEXT_EVENT_STREAM_VALUE)//用戶限制          單個窗口請求數量  窗口時長      響應的消息    
@RateLimit(limitType = RateLimitType.USER, rate = 5, rateInterval = 60, message = "AI 對話請求過于頻繁,請稍后再試")
public Flux<ServerSentEvent<String>> chatToGenCode(@RequestParam Long appId,@RequestParam String message,HttpServletRequest request) {}

在這個例子中,/chat/gen/code接口被配置為單個用戶每分鐘最多只能處理5個請求。任何超出這個頻率的請求都會被AOP切面攔截并拒絕。這種方式將限流邏輯與業務邏輯完全解耦,使得代碼更加清晰、易于維護。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/920027.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/920027.shtml
英文地址,請注明出處:http://en.pswp.cn/news/920027.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入解析Java NIO多路復用原理與性能優化實踐指南

深入解析Java NIO多路復用原理與性能優化實踐指南 技術背景與應用場景 在高并發網絡編程中&#xff0c;傳統的阻塞 I/O 模型往往因每個連接都占用一個線程或一個系統調用而導致線程資源浪費、線程切換開銷劇增等問題&#xff0c;難以滿足數萬甚至數十萬并發連接的負載要求。Jav…

目標檢測數據集 第006期-基于yolo標注格式的汽車事故檢測數據集(含免費分享)

目錄 目標檢測數據集 第006期-基于yolo標注格式的汽車事故檢測數據集(含免費分享) 超實用汽車事故檢測數據集分享&#xff0c;助力計算機視覺研究&#xff01; 1、背景 2、數據詳情 數據集基本信息 結構組成 標注格式與示例 類標簽說明 數據增強情況 3、應用場景 4、…

應用密碼學(書籍學習筆記、基礎知識) 一

本博客為讀《應用密碼學》所得筆記 文章目錄一、 加密與解密1.2 秘鑰Key1.2.1 引入秘鑰K1.2.2 加密秘鑰K1&#xff0c;解密秘鑰K2二、對稱算法 VS 公開密鑰算法**① 對稱算法** - 傳統密碼算法 **(Symmetric Algorithm) &#x1f511;****② 非對稱算法特點** - 公開秘鑰算法 *…

【攻防世界】Web_php_include

1.信息收集題目&#xff1a;Web_php_include &#xff1a;PHP文件包含漏洞2.思路&#xff1a;1.代碼審計&#xff1a;<?php show_source(__FILE__); echo $_GET[hello]; $page$_GET[page]; while (strstr($page, "php://")) { //在一個字符串中查…

cmake--CPack/deb

deb包的需求 怎么使用cmake把項目的依賴想打包為deb包,把項目的可執行文件和依賴文件打包為deb包,又怎么樣配置apt源,讓項目在jenkins構建之后,可以通過sudo apt install 下載deb包和安裝到任意主機上? 整體流程概覽 使用CMake構建項目:確保你的項目可以被CMake正確編譯…

七十五、【Linux數據庫】部署Redis服務 、 部署LNMP+Redis

Redis 與 LNMP 集成功能概述 Redis 核心功能 內存數據存儲:高速讀寫性能 數據結構豐富:字符串、哈希、列表、集合等 持久化支持:RDB快照和AOF日志 發布訂閱:消息隊列功能 高可用:主從復制、哨兵模式、集群 LNMP+Redis 集成價值 會話共享:多Web服務器共享Session 數據緩存…

從YOLOv5到RKNN:零沖突轉換YOLOv5模型至RK3588 NPU全指南

從YOLOv5到RKNN&#xff1a;零沖突轉換YOLOv5模型至RK3588 NPU全指南 在嵌入式AI領域&#xff0c;將訓練好的深度學習模型高效部署到邊緣設備的NPU&#xff08;神經網絡處理器&#xff09;上是提升性能的關鍵。本文將詳細介紹如何在Ubuntu 20.04環境下&#xff0c;將YOLOv5l模型…

DNS的解析過程是怎樣的?它基于傳輸層的什么協議?

問題DNS的解析過程是怎樣的&#xff1f;它基于傳輸層的什么協議&#xff1f;我的回答&#xff1a;DNS解析過程是將域名轉換為IP地址的一系列步驟。這個過程涉及多級緩存和查詢&#xff1a;首先是瀏覽器緩存&#xff0c;瀏覽器會先檢查自己的DNS緩存是否有記錄。接著是操作系統緩…

模擬互聯網大廠Java面試:電商場景下的技術探討

模擬互聯網大廠Java面試&#xff1a;電商場景下的技術探討 場景概述 在這場模擬面試中&#xff0c;我們設定了一位互聯網大廠的面試官與候選人小C之間的對話。面試官嚴肅專業&#xff0c;而小C則是搞笑的“水貨程序員”。通過三輪問答&#xff0c;我們探索了Java技術棧在電商場…

遙感機器學習入門實戰教程|Sklearn案例⑤:集成學習方法全覽

在機器學習的實際應用中&#xff0c;單一分類器往往存在局限&#xff1a;比如決策樹容易過擬合&#xff0c;kNN 對噪聲敏感&#xff0c;邏輯回歸在高維數據下收斂慢。為了提升整體效果&#xff0c;我們通常會采用 集成學習&#xff08;Ensemble Learning&#xff09;。 這篇文章…

大模型在垂直場景中的創新應用:搜索、推薦、營銷與客服的新玩法

1. 引言 背景介紹:簡述大模型(如GPT、BERT等)的發展歷程及其在AI領域的核心作用,強調其在垂直場景中的潛力。 主題聚焦:說明本文將深入探討搜索、推薦、營銷、客服四大場景,分析大模型帶來的創新開發方式。 目的與意義:闡述新玩法如何提升效率、增強用戶體驗,并推動行業…

華為倉頡語言的class(類)初步

華為倉頡語言的class&#xff08;類&#xff09;初步 class 概念 【官方文檔 https://cangjie-lang.cn/docs?url%2F1.0.0%2Fuser_manual%2Fsource_zh_cn%2Fclass_and_interface%2Fclass.html 】 class 是倉頡面向對象體系的核心&#xff0c;用來描述“引用類型”對象。與 s…

健康常識查詢系統|基于java和小程序的健康常識查詢系統設計與實現(源碼+數據庫+文檔)

健康常識查詢系統 目錄 基于java和小程序的健康常識查詢系統設計與實現 一、前言 二、系統設計 三、系統功能設計 小程序功能設計 后臺功能設計 四、數據庫設計 五、核心代碼 六、論文參考 七、最新計算機畢設選題推薦 八、源碼獲取&#xff1a; 博主介紹&#xf…

MySQL的高可用+MHA

即MySQL 主從復制高可用架構&#xff0c;是一套優秀的MySQL 高可用解決方案&#xff0c;由日本 DeNA 公司 youshimaton 開發&#xff0c;主要用于保障 MySQL 數據庫在主服務器出現故障時&#xff0c;能快速進行主從切換&#xff0c;減少數據庫服務中斷時間。其核心特點包括&…

淘寶pc端首頁做了哪些性能優化?

淘寶PC端首頁作為中國電商領域流量最大的頁面之一&#xff0c;其性能優化手段可以說是業界標桿&#xff0c;非常全面和深入。這些優化不是單一技術&#xff0c;而是一個完整的體系。 我們可以從以下幾個層面來分析和理解淘寶首頁所做的性能優化&#xff1a; 一、核心指標與整體…

讓醫學數據更直觀——MedCalc 23.1.7 最新版使用體驗

軟件介紹 MedCalc 23.1.7是一款功能強大的生物醫學研究統計軟件&#xff0c;專為醫學科研人員和醫療保健專家設計。它提供了豐富的統計分析工具和方法&#xff0c;旨在幫助用戶更好地分析和解釋醫學數據。以下是該軟件的一些主要特點&#xff1a; 一、數據導入和管理 支持導…

Text2SQL、ChatBI簡介

概述 傳統BI的三大核心瓶頸&#xff1a; 問數之難&#xff1a;不同用戶往往存在個性化的分析邏輯&#xff0c;盡管企業內部已經創建大量報表和看板&#xff0c;但仍然無法完全滿足業務部門對數據的個性化需求。但傳統BI門檻較高&#xff0c;非技術人員在統一培訓前&#xff0…

神經網絡中 標量求導和向量求導

0. 引出問題 在神經網絡反向傳播過程中 loss [loss?,loss?, loss?]&#xff0c;為什么 ?loss/?w ?loss?/?w ?loss?/?w ?loss?/?w ?loss?/?w 和 loss 維度一樣都是三位向量 &#xff0c;[?loss?/?w, ?loss?/?w, ?loss?/?w] 就變成3*3的矩陣 如下所…

tcpdump命令打印抓包信息

tcpdump命令打印抓包信息 下面是在服務器抓取打印服務端7701端口打印 rootgb:/home/gb# ifconfig -a eth0: flags4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500inet 10.250.251.197 netmask 255.255.255.0 broadcast 10.250.251.255inet6 fe80::76fe:48ff:fe94:5a5 …

Mysql-經典實戰案例(13):如何通過Federated實現跨實例訪問表

實現原理&#xff1a;使用Federated引擎本創建一個鏈接表實現&#xff0c;但是Federated 引擎只是一個按列的順序和類型解析遠程返回的數據流準備工作&#xff1a; 1. 本地庫啟用 Federated 引擎查看是否已啟用&#xff1a; SHOW ENGINES;如果Federated 引擎的 Support 是 YES …