神器!API接口限流就是這么簡單
環(huán)境:SpringBoot3.2.5
1. 簡介
訪問速率限制是一種API訪問限制的策略。它限制客戶端在一定時間內(nèi)調(diào)用 API 的次數(shù)。這有助于保護應(yīng)用程序接口,防止無意或惡意的過度使用。
速率限制通常是通過跟蹤 IP 地址或更具體的業(yè)務(wù)方式(如 API 密鑰或訪問令牌等方式)來應(yīng)用于 API 的。作為 API 開發(fā)人員,當客戶端達到限制時,我們有幾種選擇:
- 請求排隊,直到剩余時間結(jié)束(這也是最常用的方式)
- 拒絕請求(HTTP 429 請求過多)
本篇文章將介紹一款開源的組件Bucket4j,該組件提供了強大的限流功能。基于基于令牌桶算法。既可用于獨立的 JVM 應(yīng)用程序,也可用于集群環(huán)境。它還通過 JCache(JSR107)規(guī)范支持內(nèi)存或分布式緩存。
令牌桶算法
假設(shè)我們有一個 "桶",其容量被定義為可容納的令牌數(shù)量。每當消費者想要訪問 API 端點時,就必須從桶中獲取一個令牌。如果有令牌,我們就會從數(shù)據(jù)桶中移除令牌,并接受請求。反之,如果程序桶中沒有令牌,我們就會拒絕請求。
在請求消耗令牌(token)的同時,我們也在以某種固定的速度補充令牌。
考慮一個速率限制為每分鐘 100 個請求的應(yīng)用程序接口。我們可以創(chuàng)建一個容量為 100 的水桶,每分鐘填充 100 個令牌。如果我們在一分鐘內(nèi)收到 70 個請求,少于可用令牌的數(shù)量,那么在下一分鐘開始時,我們只需再添加 30 個令牌,就能使水桶達到容量。另一方面,如果我們在 40 秒內(nèi)用完了所有令牌,我們將等待 20 秒來重新裝滿令牌桶。
接下來將詳細介紹在Spring Boot中如何使用Bucket4j實現(xiàn)限流。
2. 實戰(zhàn)案例
2.1 環(huán)境準備
引入依賴
<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>
接下來的案例是基于redis的,所以引入了jedis。你也可以是lettuce或者是redisson但是這2個貌似需要是webflux環(huán)境。
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);
}
以上基礎(chǔ)環(huán)境就準備好了,接下來就可以進行規(guī)則配置。而規(guī)則的配置可以基于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))) ;
}
}
接下來我將基于上面的接口進行限流的配置。
2.2 基于配置文件
基于配置文件的規(guī)則配置底層實現(xiàn)是通過Filter。
bucket4j:
cache-to-use: redis-jedis
filter-config-caching-enabled: true
filters:
- cache-name: product_cache_name
id: product_filter
# 配置請求url的規(guī)則;這里底層是通過正則表達式進行匹配的
url: /products/.*
rate-limits:
-
#這里的cache-key非常關(guān)鍵;用于區(qū)分不同請求的情況;
#比如,這里我會根據(jù)不同的請求id來現(xiàn)在訪問速率
#這里可以寫spel表達式,這里調(diào)用的是HttpServletRequest#getParameter方法
cache-key: getParameter("id")
bandwidths:
#配置桶的容量
- capacity: 2
# 時間
time: 30
# 單位
unit: seconds
# 填充速度;這會每隔30秒進行填充
refill-speed: interval
如何配置后,訪問/products/xxx接口,結(jié)果如下:
圖片
圖片
分別請求id為1,2時,如果超過了配置桶的容量,則默認顯示上面的錯誤消息,同時Redis中會分別記錄這2個key
圖片
只有等到30s后才能繼續(xù)訪問。
修改默認的限流提示
bucket4j:
filters:
- cache-name: product_cache_name
http-content-type: 'application/json;charset=utf-8'
http-response-body: '{"code": -1, "message": "請求太快了"}'
注意:你必須同時要設(shè)置content-type設(shè)置字符編碼,否則會亂碼。
條件放行
你也可以通過如下屬性進行有條件的放行;
bucket4j:
filters:
- cache-name: product_cache_name
rate-limits:
-
skip-condition: 'getParameter("id").equals("6")'
當請求id的值為6時則跳過規(guī)則,直接方向。
以上是基于配置文件規(guī)則的應(yīng)用,它還有很多其它的配置屬性,詳細查看官方文檔
https://github.com/MarcGiffing/bucket4j-spring-boot-starter
接下來介紹基于注解的方式。
2.3 基于注解
通過"@RateLimiting"注解,AOP 可以攔截目標方法。這樣,你就可以全面訪問方法參數(shù),輕松定義速率限制鍵或有條件地跳過速率限制。
配置文件中配置規(guī)則
bucket4j:
methods:
- name: storage_rate #在代碼中會通過該名稱引用
cache-name: storage_cache_name
rate-limit:
bandwidths:
- capacity: 2
time: 30
unit: seconds
refill-speed: interval
接口注解,配置限流
@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))) ;
}
// 回退方法的簽名必須與業(yè)務(wù)方法一致
public R<Storage> getStorageFallback(Integer id, String name) {
return R.failure(String.format("請求id=%d,name=%s被限流", id, name)) ;
}
skipCondition:該屬性定義了如果請求的name的值為admin則跳過,不限流。
圖片
圖片
@RateLimiting注解還可以應(yīng)用到類中,這樣該類中的所有方法都會被限流,如下示例:
@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))
) ;
}
}
上面代碼queryStorageById會被限流,而queryStorages方法被@IgnoreRateLimiting注解標準,所以不會被限流。
關(guān)于Bucket4j就介紹這么多,它還有非常多的其它功能,你可以參考官方文檔進行查看。