源码解读一 :ProcessorSlot责任链

[TOC]

概述

在总体设计章节中,我们知道 Sentinel 是通过不同的 ProcessorSlot 实现不同的功能,并且将不同的 ProcessorSlot 组装为一个责任链,也就是 ProcessorSlotChain,来对每一次资源访问使用不同 ProcessorSlot 进行处理从而实现对应的功能。那么在此之前,首先就需要进行 ProcessorSlotChain 的创建。

在 Sentinel 中,每一个资源都有与之对应的独立的 ProcessorSlotChain 。为此,Sentinel 中提供了一个方法 CtSph#lookProcessChain 来获取资源对应的 ProcessorSlotChain 。下面会针对整个获取和初始化的流程进行分析。

代码实现

初始化 ProcessorSlotChain

获取 SlotChainBuilder

Sentinel 所有的扩展点都是通过 SPI 的方式来实现。获取 ProcessorSlotChain 也不例外。对象 CtSph 内部有一个 Map 类型的属性 chainMap 用于存储资源和 ProcessorSlotChain 的映射关闭。方法 CtSph#lookProcessChain 首先是在这个 chainMap 中查找当前资源是否已经存在对应的 ProcessorSlotChain 。如果存在的话直接返回即可,如果不存在则新建对应的链条,并且汇总之前 chainMap 中的数据,创建一个新的 chainMap 对象来取代原先的 chainMap 对象。采用这种方式是因为一个系统一旦运行,其资源大致上是固定的,对应的链条也是固定的。

这是一个读多写少的场景,因此使用 HashMap 比 ConcurrentHashMap 更有性能上的优势。在 Sentinel 的其他配置信息上也都能看到这种实现思路。用非线程安全的容器承载数据,当容器内的数据发生变化时,则直接创建新的容器来替换旧的容器。通过这种方式实现多线程间正确的并发读可见性,并且因为是读多写少的场景,非线程安全的容器比线程安全的并发容器在性能表现上要更好。

如果需要新建 ProcessorSlotChain,则是调用静态方法 SlotChainProvider#newSlotChain 来实现。从类名 SlotChainProvider 就可以看出,该类的职责就是提供一个 ProcessorSlotChain 实例。但是具体的 ProcessorSlotChain 并不是该类来直接创建的,而是委托给了 SlotChainBuilder 这个接口。整体 newSlotChain 方法的流程如下

image-20201006104652334

这边唯一需要展开说明的就是这个 SPI 加载机制。Sentinel 对 SPI 机制的使用是自己封装了一个 SpiLoader 的类来实现的。其方法loadFirstInstanceOrDefault 从名字就可以看出含义:通过 SPI 机制加载第一个实例,如果不存在,则使用默认实例。方法的内部实现也仅仅是使用了 SPI 机制加载对应的接口的实现罢了,不展开说明。在默认使用情况下,也就是开发者都没有通过 SPI 进行这些接口的扩展情况下,Sentinel 都提供了默认的实现类,在这里的就是 DefaultSlotChainBuilder 。此外,该方***使用一个类变量 SERVICE_LOADER_MAP ,用来存储类名和 ServiceLoader 的映射关系。这意味着多次调用该方法得到的实例(如果是从 SPI 方式中加载的)是相同的。这对于某些全局属性或者只需要初始化一次的类是有利的。而如果需要多实例的场合,该方法就不太适用了。

DefaultSlotChainBuilder 构建 ProcessorSlotChain 实例

DefaultSlotChainBuilder 的 build 方法很简单,其主要内容就是:

  • 创建 DefaultProcessorSlotChain 实例。
  • 通过 SpiLoader ,使用 SPI 机制加载 ProcessorSlot 实例,并且根据对应类上的 SpiOrder 注解信息将实例进行排序。
  • 将排序后的 ProcessorSlot 实例按照顺序添加到 DefaultProcessorSlotChain 中,返回该 DefaultProcessorSlotChain 实例。

