加锁与令牌桶算法-限流设计对比

加锁与令牌桶算法-限流设计对比

1. 核心原理对比

令牌桶限流:

  • 系统以恒定速率向桶中放入令牌

  • 每个请求需要获取一个令牌才能执行

  • 当桶满时,新令牌被丢弃

  • 当桶空时,请求必须等待或直接被拒绝

加锁限流:

  • 基于时间窗口的计数器

  • 每个时间窗口(如1秒)内只允许固定数量的请求

  • 使用锁保护计数器

  • 当计数器达到阈值时拒绝请求

2、代码实现对比

令牌桶算法

核心思路是通过带缓冲的channel模拟令牌桶,每个空结构体代表一个可用令牌。

初始化时根据设定的最大令牌数(maxTokens)和补充间隔(refillInterval)自动计算出每次应补充的令牌数量,确保每秒补充的令牌总数精确等于最大容量。

限流器通过阻塞(Wait)和非阻塞(Allow)两种获取令牌方式,并通过独立的后台协程定时补充令牌,补充时会动态计算可用空间防止溢出。


// SmoothLimiter 实现平滑的限流控制
type SmoothLimiter struct {
	mu              sync.Mutex    // 互斥锁,保护对共享资源的并发访问
	tokenChan       chan struct{} // 带缓冲的通道,用于存储令牌(空结构体节省内存)
	refillInterval  time.Duration // 令牌补充的时间间隔(如10ms)
	tokensPerRefill int           // 每次补充的令牌数量(根据QPS计算得出)
	isActive        bool          // 控制限流器是否运行的标志位
}

// NewSmoothLimiter 创建新的限流器实例
// maxTokens: 令牌桶最大容量(最大突发请求量)
// refillInterval: 令牌补充间隔,决定限流精度(间隔越小越平滑)
func NewSmoothLimiter(maxTokens int, refillInterval time.Duration) *SmoothLimiter {
	// 初始化限流器结构体
	limiter := &SmoothLimiter{
		mu:              sync.Mutex{},                     // 初始化互斥锁
		tokenChan:       make(chan struct{}, maxTokens),   // 创建带缓冲的channel,容量为maxTokens
		refillInterval:  refillInterval,                   // 设置令牌补充间隔
		tokensPerRefill: calculateRefillTokens(maxTokens, refillInterval), // 计算每次补充量
		isActive:        true,                             // 默认激活状态
	}

	// 启动后台goroutine定期补充令牌
	go limiter.startRefillRoutine()
	return limiter
}

// calculateRefillTokens 计算每次补充的令牌数量
// maxTokens: 桶容量
// interval: 补充间隔
// 返回值: 每次应该补充的令牌数(向上取整)
func calculateRefillTokens(maxTokens int, interval time.Duration) int {
	// 计算公式:(maxTokens * interval) / 1秒
	// 例如100容量,100ms间隔 => 100*0.1/1 = 10个/次
	return int(math.Ceil(float64(maxTokens) * float64(interval) / float64(time.Second)))
}

// Allow 尝试获取令牌(非阻塞方式)
// 返回值: true表示获取成功,false表示令牌不足
func (sl *SmoothLimiter) Allow() bool {
	select {
	case <-sl.tokenChan:  // 尝试从channel读取令牌
		return true       // 成功获取
	default:             // channel为空时执行
		return false     // 获取失败
	}
}

// Wait 阻塞等待直到获取令牌
// 无返回值,调用会阻塞直到有可用令牌
func (sl *SmoothLimiter) Wait() {
	<-sl.tokenChan  // 从channel读取,无令牌时会阻塞
}

// Stop 停止限流器并释放资源
// 安全关闭channel,停止后台goroutine
func (sl *SmoothLimiter) Stop() {
	sl.mu.Lock()         // 获取锁
	defer sl.mu.Unlock() // 确保锁释放
	
	sl.isActive = false  // 设置停止标志
	close(sl.tokenChan)  // 关闭channel(会使得所有等待的Wait()立即返回)
}

