一次性搞定分布式限流————手写分布式限流框架


gitee: https://gitee.com/qiaodaimadewangcai/flood-myth

一、目标和需求分析

为了框架能满足当代互联网的基本需求,和使用的便利,优先实现以下几点需求。

  1. 支持分布式
  2. 支持SpringBoot-start
  3. 支持失败回调
  4. 单机模式下支持微秒级响应,分布式模式下支持毫秒级响应

其余还有写其他值得讨论实现的内容,优先级不是最高暂且先放着,我们以后有空再实现

  1. 支持高可靠性
  2. 支持监控
  3. 支持动态调节
  4. 支持持久化

二、初步设计

采用中心化的方式支持分布式,框架分成2部分“客户端”与”服务端”,为了方便还是将两部分写在一个工程中,客户端与服务端通过http进行通讯

三、客户端的实现

1)限流器参数定义

参数定义都是简单的pojo,不做过多说明,具体看代码

(1)限流规则定义

主要定义令牌桶的参数、限流器的运行模式和行为模式
https://gitee.com/qiaodaimadewangcai/flood-myth/blob/master/src/main/java/com/gyx/floodmyth/entity/LimiterRuleWrapper.java

(2)客户端配置定义

是一个单例模式,负责记录服务器的信息,包含一个线程池,用于向服务器发送心跳,并且拉取服务器上的信息
https://gitee.com/qiaodaimadewangcai/flood-myth/blob/master/src/main/java/com/gyx/floodmyth/entity/LimiterConfigWrapper.java

2)限流器实现

每个限流器都对应一个限流规则、一个客户端配置。

限流器中最重要的是实现一个限流算法,目前比较流行的几种限流算法——滑窗、漏桶、令牌桶。这里采用令牌桶限流。

令牌桶的实现主要包括2个部分

  • 填装令牌
  • 消耗令牌

整个限流器其实都是在令牌桶的实现上添加了一些功能

(1)接口定义

限流器主要考虑初始化方法、尝试访问限流的方法

public interface LimiterHandler {
    /**
     * 初始化
     * @param rule 限流规则的包装器
     */
    void init(LimiterRuleWrapper rule);
    /**
     * 尝试访问
     * @param tokenNum 消耗的令牌数量
     */
    boolean tryAccess(Integer tokenNum);
    /**
     * 获取限流规则标识
     */
    String getId();
    /**
     * 获取限流规则
     */
    LimiterRuleWrapper getRule();
}
(2)限流器的抽象实现

我们想看成员变量的部分,除了对限流规则、客户端配置的持有,包括一个令牌的计数器(bucket),一个令牌桶的填装器(scheduledFuture)。

  • 令牌计数器,会有多个线程频繁的读写,使用atomic包下的对象,保证线程安全

  • 令牌装填器是一个定时器,会按照配置定时增加令牌计数器,仅在单机的模式下会用到,分布式的时候令牌的填装的工作会移交给服务器

public abstract class AbstractLimiterHandler implements LimiterHandler {
    /**
     * 令牌桶
     * 初始容量为0
     */
    protected final AtomicLong bucket = new AtomicLong(0);
    /**
     * 限流规则
     */
    protected LimiterRuleWrapper rule;
    /**
     * 限流器集群配置
     */
    protected LimiterConfigWrapper config;
    /**
     * 令牌装填器
     *
     * 用于给令牌桶补充令牌
     */
    protected ScheduledFuture<?> scheduledFuture;
}

限流器初始化的时候,必须填入限流规则和客户端配置,并且会停止令牌桶的装填

在限流规则发生改变的时候,可以单独调用init方法,以便用新的规则替换旧的规则

public AbstractLimiterHandler(LimiterRuleWrapper rule, LimiterConfigWrapper config) {
    this.config = config;
    init(rule);
}

/**
 * 初始化
 * @param rule 限流规则的包装器
 */
@Override
public void init(LimiterRuleWrapper rule) {
    this.rule = rule;
    if (this.scheduledFuture != null) {
        this.scheduledFuture.cancel(true);
    }
}

尝试访问的方法会消耗令牌,当limit==0的时候,意味着不会填装令牌,所以直接返回false。

AccessStrategy是一个访问策略接口,这里使用了策略模式,提供2中访问策略

  • 快速失败策略
  • 阻塞策略
