大厂面试官问我:你知道如何利用Redis实现限流么?

大厂常见面试题 Redis限流

简单限流

限流的目的:除了控制流量,限流还有一个应用目的是用于控制用户行为,避免垃圾请求。
如何实现限流?
image.png

它的整体思路就是:每一个行为到来时,都维护一次时间窗口。将时间窗口外的记录全部清理掉,只保留窗口内的记录。
代码:

# coding: utf8
import time
import redis
client = redis.StrictRedis()
def is_action_allowed(user_id, action_key, period, max_count):
    key = 'hist:%s:%s' % (user_id, action_key)
now_ts = int(time.time() * 1000) # 毫秒时间戳
with client.pipeline() as pipe: # client 是 StrictRedis 实例
# 记录行为
    pipe.zadd(key, now_ts, now_ts) # value 和 score 都使用毫秒时间戳
# 移除时间窗口之前的行为记录,剩下的都是时间窗口内的
	pipe.zremrangebyscore(key, 0, now_ts - period * 1000)
# 获取窗口内的行为数量
	pipe.zcard(key)
# 设置 zset 过期时间,避免冷用户持续占用内存
# 过期时间应该等于时间窗口的长度,再多宽限 1s
	pipe.expire(key, period + 1)
# 批量执行
	_, _, current_count, _ = pipe.execute()
# 比较数量是否超标
return current_count <= max_count
for i in range(20):
	print is_action_allowed("laoqian", "reply", 60, 5)

java版本

public class SimpleRateLimiter {
    private Jedis jedis;
    public SimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }
    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = String.format("hist:%s:%s", userId, actionKey);
        long nowTs = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        pipe.multi();
        pipe.zadd(key, nowTs, "" + nowTs);
        pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
        Response<Long> count = pipe.zcard(key);
        pipe.expire(key, period + 1);
        pipe.exec();
        pipe.close();
        return count.get() <= maxCount;
    }
    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
        for(int i=0;i<20;i++) {
            System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
        }
    }
}

有何缺点?
因为它要记录时间窗口内所有的行为记录,如果这个量很大,比如限定 60s 内操作不得超过 100w 次这样的参数,它是不适合做这样的限流的,因为会消耗大量的存储空间。

漏斗限流

漏斗思想:
漏洞的容量是有限的,如果将漏嘴堵住,然后一直往里面灌水,它就会变满,直至再也装不进去。如果将漏嘴放开,水就会往下流,流走一部分之后,就又可以继续往里面灌水。如果漏嘴流水的速率大于灌水的速率,那么漏斗永远都装不满。如果漏嘴流水速率小于灌水的速率,那么一旦漏斗满了,灌水就需要暂停并等待漏斗腾空。
image.png
所以,漏斗的剩余空间就代表着当前行为可以持续进行的数量,漏嘴的流水速率代表着系统允许该行为的最大频率。

漏斗限流算法

# coding: utf8
import time
class Funnel(object):
    def __init__(self, capacity, leaking_rate):
        self.capacity = capacity # 漏斗容量
	self.leaking_rate = leaking_rate # 漏嘴流水速率
	self.left_quota = capacity # 漏斗剩余空间
	self.leaking_ts = time.time() # 上一次漏水时间
def make_space(self):
    now_ts = time.time()
	delta_ts = now_ts - self.leaking_ts # 距离上一次漏水过去了多久
	delta_quota = delta_ts * self.leaking_rate # 又可以腾出不少空间了
	if delta_quota < 1: # 腾的空间太少,那就等下次吧
  	  return
	self.left_quota += delta_quota # 增加剩余空间
	self.leaking_ts = now_ts # 记录漏水时间
	if self.left_quota > self.capacity: # 剩余空间不得高于容量
   	 self.left_quota = self.capacity
def watering(self, quota):
    self.make_space()
	if self.left_quota >= quota: # 判断剩余空间是否足够
   		self.left_quota -= quota
		return True
	return False
funnels = {} # 所有的漏斗
# capacity 漏斗容量
# leaking_rate 漏嘴流水速率 quota/s
def is_action_allowed(
   	 user_id, action_key, capacity, leaking_rate):
    key = '%s:%s' % (user_id, action_key)
	funnel = funnels.get(key)
	if not funnel:
 	 	funnel = Funnel(capacity, leaking_rate)
		funnels[key] = funnel
	return funnel.watering(1)
