文章目录
- 固定窗口
- 优缺点:
- 适用场景:
- 总结:
- 滑动窗口
- 优缺点:
- 适用场景:
- 总结:
- 漏桶限流器
- 优缺点:
- 适用场景:
- 总结:
- 令牌桶
- 优缺点:
- 适用场景:
- 总结:
- 总结
今天总结一下四种限流算法,通过图形和代码的方式去一点点深入的探讨这些算法的一些本质,并且讲解以下优缺点。方便在以后的学习或者工作中,可以很好的去运用这些知识点。然后挑选最利于场景的一些算法。
固定窗口
优缺点:
优点:
- 简单易实现:固定窗口算法的逻辑简单,容易理解和实现。
- 公平性:每个请求都有机会在窗口开始时被处理,避免了某些请求长时间等待的问题。
缺点:
- 临界点问题:在窗口切换的瞬间,可能会有大量请求同时到达,导致短时间内的请求量超过限制,这可能会对系统造成压力。
- 不够灵活:窗口大小固定,无法根据实际流量动态调整。
适用场景:
- 适用于请求频率相对稳定的场景:如果系统能够承受短时间内的高并发请求,固定窗口算法是一个不错的选择。
- 适用于需要简单限流策略的场景:对于那些不需要复杂限流逻辑的系统,固定窗口算法提供了一个简单而有效的解决方案。
总结:
固定窗口限流算法是一种基础的限流策略,适用于需要简单限流控制的场景。然而,它可能不适合那些需要更平滑流量控制或需要根据实际流量动态调整限流策略的系统。在实际应用中,可能需要结合其他限流算法(如滑动窗口或令牌桶算法)来满足更复杂的业务需求。
package limiter
import (
"sync"
"time"
)
// FixedWindowLimiter 结构代表一个固定窗口限流器。
// 它使用固定大小的时间窗口来限制请求的数量。
type FixedWindowLimiter struct {
limit int // 请求上限,窗口内允许的最大请求数
window time.Duration // 窗口时间大小,即时间窗口的长度
counter int // 计数器,记录当前窗口内的请求数
lastTime time.Time // 上一次请求的时间
mutex sync.Mutex // 互斥锁,用于同步,避免并发访问导致的问题
}
// NewFixedWindowLimiter 构造函数创建并初始化一个新的 FixedWindowLimiter 实例。
func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowLimiter {
return &FixedWindowLimiter{
limit: limit,
window: window,
lastTime: time.Now(), // 初始化时设置当前时间为窗口开始时间
}
}
// TryAcquire 尝试获取一个请求的机会。
// 如果当前窗口内请求数未达到上限,增加计数器并返回 true。
// 如果请求数已达到上限或窗口已过期,返回 false。
func (l *FixedWindowLimiter) TryAcquire() bool {
l.mutex.Lock()
defer l.mutex.Unlock()
now := time.Now()
// 检查当前时间与上次请求时间差是否超过窗口大小
if now.Sub(l.lastTime) > l.window {
l.counter = 0 // 如果窗口过期,重置计数器
l.lastTime = now // 更新窗口开始时间为当前时间
}
// 如果当前请求数未达到上限,允许请求
if l.counter < l.limit {
l.counter++ // 请求成功,增加计数器
return true // 返回 true 表示请求已成功获取
}
// 如果请求数已达到上限,请求失败
return false
}
滑动窗口
优缺点:
优点:
- 平滑处理请求:通过将大窗口划分为多个小窗口,可以更平滑地处理请求,避免固定窗口算法的临界点问题。
- 灵活性:可以根据需要调整小窗口的大小和数量,以适应不同的流量模式。
- 并发性能:使用读写锁允许多个并发读取操作,提高了并发性能。
缺点:
- 复杂性:相较于固定窗口算法,滑动窗口算法的实现和维护更复杂。
- 内存****使用:需要存储每个小窗口的计数器,可能会占用更多的内存,尤其是在窗口数量很大时。
适用场景:
- 适用于请求流量波动较大:如果系统需要处理不均匀的请求流量,滑动窗口算法可以更好地平滑流量峰值。
- 适用于需要更精细控制:当需要根据实际流量动态调整限流策略时,滑动窗口算法提供了更高的灵活性。
- 不适用于简单场景:对于简单的限流需求,固定窗口算法可能更简单、更易于实现。
总结:
滑动窗口限流算法是一种有效的流量控制机制,特别适合处理请求流量波动较大的场景。然而,它需要更多的计算和内存资源,因此在资源受限的环境中可能不是最佳选择。在实际应用中,需要根据具体的业务需求和系统资源来选择合适的限流算法。
package limiter
import (
"errors"
"sync"
"time"
)
// SlidingWindowLimiter 滑动窗口限流器,用于控制请求的速率。
type SlidingWindowLimiter struct {
limit int // 窗口内允许的最大请求数
window int64 // 窗口时间大小(纳秒)
smallWindow int64 // 小窗口时间大小(纳秒)
smallWindows int64 // 窗口内小窗口的数量
counters map[int64]int // 每个小窗口的请求计数
mutex sync.RWMutex // 使用读写锁提高并发性能
}
// NewSlidingWindowLimiter 创建并初始化滑动窗口限流器。
func NewSlidingWindowLimiter(limit int, window, smallWindow time.Duration) (*SlidingWindowLimiter, error) {
if int64(window%smallWindow) != 0 {
return nil, errors.New("window size must be divisible by the small window size")
}
return &SlidingWindowLimiter{
limit: limit,
window: int64(window),
smallWindow: int64(smallWindow),
smallWindows: int64(window / smallWindow),
counters: make(map[int64]int),
}, nil
}
// TryAcquire 尝试在当前窗口内获取一个请求的机会。
func (l *SlidingWindowLimiter) TryAcquire() bool {
l.mutex.RLock() // 读锁,允许多个并发读取
defer l.mutex.RUnlock()
now := time.Now().UnixNano()
currentSmallWindow := now / l.smallWindow * l.smallWindow // 当前小窗口的起始点
// 清理过期的小窗口计数器
l.cleanExpiredWindows(now)
// 检查并更新当前小窗口的计数
l.mutex.Lock() // 写锁,更新计数器
defer l.mutex.Unlock()
count, exists := l.counters[currentSmallWindow]
if !exists || count < l.limit {
l.counters[currentSmallWindow] = count + 1
return true
}
return false
}
// cleanExpiredWindows 清理已过期的小窗口计数器。
func (l *SlidingWindowLimiter) cleanExpiredWindows(now int64) {
startSmallWindow := now/l.smallWindow*l.smallWindow - l.window
for smallWindow := range l.counters {
if smallWindow < startSmallWindow {
delete(l.counters, smallWindow)
}
}
}
// 注意:cleanExpiredWindows 方法应该在持有读锁的情况下调用,以避免在遍历和修改计数器时产生竞态条件。
漏桶限流器
优缺点:
优点:
- 平滑处理请求:漏桶算法能够平滑处理请求,即使在流量突发的情况下也能保持稳定的处理速率。
- 简单易实现:相对于其他复杂的限流算法,漏桶算法的实现较为简单。
- 并发性能:使用读写锁允许多个并发读取操作,提高了并发性能。
缺点:
- 固定速率:漏桶算法以固定速率处理请求,可能不适合需要动态调整处理速率的场景。
- 可能的延迟:在高流量情况下,请求可能需要等待桶内水位下降才能被处理,这可能导致延迟。
适用场景:
- 适用于请求处理速率相对稳定的场景:例如,定时任务执行、消息队列处理等。
- 适用于需要平滑流量的场景:例如,防止短时间内大量请求对系统造成压力。
- 不适用于需要快速响应的场景:例如,实时性要求高的在线游戏或交易系统。
总结:
漏桶限流算法是一种有效的流量控制机制,特别适合处理请求速率相对稳定的服务。然而,它可能不适合那些需要快速响应或动态调整处理速率的系统。在实际应用中,需要根据具体的业务需求和系统资源来选择合适的限流算法。
package limiter
import (
"errors"
"math"
"sync"
"time"
)
// LeakyBucketLimiter 漏桶限流器
type LeakyBucketLimiter struct {
peakLevel int // 最高水位
currentLevel int // 当前水位
currentVelocity int // 水流速度/秒
lastTime time.Time // 上次放水时间
mutex sync.RWMutex // 使用读写锁提高并发性能
}
// NewLeakyBucketLimiter 初始化漏桶限流器
func NewLeakyBucketLimiter(peakLevel, currentVelocity int) (*LeakyBucketLimiter, error) {
if currentVelocity <= 0 {
return nil, errors.New("currentVelocity must be greater than 0")
}
if peakLevel < currentVelocity {
return nil, errors.New("peakLevel must be greater than or equal to currentVelocity")
}
return &LeakyBucketLimiter{
peakLevel: peakLevel,
currentLevel: 0, // 初始化时水位为0
currentVelocity: currentVelocity,
lastTime: time.Now(),
}, nil
}
// TryAcquire 尝试获取处理请求的权限
func (l *LeakyBucketLimiter) TryAcquire() bool {
l.mutex.RLock() // 读锁,允许多个并发读取
defer l.mutex.RUnlock()
// 如果上次放水时间距今不到1秒,不需要放水
now := time.Now()
interval := now.Sub(l.lastTime)
l.mutex.Lock() // 写锁,更新水位
defer l.mutex.Unlock()
// 计算放水后的水位
if interval >= time.Second {
l.currentLevel = int(math.Max(0, float64(l.currentLevel)-(interval/time.Second).Seconds()*float64(l.currentVelocity)))
l.lastTime = now
}
// 尝试增加水位
if l.currentLevel < l.peakLevel {
l.currentLevel++
return true
}
return false
}
令牌桶
优缺点:
优点:
- 允许突发流量:令牌桶算法允许在桶中积累令牌,从而允许一定程度的突发流量。
- 平滑流量:通过控制令牌的生成速率,可以平滑处理请求。
- 简单易实现:算法实现相对简单,易于理解和维护。
缺点:
- 可能的延迟:在桶中没有足够令牌的情况下,请求需要等待令牌生成,可能导致延迟。
- 固定速率:虽然允许突发流量,但令牌的生成速率是固定的,不够灵活。
适用场景:
- 适用于需要平滑流量的场景:例如,网络流量控制、API 速率限制等。
- 适用于允许一定程度突发流量的场景:例如,促销活动期间的流量突增。
- 不适用于需要严格实时处理的场景:例如,实时交易系统,因为请求可能需要等待令牌生成。
总结:
令牌桶限流算法是一种有效的流量控制机制,特别适合处理需要平滑流量和允许一定程度突发流量的场景。然而,它可能不适合那些需要快速响应或动态调整处理速率的系统。在实际应用中,需要根据具体的业务需求和系统资源来选择合适的限流算法。
package limiter
import (
"sync"
"time"
)
// TokenBucketLimiter 令牌桶限流器
type TokenBucketLimiter struct {
capacity int // 容量
currentTokens int // 令牌数量
rate int // 发放令牌速率/秒
lastTime time.Time // 上次发放令牌时间
mutex sync.Mutex // 避免并发问题
}
// NewTokenBucketLimiter 创建一个新的令牌桶限流器实例。
func NewTokenBucketLimiter(capacity, rate int) *TokenBucketLimiter {
return &TokenBucketLimiter{
capacity: capacity,
rate: rate,
lastTime: time.Now(),
currentTokens: 0, // 初始化时桶中没有令牌
}
}
// TryAcquire 尝试从令牌桶中获取一个令牌。
func (l *TokenBucketLimiter) TryAcquire() bool {
l.mutex.Lock()
defer l.mutex.Unlock()
now := time.Now()
interval := now.Sub(l.lastTime) // 计算时间间隔
// 如果距离上次发放令牌超过1秒,则发放新的令牌
if interval >= time.Second {
// 计算应该发放的令牌数量,但不超过桶的容量
newTokens := int(interval/time.Second) * l.rate
l.currentTokens = minInt(l.capacity, l.currentTokens+newTokens)
// 更新上次发放令牌的时间
l.lastTime = now
}
// 如果桶中没有令牌,则请求失败
if l.currentTokens == 0 {
return false
}
// 桶中有令牌,消费一个令牌
l.currentTokens--
return true
}
// minInt 返回两个整数中的较小值。
func minInt(a, b int) int {
if a < b {
return a
}
return b
}package limiter
import (
"sync"
"time"
)
// TokenBucketLimiter 令牌桶限流器
type TokenBucketLimiter struct {
capacity int // 容量
currentTokens int // 令牌数量
rate int // 发放令牌速率/秒
lastTime time.Time // 上次发放令牌时间
mutex sync.Mutex // 避免并发问题
}
// NewTokenBucketLimiter 创建一个新的令牌桶限流器实例。
func NewTokenBucketLimiter(capacity, rate int) *TokenBucketLimiter {
return &TokenBucketLimiter{
capacity: capacity,
rate: rate,
lastTime: time.Now(),
currentTokens: 0, // 初始化时桶中没有令牌
}
}
// TryAcquire 尝试从令牌桶中获取一个令牌。
func (l *TokenBucketLimiter) TryAcquire() bool {
l.mutex.Lock()
defer l.mutex.Unlock()
now := time.Now()
interval := now.Sub(l.lastTime) // 计算时间间隔
// 如果距离上次发放令牌超过1秒,则发放新的令牌
if interval >= time.Second {
// 计算应该发放的令牌数量,但不超过桶的容量
newTokens := int(interval/time.Second) * l.rate
l.currentTokens = minInt(l.capacity, l.currentTokens+newTokens)
// 更新上次发放令牌的时间
l.lastTime = now
}
// 如果桶中没有令牌,则请求失败
if l.currentTokens == 0 {
return false
}
// 桶中有令牌,消费一个令牌
l.currentTokens--
return true
}
// minInt 返回两个整数中的较小值。
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
总结
上面的就是对于四种限流算法的描述,可以通过其中的代码,还有一些具体的分析,利用流程图和模拟图的整体搭配,保证了学习效率