漏斗限流详述

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 漏斗限流详述

订阅专栏

本文已收录于专栏


❤️《Redis之大厂必备技能包》❤️


上千人点赞收藏的,全套Redis学习资料,大厂必备技能!


目录


1、需求


2、常见的错误设计


3、漏斗限流


3.1 解决方案


3.2 Java代码实现


3.3 结合Redis实现


4、总结


1、需求

限定用户的某个行为在指定时间T内,只允许发生N次。假设T为1秒钟,N为1000次。


2、常见的错误设计

程序员设计了一个在每分钟内只允许访问1000次的限流方案,如下图01:00s-02:00s之间只允许访问1000次,这种设计最大的问题在于,请求可能在01:59s-02:00s之间被请求1000次,02:00s-02:01s之间被请求了1000次,这种情况下01:59s-02:01s间隔0.02s之间被请求2000次,很显然这种设计是错误的。image.png3、漏斗限流

3.1 解决方案

漏斗容量有限,当流水的的速度小于灌水的速度,漏斗就会水满溢出,利用这个原理我们可以设计限流代码!漏斗的剩余的空间就代表着当前行为(请求)可以持续进行的数量,漏斗的流水速率代表系统允许行为(请求)发生的最大频率,通常安装系统的处理能力权衡后进行设值。


image.png

package com.lizba.redis.limit;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * <p>
 *      漏斗限流
 * </p>
 *
 * @Author: Liziba
 */
public class FunnelRateLimiter {
    /** map用于存储多个漏斗 */
    private Map<String, Funnel> funnels = new ConcurrentHashMap<>();
    /**
     * 请求(行为)是否被允许
     *
     * @param userId        用户id
     * @param actionKey     行为key
     * @param capacity      漏斗容量
     * @param leakingRate   剩余容量
     * @param quota         请求次数
     * @return
     */
    public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate, int quota) {
        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.waterLeaking(quota);
    }
    /**
     * 漏斗类
     */
    class Funnel {
        /** 漏斗容量 */
        int capacity;
        /** 漏斗流速,每毫秒允许的流速(请求) */
        float leakingRate;
        /** 漏斗剩余空间 */
        int leftCapacity;
        /** 上次漏水时间 */
        long leakingTs;
        public Funnel(int capacity, float leakingRate) {
            this.capacity = this.leftCapacity = capacity;
            this.leakingRate = leakingRate;
            leakingTs = System.currentTimeMillis();
        }
        /**
         * 计算剩余空间
         */
        void makeSpace() {
            long nowTs = System.currentTimeMillis();
            long intervalTs = nowTs - leakingTs;
            int intervalCapacity = (int) (intervalTs * leakingRate);
            // int 溢出
            if (intervalCapacity < 0) {
                this.leftCapacity = this.capacity;
                this.leakingTs = nowTs;
                return;
            }
            // 腾出空间 >= 1
            if (intervalCapacity < 1) {
                return;
            }
            // 增加漏斗剩余容量
            this.leftCapacity += intervalCapacity;
            this.leakingTs = nowTs;
            // 容量不允许超出漏斗容量
            if (this.leftCapacity > this.capacity) {
                this.leftCapacity = this.capacity;
            }
        }
        /**
         * 漏斗流水
         *
         * @param quota     流水量
         * @return
         */
        boolean waterLeaking(int quota) {
            // 触发漏斗流水
            this.makeSpace();
            if (this.leftCapacity >= quota) {
                leftCapacity -= quota;
                return true;
            }
            return false;
        }
    }
}

测试代码:

计算机运行如下的代码速度会非常的块,我通过TimeUnit.SECONDS.sleep(2);模拟客户端过一段时间后再请求。

设置漏斗容量为10,每毫秒允许0.002次请求(2 次/秒),每次请求数量为1;image.pngimage.png

3.3 结合Redis实现

我们采用hash结构,将Funnel的属性字段,放入hash中,并且在代码中进行运算即可

package com.lizba.redis.limit;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
/**
 * <p>
 *      redis hash 漏斗限流
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/9/7 23:46
 */
