2. 核心概念:QPS、TPS、并发连接数、令牌桶、漏桶、滑动窗口、计数器算法
在金融API的限流与熔断设计中,理解核心度量指标与算法原理是构建稳定系统的基石。本章将逐一拆解这些概念,帮助你在架构决策时做到心中有数。
2.1 核心度量指标
三个指标共同刻画了API的负载特征与容量边界,任何限流策略都必须基于这些量化数据来设定阈值。
| 指标 | 全称 | 定义 | 金融场景示例 |
|---|---|---|---|
| QPS | Queries Per Second | 每秒查询数,侧重读请求 | 账户余额查询接口,单机限流 2000 QPS |
| TPS | Transactions Per Second | 每秒事务数,侧重写/状态变更 | 转账提交接口,单机限流 500 TPS |
| 并发连接数 | Concurrent Connections | 同一时刻正在处理的连接数 | WebSocket 行情推送,最大并发 10000 |
架构要点: 金融核心链路中,TPS 通常比 QPS 更关键,因为事务涉及数据库写锁、分布式事务协调等重资源。并发连接数则直接影响线程池与连接池的耗尽风险。
2.2 经典限流算法
以下五种算法是业界实现限流组件的核心基础,从简单计数器到平滑滑动窗口,各有适用场景。
2.2.1 计数器算法
最简单的实现:将时间划分为固定窗口(如1秒),每个窗口内维护一个计数器,达到阈值则拒绝请求。缺点是在窗口边界处可能出现突发流量(两倍阈值)。
# 伪代码:固定窗口计数器
counter = 0
window_start = now()
threshold = 1000
def allow_request():
if now() - window_start >= 1s:
counter = 0
window_start = now()
if counter >= threshold:
return False
counter += 1
return True
2.2.2 滑动窗口算法
将固定窗口细分为多个小格子(如1秒分为10个100ms格子),滑动统计最近N个格子的总量。解决了计数器算法的边界毛刺问题,精度由格子数量决定。
2.2.3 漏桶算法
将请求视为水滴,漏桶以固定速率漏水(处理请求),桶满则溢出(拒绝)。强制输出速率恒定,适合保护下游系统不被突发流量冲垮。
# 伪代码:漏桶算法
capacity = 1000 # 桶容量
leak_rate = 100 # 每秒漏水数
water = 0
last_leak = now()
def allow_request():
# 先漏水
elapsed = now() - last_leak
water = max(0, water - leak_rate * elapsed)
last_leak = now()
if water >= capacity:
return False # 桶满,溢出
water += 1
return True
2.2.4 令牌桶算法
以固定速率向桶中放入令牌,请求到来时需消耗一个令牌。允许一定程度的突发(桶中可累积令牌),是业界最广泛使用的限流算法(如 Guava RateLimiter)。
2.2.5 算法对比
| 算法 | 输出速率 | 突发能力 | 精度 | 内存开销 | 金融适用场景 |
|---|---|---|---|---|---|
| 计数器 | 不恒定 | 有(边界毛刺) | 低 | 极低 | 非关键路径的简单限流 |
| 滑动窗口 | 平滑 | 受控 | 高(可配置) | 中(格子数相关) | 核心交易接口限流 |
| 漏桶 | 严格恒定 | 无 | 高 | 低 | 保护数据库连接池 |
| 令牌桶 | 平均恒定 | 有(可配置) | 高 | 低 | 最通用,适合大部分API |
选型建议: 金融API限流首选令牌桶算法(如 Redis-Cell、Guava RateLimiter),兼顾平均速率控制与突发容忍。对于需要严格保护下游数据库的场景,可在令牌桶之后串联漏桶做二次整形。
2.3 并发连接数控制
与QPS/TPS不同,并发连接数控制的是同时处理中的请求数量,而非速率。在金融系统中,过高的并发连接会导致线程池耗尽、数据库连接池枯竭、内存溢出。
- 信号量(Semaphore):Java 中
java.util.concurrent.Semaphore,Go 中可用带缓冲 channel 模拟。 - 连接池限制:数据库连接池(HikariCP)、HTTP 连接池(Apache HttpClient)均需设置最大连接数。
- Tomcat/Netty 线程池:设置
server.tomcat.max-threads或boss/worker 线程数。
// Go 示例:使用 buffered channel 控制并发数
var sem = make(chan struct{}, 100) // 最大并发 100
func handleRequest(w http.ResponseWriter, r *http.Request) {
select {
case sem <- struct{}{}:
defer func() { <-sem }()
// 处理业务逻辑
default:
http.Error(w, "too many concurrent requests", 429)
}
}
2.4 限流与熔断的关系
限流和熔断是金融系统稳定性的两道防线,但目标不同:
- 限流:保护自身系统不被过多请求压垮,是主动防御。
- 熔断:保护调用链路,当下游故障时快速失败,避免雪崩,是被动防御。
常见误区: 不要用限流替代熔断。当第三方支付网关响应缓慢时,限流只能减少新请求进入,但已阻塞的线程仍会堆积。此时必须使用熔断器(如 Hystrix、Resilience4j)快速断开调用,释放资源。
2.5 金融场景实战要点
- 阈值设定:基于压测结果设定 QPS/TPS 阈值,预留 30%~50% 的缓冲水位。
- 分布式限流:使用 Redis + Lua 脚本实现原子性令牌桶或滑动窗口,避免单机限流在负载均衡下失效。
- 监控告警:对限流触发次数、熔断器状态、并发连接数设置实时监控,阈值到达 80% 时发出预警。
- 优雅拒绝:被限流的请求应返回标准 HTTP 429(Too Many Requests),并在响应头中加入
Retry-After字段。
# Redis + Lua 实现令牌桶(核心逻辑)
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4]) -- 本次请求消耗令牌数
local bucket = redis.call('hmget', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now
-- 计算时间间隔内生成的令牌
local delta = math.max(0, now - last_refill)
tokens = math.min(capacity, tokens + delta * rate)
if tokens >= requested then
tokens = tokens - requested
redis.call('hmset', key, 'tokens', tokens, 'last_refill', now)
return 1 -- 允许
else
return 0 -- 拒绝
end