/**
 * 尝试访问
 *
 * @param tokenNum 消耗的令牌数量
 */
@Override
public boolean tryAccess(Integer tokenNum) {
    if (rule.isEnable()) {
        //限流功能已关闭
        return true;
    }
   if (rule.getLimit() == 0) {
        return false;
    }
    return AccessStrategy.strategy.get(rule.getAccessModel()).tryAccess(bucket, rule,tokenNum);
}

getId方法、getRule只是简单的get方法,前一个返回rule的id,后一个返回rule。

(2)访问策略实现

访问策略一共有2种,代码结构上通过策略模式进行解耦,以满足开闭原则。
接口中包含一个静态变量,和一个方法。静态变量实际上是一个简单工厂,用来初始化和访问不同的策略。

public interface AccessStrategy {
    /**
     * 用于访问策略
     */
    Map<AccessModel, AccessStrategy> strategy = new HashMap<AccessModel, AccessStrategy>(2) {{
        put(AccessModel.FAIL_FAST, new FailFastAccess());
        put(AccessModel.BLOCKING, new BlockingAccess());
    }};

    /**
     * 尝试访问
     *
     * @param bucket   令牌桶
     * @param rule     限流器规则
     * @param tokenNum 消耗的令牌数量
     */
    boolean tryAccess(AtomicLong bucket, LimiterRuleWrapper rule, Integer tokenNum);
}

快速失败访问策略,令牌不够立马失败,返回失败

public class FailFastAccess implements AccessStrategy {
    @Override
    public boolean tryAccess(AtomicLong bucket, LimiterRuleWrapper rule, Integer tokenNum) {
        //CAS获取令牌,没有令牌立即失败
        long l = bucket.longValue();
        while (l >= tokenNum) {
            if (bucket.compareAndSet(l, l - tokenNum)) {
                return true;
            }
            l = bucket.longValue();
        }
        return false;
    }
}

阻塞访问策略,令牌不够的时候,阻塞线程,直到令牌足够

public class BlockingAccess implements AccessStrategy {
    @Override
    public boolean tryAccess(AtomicLong bucket, LimiterRuleWrapper rule,Integer tokenNum) {
        //CAS获取令牌,阻塞直到成功
        long l = bucket.longValue();
        while (!(l >= tokenNum && bucket.compareAndSet(l, l - tokenNum))) {
            sleep(rule);
            l = bucket.longValue();
        }
        return true;
    }
    /**  sleep方法 **/
}
(3)单机限流器的实现

单机限流器继承抽象限流器,也是通过父类的构造器进行初始化,这里需要注意父类的构造器中会调用init初始化方法,但是实际执行的init并非父类中的init方法,而是子类重写的init方法。

public class LocalLimiterHandler extends AbstractLimiterHandler {

    public LocalLimiterHandler(LimiterRuleWrapper rule, LimiterConfigWrapper config) {
        super(rule, config);
    }
}

重写父类中的init方法,在父类的基础上额外初始化定时器,将限流规则中的参数,填入到线程池中就行了,定时器会按照指定的周期,定时的装填令牌。