public class FunnelRateLimiterByHash {
    private Jedis client;
    public FunnelRateLimiterByHash(Jedis client) {
        this.client = client;
    }
    /**
     * 请求是否成功
     *
     * @param userId
     * @param actionKey
     * @param capacity
     * @param leakingRate
     * @param quota
     * @return
     */
    public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate, int quota) {
        String key = this.key(userId, actionKey);
        long nowTs = System.currentTimeMillis();
        Map<String, String> funnelMap = client.hgetAll(key);
        if (funnelMap == null || funnelMap.isEmpty()) {
            return initFunnel(key, nowTs, capacity, quota);
        }
        long intervalTs = nowTs - Long.parseLong(funnelMap.get("leakingTs"));
        int intervalCapacity = (int) (intervalTs * leakingRate);
        // 时间过长, int可能溢出
        if (intervalCapacity < 0) {
            intervalCapacity = 0;
            initFunnel(key, nowTs, capacity, quota);
        }
        // 腾出空间必须 >= 1
        if (intervalCapacity < 1) {
            intervalCapacity = 0;
        }
        int leftCapacity = Integer.parseInt(funnelMap.get("leftCapacity")) + intervalCapacity;
        if (leftCapacity > capacity) {
            leftCapacity = capacity;
        }
        return initFunnel(key, nowTs, leftCapacity, quota);
    }
    /**
     * 存入redis,初始funnel
     *
     * @param key
     * @param nowTs
     * @param capacity
     * @param quota
     * @return
     */
    private boolean initFunnel(String key,long nowTs, int capacity, int quota) {
        Map<String, String> funnelMap = new HashMap<>();
        funnelMap.put("leftCapacity", String.valueOf((capacity > quota) ? (capacity - quota) : 0));
        funnelMap.put("leakingTs", String.valueOf(nowTs));
        client.hset(key, funnelMap);
        return capacity >= quota;
    }
    /**
     * 限流key
     *
     * @param userId
     * @param actionKey
     * @return
     */
    private String key(String userId, String actionKey) {
        return String.format("limit:%s:%s", userId, actionKey);
    }
}

image.pngimage.png4、总结

上述说了两种实现漏斗限流的方式,其实思想都是一样的,但是这两者都无法在分布式环境中使用,即便是在单机环境中也是不准确的,存在线程安全问题/原子性问题,因此我们一般使用Redis提供的限流模块Redis-Cell来限流,Redis-Cell提供了原子的限流指令cl.throttle,这个留到后续在详细说吧,我要睡觉去了!


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
数据挖掘 UED
功能发布-事件分析之漏斗分析
漏斗分析是基于事件的一种分析模型。 漏斗分析主要是对一个多步骤的场景进行的每一步的转化数据分析。可以理解为是从顶部(广泛数据)到底部(目标数据)逐步筛选和转化分析的过程。
功能发布-事件分析之漏斗分析
|
4月前
|
算法 API 缓存
高并发架构设计三大利器:缓存、限流和降级问题之固定窗口限流算法的原理是什么
高并发架构设计三大利器:缓存、限流和降级问题之固定窗口限流算法的原理是什么
|
4月前
|
存储 NoSQL Java
通用快照方案问题之通过Sleuth进行耗时分析和链路优化如何解决
通用快照方案问题之通过Sleuth进行耗时分析和链路优化如何解决
43 0
|
4月前
|
存储 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
|
4月前
|
存储 算法 缓存
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
高并发架构设计三大利器:缓存、限流和降级问题之滑动窗口算法的原理是什么
|
4月前
|
缓存 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之使用代码实现漏桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用代码实现漏桶算法问题如何解决
|
6月前
|
监控 Dubbo 测试技术
如何做好一次服务接口压测?
如何做好一次服务接口压测?
93 0
|
前端开发 数据可视化 关系型数据库
巧用 “ 火焰图 ” 快速分析链路性能
巧用 “ 火焰图 ” 快速分析链路性能
334 0
巧用 “ 火焰图 ” 快速分析链路性能
|
消息中间件 缓存 运维
10张图带你彻底搞懂限流、熔断、服务降级
10张图带你彻底搞懂限流、熔断、服务降级
1351 0
10张图带你彻底搞懂限流、熔断、服务降级
|
算法 数据库 UED
面试常见问题-限流策略有哪些,滑动窗口算法和令牌桶区别,使用场景
面试常见问题-限流策略有哪些,滑动窗口算法和令牌桶区别,使用场景
693 0