for i in range(20):
   	print is_action_allowed('laoqian', 'reply', 15, 0.5)

public class FunnelRateLimiter {
    static class Funnel {
        // 容量
        int capacity;
        // 漏嘴流水速率
        float leakingRate;
        // 漏斗剩余空间
        int leftQuota;
        // 上一次漏水时间
        long leakingTs;
        public Funnel(int capacity, float leakingRate) {
            this.capacity = capacity;
            this.leakingRate = leakingRate;
            this.leftQuota = capacity;
            this.leakingTs = System.currentTimeMillis();
        }
        void makeSpace() {
            long nowTs = System.currentTimeMillis();
            long deltaTs = nowTs - leakingTs;
            int deltaQuota = (int) (deltaTs * leakingRate);
            if (deltaQuota < 0) { // 间隔时间太长,整数数字过大溢出
                this.leftQuota = capacity;
                this.leakingTs = nowTs;
                return;
            }
            if (deltaQuota < 1) { // 腾出空间太小,最小单位是 1
                return;
            }
            this.leftQuota += deltaQuota;
            this.leakingTs = nowTs;
            if (this.leftQuota > this.capacity) {
                this.leftQuota = this.capacity;
            }
        }
        boolean watering(int quota) {
            makeSpace();
            if (this.leftQuota >= quota) {
                this.leftQuota -= quota;
                return true;
            }
            return false;
        }
    }
    private Map<String, Funnel> funnels = new HashMap<>();
    public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate) {
        String key = String.format("%s:%s", userId, actionKey);
        Funnel funnel = funnels.get(key);
        if (funnel == null) {
            funnel = new Funnel(capacity, leakingRate);
            funnels.put(key, funnel);
        }
        return funnel.watering(1); // 需要 1 个 quota
    }
}

以上是代码实现漏斗限流的思想,但是在分布式环境中,用Redis的会更多。

Redis-Cell

Redis 4.0 提供了一个限流 Redis 模块,它叫 redis-cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有了这个模块,限流问题就非常简单了

该模块只有 1 条指令 cl.throttle,它的参数和返回值都略显复杂,接下来让我们来看看这个指令具体该如何使用。
image.png

  • laoqian 就是 key值
  • 15 是令牌的初始容量,初始容量是该数加一,也就是16,初始化时,令牌是满的
  • 30 60 就是速率,意思是60秒内可以请求30次
  • 1 表示执行命令时取走的令牌数量,默认是 1
127.0.0.1:6379> cl.throttle user123 15 30 60 1
1) (integer) 0  #0表示允许,1表示拒绝
2) (integer) 16 #漏斗的容量是 16
3) (integer) 15 #漏斗中还剩余的数量,本次执行取走了一个,所以还有15个
4) (integer) -1 #如果拒绝了需要等待的时间,单位是秒
5) (integer) 2  #表示漏斗装满需要等待的时间
127.0.0.1:6379> cl.throttle user123 15 30 60 4
1) (integer) 0
2) (integer) 16
3) (integer) 12
4) (integer) -1
5) (integer) 8
127.0.0.1:6379> cl.throttle user123 15 30 60 4
1) (integer) 0
2) (integer) 16
3) (integer) 8
4) (integer) -1
5) (integer) 14
127.0.0.1:6379> cl.throttle user123 15 30 60 4
1) (integer) 0
2) (integer) 16
3) (integer) 5
4) (integer) -1
5) (integer) 20
127.0.0.1:6379> cl.throttle user123 15 30 60 4
1) (integer) 0
2) (integer) 16
3) (integer) 2
4) (integer) -1
5) (integer) 27
127.0.0.1:6379> cl.throttle user123 15 30 60 4
1) (integer) 1
2) (integer) 16
3) (integer) 2
4) (integer) 2
5) (integer) 26
127.0.0.1:6379> cl.throttle user123 15 30 60 4
1) (integer) 0
2) (integer) 16
3) (integer) 0
4) (integer) -1
5) (integer) 31
127.0.0.1:6379> cl.throttle user123 15 30 60 17
1) (integer) 1
2) (integer) 16
3) (integer) 1
4) (integer) -1
5) (integer) 28
127.0.0.1:6379> cl.throttle user123 15 30 60 17
1) (integer) 1
2) (integer) 16
3) (integer) 16
4) (integer) -1
5) (integer) 0
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

暮起

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值