限流实现方案

限流

分布式系统中,由于接口API无法控制上游调用方的行为,因此当瞬时请求量突增时,会导致服务器占用过多资源,发生响应速度降低、超时、乃至宕机,甚至引发雪崩造成整个系统不可用。
限流,Rate Limiting,就是对API的请求量进行限制,对于超出限制部分的请求作出快速拒绝、快速失败、丢弃处理,以保证本服务以及下游资源系统的稳定。

哪些原因会带来瞬时请求量突增?
1,热点业务、突发热点数据带来的激增。例如微博热搜的爆点。
2,上游系统的bug导致。
3,恶意的攻击流量。
实现限流的方法很多,一句话来讲,就是限制每秒钟内API可处理的请求量。常见的有以下几种算法:固定窗口计数法、滑动窗口计数法、漏桶算法、令牌桶算法。

固定窗口

思路

  1. 将时间划分为固定的窗口,例如2s
  2. 当请求进来,在此窗口内的计数器加1
  3. 在此窗口内的计数器达到了最大请求值,直接抛弃
  4. 窗口过期,计数器清零

伪代码(貌似不对哦)

local max = 1000
local duration = 2
local now = current.time()	//以时间戳来当key

// 清除前一个窗口计数器
redis.call("del", now-duration)

// 获得当前窗口计数器
count = redis.call("get", now)

if count >= max 
	return false
else
    redis.call("incr", now, 1)
    return true

滑动窗口

滑动窗口和固定窗口的区别是,如果以2秒为例,

思路

  1. 定义窗口大小,以2秒为窗口

  2. 每进来一个请求都插入到redis的有序数列中

  3. 删除 [当前时间-窗口,当前时间] 这个区间外的元素

  4. 以当前时间为最后的最小区间(认为是最后一秒)

  5. 统计**[当前时间-窗口,当前时间]** 这个区间内的元素个数,大于就抛弃

伪代码

package ratelimiter

import (
	"sync"
	"time"
)

var limit = NewSlidingWindow()

type SlidingWindow struct {
	maxRequest   int         // 窗口下的最大请求数
	window       int         // 窗口大小
	timeSlice    int         // 时间间隔
	LastTimeSeek int         // 最后一次更新的时间余数
	countData    map[int]int // 每个时间间隔的统计数量
	mux          sync.Mutex  // 线程安全锁
}

// SlidingWindow 滑动窗口
func NewSlidingWindow() *SlidingWindow {
	return &SlidingWindow{
		window:       60,
		timeSlice:    1,
		LastTimeSeek: 0,
		countData:    make(map[int]int),
	}
}

// Entry 入口
func (s *SlidingWindow) Entry() {
	if s.count() {
		// todo::success
	} else {
		// todo::reject request
	}
}

func (s *SlidingWindow) count() bool {
	// 统计需要加锁,并发做到线程安全
	s.mux.Lock()
	defer s.mux.Unlock()

	//当前时间戳
	now := time.Now().Unix()

	// 获取余数,当前请求保存到哪个slice节点
	seek := int(now) % s.window

	// 判断跟上次余数是否一样,如果不一样说明要把上一轮的统计清除
	if seek != s.LastTimeSeek {
		s.countData[seek] = 1
	} else {
		s.countData[seek]++
	}

	var res int
	for _, v := range s.countData {
		res = res + v
	}

	if res > s.maxRequest {
		return false
	}
	return true
}

漏桶算法

漏斗桶(Leaky Bucket)算法是一种用于流量控制的算法,它确保流入系统的请求以固定的速率被处理,不会超过系统的处理能力

我们使用Redis来存储漏斗桶的参数,

  • 包括容量(capacity)
  • 漏水速率(rate)
  • 上次漏水时间(last_leak_time)
  • 当前的水位(water_level): 当前请求量

在每次请求到达时,我们根据时间的流逝来计算漏水的数量,然后更新漏斗的状态。如果漏斗已满,请求就会被限制。