// startRefillRoutine 启动后台令牌补充协程
// 内部方法,由NewSmoothLimiter自动调用
func (sl *SmoothLimiter) startRefillRoutine() {
	// 初始填充一次令牌
	sl.refillTokens()

	// 创建定时器,按照设定间隔触发
	ticker := time.NewTicker(sl.refillInterval)
	defer ticker.Stop() // 确保协程退出时停止ticker

	// 主循环
	for {
		select {
		case <-ticker.C:  // 定时触发
			sl.mu.Lock()  // 获取锁检查状态
			if !sl.isActive {
				sl.mu.Unlock() // 释放锁
				return        // 退出协程
			}
			sl.mu.Unlock() // 释放锁
			
			// 执行令牌补充
			sl.refillTokens()
		}
	}
}

// refillTokens 执行实际的令牌补充操作
// 内部方法,持有锁的情况下调用
func (sl *SmoothLimiter) refillTokens() {
	sl.mu.Lock()         // 获取锁
	defer sl.mu.Unlock() // 确保锁释放

	// 计算当前可用的令牌槽位
	availableSpace := cap(sl.tokenChan) - len(sl.tokenChan)
	if availableSpace <= 0 {
		return  // 桶已满,无需补充
	}

	// 确定实际补充数量(不能超过可用空间)
	refillCount := sl.tokensPerRefill
	if refillCount > availableSpace {
		refillCount = availableSpace
	}

	// 批量填充令牌
	for i := 0; i < refillCount; i++ {
		select {
		case sl.tokenChan <- struct{}{}:  // 非阻塞写入令牌
		default:                         // 意外情况处理
			return                      // 通常不会执行到这里
		}
	}
}

加锁限流算法

核心思路是将时间划分为固定长度的窗口(如1秒),通过互斥锁保护每个窗口期内的请求计数器。

每当新的请求到达时,首先检查当前时间是否超过窗口结束时间:若已超期则重置计数器和时间窗口,若在窗口期内则检查请求数是否已达上限——未超限时计数器递增并放行请求,已超限时根据调用方式选择立即拒绝(Allow)或按剩余时间比例休眠等待(Wait)。

这种实现严格保证任何时间窗口内的请求量都不超过设定阈值,适合需要硬性QPS限制的场景,虽然窗口切换时可能产生轻微的突发流量,但通过调整窗口大小(如改用100ms窗口)可以实现更平滑的控制。

// LockLimiter 基于互斥锁的限流器实现
type LockLimiter struct {
	mu         sync.Mutex    // 保护所有共享变量的互斥锁
	count      int           // 当前窗口内的请求计数
	windowSize time.Duration // 时间窗口大小(默认1秒)
	windowEnd  time.Time     // 当前窗口结束时间
	maxRequests int         // 窗口内最大允许请求数
}

// NewLockLimiter 创建锁式限流器
// maxRequests: 每秒最大请求数
// windowSize: 时间窗口大小(通常1秒)
func NewLockLimiter(maxRequests int, windowSize time.Duration) *LockLimiter {
	return &LockLimiter{
		windowSize:  windowSize,
		maxRequests: maxRequests,
		windowEnd:   time.Now().Add(windowSize), // 初始化窗口结束时间
	}
}

// Allow 非阻塞式请求检查
func (ll *LockLimiter) Allow() bool {
	ll.mu.Lock()
	defer ll.mu.Unlock()

	now := time.Now()
	
	// 如果当前时间超过窗口结束时间,重置计数器和窗口
	if now.After(ll.windowEnd) {
		ll.count = 0
		ll.windowEnd = now.Add(ll.windowSize)
	}

	// 检查是否超过限制
	if ll.count >= ll.maxRequests {
		return false
	}

	ll.count++
	return true
}

// Wait 阻塞式请求等待
func (ll *LockLimiter) Wait() {
	for !ll.Allow() {
		// 计算需要等待的时间
		remaining := time.Until(ll.windowEnd)
		if remaining > 0 {
			time.Sleep(remaining / time.Duration(ll.maxRequests-ll.count+1))
		}
	}
}

你可能感兴趣的:(Linux应用编程,网络,服务器,数据库)