这边先讲讲第二步,也就是加载 ProcessorSlot 的过程。这一步是通过方法 SpiLoader#loadPrototypeInstanceListSorted 来实现加载的,方法名也表明了该方法返回的是一个排序后的实例列表。此外,该方法多次调用,每次返回的列表中都是全新的实例。这是因为该方法用于通过 SPI 加载 ProcessorSlot 实例列表,前文提到过,在 Sentinel 中,对于每一个资源,其都拥有独立的 ProcessorSlot 链条。也就是说,每一个不同的资源,其对应的 ProcessorSlot 处理器也是独立的。

了解了这一点后,loadPrototypeInstanceListSorted 方法就没有太多的内容了。方法的实现步骤主要是:

  • 通过 SPI 机制加载 ProcessorSlot 实例。
  • 解析 ProcessorSlot 实现类上的 SpiOrder 注解,取得注解中的数字值作为排序依据。如果注解不存在的话,则排序数字值按照最小数字处理。
  • 依据每一个 ProcessorSlot 不同的排序数字值,按照升序排序。返回排序后的 ProcessorSlot 列表。

排序的这段代码,Sentinel 写的比较复杂且奇怪,自己通过 List 去实现排序,而没有使用 JDK 直接提供的 Collections#sort(List<t>) 方法。显得代码比较臃肿。</t>

DefaultProcessorSlotChain 内部逻辑

在了解了 SpiLoader#loadPrototypeInstanceListSorted 实现排序加载 ProcessorSlot 实例列表后,我们来看下 DefaultProcessorSlotChain 的内部构造和责任链的实现模式。仍然先看下类图,如下

链条的基类 ProcessorSlotChain 也实现了处理插槽 ProcessorSlot 接口。这也是责任链模式常见的一种实现方式。就是链条类本身也实现了链条中节点的接口,这样对于外部调用者而言,实际上只需要关心一个处理器接口就好了。无需知道底层是一个链条还是说只是一个节点。

我们先看下 ProcessorSlot 的接口定义,如下

public interface ProcessorSlot<T> {
    void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, boolean prioritized,
               Object... args) throws Throwable;
    void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized,
                   Object... args) throws Throwable;
    void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
    void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
}

ProcessorSlot 接口是由对称的两组方法组成 entry 和 fireEntry 为一组,exit 和 fireExit 为一组。在 Sentinel 中的访问资源的模式是

  • 通过 Sph 的 entry 方法,获取对资源的访问许可。如果成功获得 Entry 对象,则说明资源访问请求被允许。
  • 访问具体的资源(比如执行受保护的代码块)。
  • 执行 Entry.exit 方法,表明该资源的访问结束。

在第一步 Sph.entry 方法中,Entry 对象被创建后,就需要经过 ProcessosSlotChain 的处理,也就是各个 ProcessorSlot 节点执行其 entry 方法,来启动各个节点对该资源本次访问的数据度量的开始(在度量的过程中可能就会触发阻断异常)。

在第三步请求结束后,Entry.exit 方法内部中,在方法返回前,Entry对象也需要经过 ProcessosSlotChain 中各个 ProcessosSlot 节点的处理,执行其 exit 方法来完成各个节点对该资源本次访问的数据度量的结束。

fireEntry 方法表示该 Slot 的 entry 方法已经执行完毕,可以将 entry 对象传递给下一个 slot ;fireExit 方法表示该 Slot 的 exit 方法已经执行完毕,可以将 entry 对象传递给下一个 Slot 。

fireEntry 和 fireExit 都是为了实现责任链模式而在 ProcessorSlot 上添加的方法。但是一般而言,责任链是责任链,处理节点是处理节点。处理节点接口中是不需要有一个方法特意用来实现任务交接的,这应该是责任链本身的工作。将传递方法放入到节点接口中,是一种对处理节点接口的污染做法。实现责任链有许多代码模式,这里举例常见的两种,如下:

  • 在节点接口的方法中,方法的最后一个入参是一个 Proxy 接口对象,这个 Proxy 接口的方法签名和节点接口的方法签名是一致的。Proxy 对象代表的就是下一个处理节点。那么在实现节点方法的时候,如果本节点的内容处理完毕了,则可以调用 Proxy 对象的同名方法,并且传入参数,实现处理任务从本节点流转到下一个节点。由于 Proxy 接口和处理节点接口一致,因此外部调用者无需了解链条信息,只需要掌握处理节点接口知识即可。
  • 使用单独的节点上下文类来持有节点接口的实现对象。节点上下文本身去实现链表的功能,比如持有 next 和 prev 属性,通过节点上下文形成链表,变相的实现了处理器接口实现形成了链表。而节点上下文的方法接口一般也是与处理器接口相同。但是在内部实现上一般都遵循 2 个步骤:1)首先调用处理器接口的同名方法;2)处理器接口方法调用成功后,上下文节点调用下一个上下文节点的同名方法,实现任务的流转。由于上下文节点接口与处理节点接口一致,外部调用者也无需了解链条信息。

