1. 簡介
訪問速率限制是一種API訪問限制的策略。它限制客戶端在一定時間內呼叫 API 的次數。這有助於保護應用程式介面,防止無意或惡意的過度使用。
速率限制通常是透過跟蹤 IP 地址或更具體的業務方式(如 API 金鑰或訪問令牌等方式)來應用於 API 的。作為 API 開發人員,當客戶端達到限制時,我們有幾種選擇:
請求排隊,直到剩餘時間結束(這也是最常用的方式)
拒絕請求(HTTP 429 請求過多)
本篇文章將介紹一款開源的元件Bucket4j,該元件提供了強大的限流功能。基於基於令牌桶演算法。既可用於獨立的 JVM 應用程式,也可用於叢集環境。它還透過 JCache(JSR107)規範支援記憶體或分散式快取。
令牌桶演算法
假設我們有一個 "桶",其容量被定義為可容納的令牌數量。每當消費者想要訪問 API 端點時,就必須從桶中獲取一個令牌。如果有令牌,我們就會從資料桶中移除令牌,並接受請求。反之,如果程式桶中沒有令牌,我們就會拒絕請求。
在請求消耗令牌(token)的同時,我們也在以某種固定的速度補充令牌。
考慮一個速率限制為每分鐘 100 個請求的應用程式介面。我們可以建立一個容量為 100 的水桶,每分鐘填充 100 個令牌。如果我們在一分鐘內收到 70 個請求,少於可用令牌的數量,那麼在下一分鐘開始時,我們只需再新增 30 個令牌,就能使水桶達到容量。另一方面,如果我們在 40 秒內用完了所有令牌,我們將等待 20 秒來重新裝滿令牌桶。
接下來將詳細介紹在Spring Boot中如何使用Bucket4j實現限流。
2. 實戰案例
2.1 環境準備
引入依賴
<dependency> <groupId>com.giffing.bucket4j.spring.boot.starter</groupId> <artifactId>bucket4j-spring-boot-starter</artifactId> <version>0.12.7</version> </dependency> <dependency> <groupId>com.bucket4j</groupId> <artifactId>bucket4j-redis</artifactId> <version>8.10.1</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.
接下來的案例是基於redis的,所以引入了jedis。你也可以是lettuce或者是redisson但是這2個貌似需要是webflux環境。
jedis配置
@Bean public JedisPool jedisPool( @Value("${spring.data.redis.port}") Integer port, @Value("${spring.data.redis.host}") String host, @Value("${spring.data.redis.password}") String password, @Value("${spring.data.redis.database}") Integer database ) { // buildPoolConfig方法自己進行配置吧 final JedisPoolConfig poolConfig = buildPoolConfig(); return new JedisPool(poolConfig, host, port, 60000, password, database); } 1.2.3.4.5.6.7.8.9.10.11.
以上基礎環境就準備好了,接下來就可以進行規則配置。而規則的配置可以基於2中方式,基於配置檔案和基於註解(AOP)。
定義介面
@RestController @RequestMapping("/products") public class ProductController { @GetMapping("/{id}") public Product getProduct(@PathVariable Integer id) { return new Product(id, "商品 - " + id, BigDecimal.valueOf(new Random().nextDouble(1000))) ; } } 1.2.3.4.5.6.7.8.9.10.
接下來我將基於上面的介面進行限流的配置。
2.2 基於配置檔案
基於配置檔案的規則配置底層實現是透過Filter。
bucket4j: cache-to-use: redis-jedis filter-config-caching-enabled: true filters: - cache-name: product_cache_name id: product_filter # 配置請求url的規則;這裏底層是透過正規表示式進行匹配的 url: /products/.* rate-limits: - #這裏的cache-key非常關鍵;用於區分不同請求的情況; #比如,這裏我會根據不同的請求id來現在訪問速率 #這裏可以寫spel表示式,這裏呼叫的是HttpServletRequest#getParameter方法 cache-key: getParameter("id") bandwidths: #配置桶的容量 - capacity: 2 # 時間 time: 30 # 單位 unit: seconds # 填充速度;這會每隔30秒進行填充 refill-speed: interval 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.
修改預設的限流提示
bucket4j: filters: - cache-name: product_cache_name http-content-type: 'application/json;charset=utf-8' http-response-body: '{"code": -1, "message": "請求太快了"}' 1.2.3.4.5.
注意:你必須同時要設定content-type設定字元編碼,否則會亂碼。
條件放行
你也可以透過如下屬性進行有條件的放行;
bucket4j: filters: - cache-name: product_cache_name rate-limits: - skip-condition: 'getParameter("id").equals("6")' 1.2.3.4.5.6.
當請求id的值為6時則跳過規則,直接方向。
以上是基於配置檔案規則的應用,它還有很多其它的配置屬性,詳細檢視官方文件
接下來介紹基於註解的方式。
2.3 基於註解
透過"@RateLimiting"註解,AOP 可以攔截目標方法。這樣,你就可以全面訪問方法引數,輕鬆定義速率限制鍵或有條件地跳過速率限制。
配置檔案中配置規則
bucket4j: methods: - name: storage_rate #在程式碼中會透過該名稱引用 cache-name: storage_cache_name rate-limit: bandwidths: - capacity: 2 time: 30 unit: seconds refill-speed: interval 1.2.3.4.5.6.7.8.9.10.
介面註解,配置限流
@RateLimiting( name = "storage_rate", cacheKey = "'storage-' + #id", skipCondition = "#name eq 'admin'", ratePerMethod = true, fallbackMethodName = "getStorageFallback" ) @GetMapping("/{id}") public R<Storage> getStorage(@PathVariable Integer id, String name) { return R.success(new Storage(id, "SP001 - " + id, new Random().nextInt(10000))) ; } // 回退方法的簽名必須與業務方法一致 public R<Storage> getStorageFallback(Integer id, String name) { return R.failure(String.format("請求id=%d,name=%s被限流", id, name)) ; } 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.
skipCondition:該屬性定義瞭如果請求的name的值為admin則跳過,不限流。
@RateLimiting註解還可以應用到類中,這樣該類中的所有方法都會被限流,如下示例:
@Service @RateLimiting( name = "storage_rate", cacheKey = "getName", ratePerMethod = false ) public class StorageService { public Storage queryStorageById(Integer id) { return new Storage(id, "SP001 - " + id, new Random().nextInt(10000)) ; } @IgnoreRateLimiting public List<Storage> queryStorages() { return List.of( new Storage(1, "SP001 - " + 1, new Random().nextInt(10000)), new Storage(2, "SP002 - " + 2, new Random().nextInt(10000)), new Storage(3, "SP003 - " + 3, new Random().nextInt(10000)) ) ; } } 1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.
上面程式碼queryStorageById會被限流,而queryStorages方法被@IgnoreRateLimiting註解標準,所以不會被限流。