/** * 初始化 * @param rule 限流规则的包装器 */@Overridepublic void init(LimiterRuleWrapper rule) {    super.init(rule);    if (rule.getLimit() == 0) {        return;    }    this.scheduledFuture = config.getScheduledThreadExecutor()            .scheduleAtFixedRate(() -> {                //当前的令牌数 + 每次填装的令牌数  < 最大令牌数                if (bucket.get() + rule.getLimit() < rule.getMaxLimit()) {                    bucket.set(rule.getLimit());                }                //首次延迟时间、周期单位时间、时间单位            }, rule.getInitialDelay(), rule.getPeriod(), rule.getUnit());}

其他方法都在抽象类中已经实现了不需要修改。

(4)分布式限流器的实现

分布式限流器一样继承抽象限流器,但是无需重写init方法,分布式限流器的令牌填装是通过与服务器连接完成的,所以不需要初始化定时器。

public class CloudLimiterHandler extends AbstractLimiterHandler {    public CloudLimiterHandler(LimiterRuleWrapper rule, LimiterConfigWrapper config) {        super(rule, config);    }}

重写尝试访问的方法,分布式消耗令牌的逻辑也是在客户端上实现的,和单机的逻辑没有区别,在尝试访问结束之后,会访问服务器获取令牌,填装令牌桶。

/** * 尝试访问 * * @param tokenNum 消耗的令牌数量 */@Overridepublic boolean tryAccess(Integer tokenNum) {    boolean accessFlag = super.tryAccess(tokenNum);    putCloudBucket();    return accessFlag;}

获取令牌的方法,看似繁琐,实际上只是用客户端配置中的定时器执行一个http请求,获取到令牌后填装到令牌桶中,其余的全是判断,中间用到一个经典的双重检查锁。

/** * 从集群令牌分发中心,获取令牌,填装到令牌桶中 */private void putCloudBucket() {    //校验令牌数量是否需要获取    if (bucket.get() * rule.getBatch() > rule.getRemaining()) {        return;    }    //获取定时器线程    config.getScheduledThreadExecutor().execute(() -> {        //双重检查锁  第一层        if (bucket.get() * rule.getBatch() <= rule.getRemaining()) {            //双重检查锁  上锁            synchronized (bucket) {                //双重检查锁 第二层                if (bucket.get() * rule.getBatch() <= rule.getRemaining()) {                    //发送http获取令牌,然后填装到令牌桶中                    String result = config.getAllotServer().connect(LimiterConfigWrapper.http_token, JSON.toJSONString(rule));                    if (result != null) {                        bucket.getAndAdd(Long.parseLong(result));                    }                }            }        }    });}

看完上面的代码肯定对AllotServer还不太清楚,AllotServer是对服务器资源访问和管理的类,接下来就一起看一下这个类。

3)分发服务器管理类实现

AllotServer用于记录服务器的地址,和提供相应的访问方法。一共4个成员变量

  • serverList——用于记录令牌分发服务器地址,读多写少
  • backupsList——地址的备份,当服务器访问失败的后,地址会被转移到这里
  • lock——读写锁,防止读取的地址的时候,地址发送变化
  • pos——用于轮询的计数器
public class AllotServer {    private List<String> serverList = new CopyOnWriteArrayList<>();    private List<String> backupsList = new CopyOnWriteArrayList<>();    private ReentrantLock lock = new ReentrantLock();    private int pos = 0;}

设置令牌分发服务器,map的key是服务器的ip地址,value是服务器的权重,先清空原本的列表,然后模仿CopyOnWriteArrayList,通过复制避免并发问题。
添加权重的方式,也采用比较偷懒的方式,像list中添加重复元素,权重越高的元素,重复的次数越多

public void setServer(Map<String, Integer> ip) {    // 清空List    serverList.clear();    // 重建一个Map,避免服务器的上下线导致的并发问题    Map<String, Integer> serverMap = new HashMap<>(ip);    // 取得Ip地址List    for (String server : serverMap.keySet()) {        int weight = serverMap.get(server);        //添加权重        for (int i = 0; i < weight; i++) {            serverList.add(server);        }    }}

获取服务器方法,获取服务器地址的时候需要上锁,防止冲突,当地址全部失效后,从之前失效的地址再次尝试连接,通过轮询的方式对服务器进行访问。

private String getServer() {    String server;    lock.lock();    try {        if (serverList.size()==0){            serverList.addAll(backupsList);            backupsList.clear();        }        if (pos >= serverList.size()) {            pos = 0;        }        server = serverList.get(pos);        pos++;    } finally {        lock.unlock();    }    return server;}

访问服务器的方法,非常简单向服务器发送一个http请求就行了

public String connect(String path, String data) {
    String server = getServer();
    try {
        return HttpUtil.connect("http://" + server + "/" + path)
                .setData("data", data)
                .setMethod("POST")
                .execute()
                .getBody();
    } catch (IOException e) {
        serverList.remove(server);
        backupsList.add(server);
    }
    return null;
}

4)限流器注册实现

限流器注册过程包括2部分,限流器的构造工厂和注册器。

(1)限流器构造工厂

限流器的构造工厂很简单,通过简单工厂进行创建,然后向注册器注册

public class LimiterFactory {

    public static LimiterHandler of(LimiterRuleWrapper rule) {
        return of(rule, LimiterConfigWrapper.getInstance());
    }

    public static LimiterHandler of(LimiterRuleWrapper rule, LimiterConfigWrapper config) {
        switch (rule.getLimiterModel()) {
            case LOCAL:
                //本地限流
                LimiterHandler limiter = new LocalLimiterHandler(rule, config);
                RegisterServer.registered(limiter);
                return limiter;
            case CLOUD:
                //集群限流
                limiter = new CloudLimiterHandler(rule, config);
                rule.setName(rule.getName() == null ? String.valueOf(limiter.hashCode()) : rule.getName());
                RegisterServer.registered(limiter, config);
                return limiter;
            default:
                throw new RuntimeException("无法识别限流处理器运行模式");
        }
    }
}
(2)限流注册器

注册器主要用于缓存所有的限流器,并且提供相应的访问方法

注册器通过一个map缓存所有的限流器,key为id,value为限流器实例。ConcurrentHashMap保证线程安全

public class RegisterServer {
    /**
     * 限流处理器的容器
     */
    private static Map<String, LimiterHandler> limiterContainer = new ConcurrentHashMap<>();
}

提供一个静态方法,可以很方便的访问所有的限流器

public static LimiterHandler get(String id) {
    LimiterHandler limiterHandler = limiterContainer.get(id);
    if (limiterHandler == null){
        throw new RuntimeException("无法查询到处理");
    }
    return limiterHandler;
}

本地限流器的注册非常简单,放入map就行了

public static void registered(LimiterHandler limiter) {
    if (limiterContainer.containsKey(limiter.getId())) {
        throw new RuntimeException("不可以重复注册限流处理器,限流器id:" + limiter.getId());
    }
    limiterContainer.put(limiter.getId(), limiter);
}

分布式限流器注册除了需要将对象存入map,还需要想服务器发出请求,同步服务器上的限流规则,如果连接失败,会转成本地服务运行。

/**
 * 分布式注册
 *
 * @param limiter 限流处理器
 * @param config  限流器配置包装类
 */
public static void registered(LimiterHandler limiter, LimiterConfigWrapper config) {
    //注册在本地
    registered(limiter);
    //从令牌中心拉取规则,更新本地限流规则
    rulePull(limiter, config);
}

/**
 * 从令牌中心拉取规则,更新本地限流规则
 *
 * @param limiter 限流处理器
 * @param config  限流器配置包装类
 */
private static void rulePull(LimiterHandler limiter, LimiterConfigWrapper config) {
    config.getScheduledThreadExecutor().scheduleWithFixedDelay(() -> {
        //连接远程获取配置
        String rules = config.getAllotServer().connect(LimiterConfigWrapper.http_heart, JSON.toJSONString(limiter.getRule()));
        if (rules == null) {
            //连接失败,转成本地模式运行
            LimiterRuleWrapper rule = limiter.getRule();
            rule.setLimiterModel(LimiterModel.LOCAL);
            limiter.init(rule);
            return;
        }
        LimiterRuleWrapper newestRule = JSON.parseObject(rules, LimiterRuleWrapper.class);
        if (newestRule.getVersion() > limiter.getRule().getVersion()) {
            //版本升级
            if (newestRule.getLimiterModel().equals(LimiterModel.LOCAL)) {
                //禁止改成本地模式
                newestRule.setLimiterModel(LimiterModel.CLOUD);
            }
            //更新规则
            limiterContainer.get(limiter.getId()).init(newestRule);
        }
    }, 0, 1, TimeUnit.SECONDS);
}

5)注解实现

通过注解对接口进行限流,被注解的方法就会访问限流器进行限流,如果限流失败会调用指定的回调方法

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@Inherited
public @interface Limiter {
    /**
     * Limiter id
     */
    String value() default "";
    /**
     * 令牌消耗数量
     */
    int num() default 1;
    /**
     * 回调方法
     */
    String fallback() default "";
}

注解的实现很简单,通过环绕切面,将限流器调用包裹在目标方法外,如果执行失败,就调用回调方法,这个的回调方法比较简单,所以回调方法必须和注解注释的方法在同一个类中,并且参数完全一致

@Aspect
public class LimiterAspect {

    @Pointcut("@annotation(com.gyx.floodmyth.aspect.Limiter)")
    public void pointcut() {
    }

    @Around("pointcut() && @annotation(limiter)")
    public Object around(ProceedingJoinPoint pjp, Limiter limiter) throws Throwable {
        LimiterHandler rateLimiter = RegisterServer.get(limiter.value());
        if (rateLimiter.tryAccess(limiter.num())) {
            return pjp.proceed();
        }
        //快速失败后的回调方法
        return fallback(pjp, limiter);
    }

    /**
     * 快速失败的回调方法
     * @param pjp     切入点
     * @param limiter 注解数据
     */
    private Object fallback(ProceedingJoinPoint pjp, Limiter limiter) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Signature sig = pjp.getSignature();
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("此注解只能使用在方法上");
        }
        //回调方法必须和注解注释的方法在同一个类中,并且参数完全一致
        MethodSignature msg = (MethodSignature) sig;
        Object target = pjp.getTarget();
        Method fallback = target.getClass().getMethod(limiter.fallback(), msg.getParameterTypes());
        return fallback.invoke(target, pjp.getArgs());
    }
}

