当你的API发出"SOS"信号:认识速率限制与节流
\n\n想象一下:你编写了一个完美的Kotlin应用程序。它快速、优雅,使用了协程和Flow。你将其部署到生产环境,然后——砰!——一个"好心"的用户(或者更糟,一个恶意攻击者)开始每秒向你的端点发送数千个请求。服务器崩溃,数据库哭泣,其他用户只能收到503 Service Unavailable。
熟悉这种痛苦吗?正是为了应对这种情况,才有了速率限制和节流机制。在本文中,我们不仅会讨论理论,还会通过实际示例,讲解如何在Kotlin中实现这些模式。
\n\n我们将编写真正能保护你后端的代码,无论是基于Ktor、Spring Boot的应用,还是纯协程项目。开始吧!
\n\n速率限制 vs 节流:有什么区别(为什么这很重要)?
\n\n我们从基础开始,以免混淆术语。虽然这两个概念经常被当作同义词使用,但它们之间是有细微差别的。
\n\n- \n
- 速率限制——是一种硬性限制。"你每分钟只能发送100个请求。第101个请求之后,你将收到
429 Too Many Requests,并被限制到下一分钟"。这是关于限制。 \n - 节流——是一种更温和的调节。"你不能以超过每200毫秒一次的频率发送请求。如果发送太快,我们会将其放入队列或直接忽略"。这是关于速度。 \n
在Kotlin世界中,我们最常在API层面(中间件)实现速率限制,在具体操作层面(例如,避免向外部服务器发送过多请求)实现节流。
\n\n每个Kotlin开发者都应了解的算法
\n\n在编写代码之前,让我们先选择工具。有三种经典算法非常适合Kotlin架构。
\n\n1. 令牌桶——最流行的算法
\n想象一个桶,每秒有N个新令牌滴入其中。桶有一个最大容量(突发容量)。要处理一个请求,需要取出一个令牌。如果没有令牌,请求将被拒绝。
\n优点:允许小的突发负载,实现简单。
\n\n2. 漏桶——平滑峰值
\n一个有漏洞的桶。请求进入桶中,而"漏洞"以恒定速率释放它们。如果桶满了,请求将被丢弃。
\n优点:完美地均衡负载,不会产生峰值。
\n\n3. 滑动窗口——无抖动的精确性
\n与固定的时间边界(如"1分钟")不同,我们观察一个滑动的时间段。实现起来更复杂,但能更准确地反映实际负载。
\n\n对于Kotlin项目,令牌桶是简单性和效率之间的最佳平衡点。我们将实现它。
\n\n编写代码:从零开始用Kotlin实现速率限制器(无需库)
\n\n让我们创建一个简单但可用的速率限制器,使用协程和Mutex。这将是一个线程安全的令牌桶版本。
import kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlin.time.Duration\nimport kotlin.time.TimeSource\n\nclass TokenBucketRateLimiter(\n private val maxTokens: Int,\n private val refillRate: Long, // 每秒令牌数\n private val timeSource: TimeSource = TimeSource.Monotonic\n) {\n private var tokens: Long = maxTokens.toLong()\n private var lastRefillTimestamp: Long = timeSource.markNow().elapsedNow().inWholeMilliseconds\n private val mutex = Mutex()\n\n suspend fun tryAcquire(): Boolean {\n mutex.withLock {\n refillTokens()\n return if (tokens > 0) {\n tokens--\n true\n } else {\n