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个格子的总量。解决了计数器算法的边界毛刺问题,精度由格子数量决定。

滑动窗口算法原理(1秒窗口,10个格子) 0ms 1000ms 当前滑动窗口(600ms ~ 1600ms) 12 8 15 20 18 25 10 14 22 9 滑动窗口 vs 固定窗口 • 固定窗口:每1秒重置计数器,边界处可能突发2倍流量 • 滑动窗口:统计最近1秒内所有格子的总和,平滑无毛刺 • 格子越多精度越高,但内存开销也线性增长

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)。

令牌桶算法工作流程 令牌桶(容量=10) 令牌生成器 速率:10 令牌/秒 API 请求 请求通过 请求拒绝 令牌桶允许突发:桶中最多可累积10个令牌,突发时一次消耗多个

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 金融场景实战要点

  1. 阈值设定:基于压测结果设定 QPS/TPS 阈值,预留 30%~50% 的缓冲水位。
  2. 分布式限流:使用 Redis + Lua 脚本实现原子性令牌桶或滑动窗口,避免单机限流在负载均衡下失效。
  3. 监控告警:对限流触发次数、熔断器状态、并发连接数设置实时监控,阈值到达 80% 时发出预警。
  4. 优雅拒绝:被限流的请求应返回标准 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