四、服务器实现

未完

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 分布式限流的本质是通过在分布式系统中限制请求数量或请求速率,来保证系统的可用性和稳性。它的目的是防止系统被过度请求导致的资源耗尽,如内存溢出、网络阻塞或数据库瘫痪等。分布式限流的实现方式通常包括通过预先分配的令牌桶或漏桶等算法来控制请求的速率,从而达到限流的目的。 ### 回答2: 分布式限流的本质是通过分布式系统的协同工作来限制并发访问量,保证系统的稳性和可靠性。 在传统的单机限流模式下,系统通过对单一服务节点进行限制访问数量,但随着互联网的发展和用户量的增加,单机限流往往无法满足需求。分布式限流采用了多台服务器协同工作的方式,将限流逻辑转移到分布式网关或代理层上。 分布式限流的本质是通过网关或代理层的协同工作来限制并发访问量。具体实现可以通过以下几个步骤: 1. 请求进入分布式网关或代理层:所有的请求都会首先进入分布式网关或代理层,这些网关或代理层可以是负载均衡器、反向代理、API网关等。 2. 限流策略设置:网关或代理层会根据预设的限流策略来判断是否允许请求通过。限流策略可以包括每秒允许通过的请求数、每分钟允许通过的请求数、每个用户允许的请求数等。 3. 限流算法实现:网关或代理层会根据限流策略实现相应的限流算法,例如漏桶算法、令牌桶算法等。这些算法可以根据当前系统的负载情况和预设的参数来动态地调整限制并发访问量。 4. 请求转发或拒绝:根据限流算法的结果,网关或代理层会将请求转发到后端的服务节点,或者直接拒绝请求。拒绝请求可以返回错误信息或者重向到其他页面。 通过以上步骤,分布式限流可以实现对并发访问量的限制,保证系统的稳性和可靠性。同时,分布式限流还可以根据系统负载情况动态调整限制参数,以适应不同规模和需求的系统。 ### 回答3: 分布式限流的本质是通过将请求的处理分散到多个节点中,从而实现对系统资源的控制和保护。在高并发的场景下,如果没有限制,大量请求同时涌入系统,容易导致系统资源耗尽,出现性能问题甚至系统崩溃。 分布式限流的本质是将限流操作从单个节点扩展到多个节点,通过集群间的协调和通信,实现对请求的限制和分配。其核心思想是通过集中式的限流策略和算法,将请求分配到不同的节点进行处理,确保每个节点的负载均衡,避免由于单节点处理过多请求而造成的性能问题。 分布式限流的关键点在于如何判断请求是否超出系统的承载能力,并如何合理地分配请求到各个节点。常见的限流算法包括令牌桶算法、漏桶算法等,通过设置合理的参数和规则,对请求进行限制和分配。此外,还可以通过流量控制、速率限制等段进行限流操作。 分布式限流的本质是为了保证系统的稳性和可靠性,避免由于并发量过大而导致的系统故障。通过将请求分散到多个节点中,可以降低单个节点的压力,提高系统的整体性能和吞吐量。同时,分布式限流也可以用于保护系统免受恶意攻击和异常请求的影响,提高系统的安全性。 综上所述,分布式限流的本质是通过多节点的协作和限制策略,实现对系统资源的控制和分配,以确保系统的稳性、高性能和安全性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值