伪代码

// 初始化漏斗桶
function initLeakyBucket($bucketKey, $capacity, $rate) {
    redisSet($bucketKey . ":capacity", $capacity);
    redisSet($bucketKey . ":rate", $rate);
    redisSet($bucketKey . ":last_leak_time", time());
    redisSet($bucketKey . ":water_level", 0);
}

// 处理请求
function processRequest($bucketKey) {
    // 获取漏斗桶参数
    $capacity = redisGet($bucketKey . ":capacity");
    $rate = redisGet($bucketKey . ":rate");
    $lastLeakTime = redisGet($bucketKey . ":last_leak_time");
    $waterLevel = redisGet($bucketKey . ":water_level");
    
    // 获取当前时间戳
    $currentTimestamp = time();
    
    // 计算经过的时间
    $timePassed = $currentTimestamp - $lastLeakTime;
    
    // 计算漏水的数量 , 时间 * 流出速率 = 最后一次请求 到 当前请求的固定流出量
    $leakAmount = $timePassed * $rate;
    
    // 更新漏斗的状态  桶内当前水位 - 最后一次请求 到 当前请求的固定流出量 + 1(本次请求进来,肯定是1) = 本次请求进来后的水位
    $newWaterLevel = max(0, $waterLevel - $leakAmount) + 1;
    
    // 如果漏斗已满,则限制请求  水位不大于容量就可以访问,否则反之
    if ($newWaterLevel > $capacity) {
        return false;
    }
    
    // 更新漏斗状态
    redisSet($bucketKey . ":last_leak_time", $currentTimestamp);
    redisSet($bucketKey . ":water_level", $newWaterLevel);
    
    return true;
}

令牌桶算法

在令牌桶算法中,不需要使用过期时间戳来管理令牌,因为令牌桶的本质是通过固定速率向桶中添加令牌,而不是根据令牌的到达时间来处理。我之前的回答中的示例代码可能引入了不必要的复杂性。

我们使用Redis来存储漏斗桶的参数,

  • 包括容量(capacity)
  • 令牌生产速率(rate)
  • 上次发放令牌的时间(last_leak_time)

在每次请求到达时,我们根据时间的流逝来计算漏水的数量,然后更新漏斗的状态。如果漏斗已满,请求就会被限制。

代码

// 初始化令牌桶
function initTokenBucket($bucketKey, $capacity, $rate) {
    redisSet($bucketKey . ":capacity", $capacity);
    redisSet($bucketKey . ":rate", $rate);
    redisSet($bucketKey . ":tokens", $capacity);
    redisSet($bucketKey . ":tokens", 0);
}

// 处理请求
function processRequest($bucketKey) {
    // 获取令牌桶参数
    $capacity = redisGet($bucketKey . ":capacity");
    $rate = redisGet($bucketKey . ":rate");
    $tokens = redisGet($bucketKey . ":tokens");
    $last_update = redisGet($bucketKey . ":last_update");
    
    // 获取当前时间戳
    $currentTimestamp = time();
    
    // 计算经过的时间
    $timePassed = $currentTimestamp - redisGet($bucketKey . ":last_update");
    
    // 计算新的令牌数量,但不超过容量。 原桶内token数量 + 水流量 = 此时应该有的数量,如果超出了桶最大容纳量,则取桶最大值
    $newTokens = min($capacity, $tokens + $timePassed * $rate);
    
    // 更新令牌数量
    redisSet($bucketKey . ":tokens", $newTokens);
    redisSet($bucketKey . ":last_update", $currentTimestamp);
    
    if ($newTokens >= 1) {
        // 如果有足够的令牌,则允许请求
        redisSet($bucketKey . ":tokens", $newTokens - 1);
        return true;
    } else {
        // 令牌桶为空,请求被限制
        return false;
    }
}

包推荐

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值