上述两种做法,都不需要在处理器节点接口中引入专门的用于流转任务的方法,笔者认为,Sentinel 在这块的处理上,代码是比较欠妥的。

无论如何,让我们接着看看最顶层的基类,也就是 AbstractLinkedProcessorSlot ,它的代码实现很简单,直接贴出来,如下

public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
    private AbstractLinkedProcessorSlot<?> next = null;
    @Override
    public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        if (next != null) {
            next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
        }
    }
    @SuppressWarnings("unchecked")
    void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
        throws Throwable {
        T t = (T)o;
        entry(context, resourceWrapper, t, count, prioritized, args);
    }
    @Override
    public void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        if (next != null) {
            next.exit(context, resourceWrapper, count, args);
        }
    }
    public AbstractLinkedProcessorSlot<?> getNext() {
        return next;
    }
    public void setNext(AbstractLinkedProcessorSlot<?> next) {
        this.next = next;
    }
}

可以看道,其通过一个 next 属性,实现了 ProcessorSlot 的链表关系。AbstractLinkedProcessorSlot 对 fireEntry 和 fireExit 方法的实现就是使用 next 属性完成任务的传递,上面已经说过思路了,这边不赘述。因为链条关系是依靠 AbstractLinkedProcessorSlot 实现的,所以开发者在实现自己的 ProcessorSlot 实现类的时候也一定需要继承这个基类。实际上,Sentinel 在通过 SPI 加载 ProcessosSlot 实例的时候,也会检查这个实例是不是继承了 AbstractLinkedProcessorSlot ,如果没有的话,则不会将这个 ProcessorSlot 实例加入到链条之中。

了解完 AbstractLinkedProcessorSlot 后,让我们来看下 ProcessorSlotChain 。与前者相比,ProcessorSlotChain 只是多了两个方法:addFirst 和 addLast 。这两个方法用于向链条中添加 AbstractLinkedProcessorSlot 对象,也就是增加处理节点。

最后就是 DefaultProcessorSlotChain 。这个类内部设计了两个属性:first 和 end 。两者都是 AbstractLinkedProcessorSlot 类型。而其中 first 属性指向的是一个特殊的空实现。该实现的 entry 方法的内容是直接调用 fireEntry ,也就是直接将任务传递给后面一个 Slot 处理。该实现的 exit 方法的内容是直接调用 fireExit ,也就是将任务传递给后面一个 Slot 处理。

first 和 end 就相当于链表的首节点和尾结点。有了这两个属性后,addLast 方法的实现就是将新的 Slot 实例设置为 end 节点的 next 属性,并且让 end 节点指向这个新的 slot 实例,常规的链表操作方式。如此一来,ProcessorSlot 链表就构建完成了。

使用 ProcessorSlotChain

ProcessosSlotChain 的使用十分简单,由于其实现了 ProcessosSlot 接口,因此外部将其当成处理节点来传入需要 Context 对象即可。上文说过,在资源访问前会调用 entry 方法,在资源访问结束后会调用 exit 方法。

在 ProcessorSlotChain 内部,就是简单的责任链模式传递任务,这里不再赘述。

总结

本文分析了 ProcessorSlot 责任链模式的内部实现原理,ProcessorSlotChain 的初始化流程等。搞明白这个在 Sentinel 中主要的工作模式后,下一个章节,我们来探寻 Sentinel 框架的入口类,Sph。