聊聊自定義SPI如何與sentinel整合實現熔斷限流

語言: CN / TW / HK

前言

之前我們聊了一下聊聊如何實現一個帶有攔截器功能的SPI。當時我們實現的核心思路是利用了責任鏈+動態代理。今天我們再聊下通過動態代理如何去整合sentinel實現熔斷限流

前置知識

alibaba sentinel簡介

Sentinel 是面向分散式服務架構的流量控制組件,主要以流量為切入點,從限流、流量整形、熔斷降級、系統負載保護、熱點防護等多個維度來幫助開發者保障微服務的穩定性。

sentinel工作流程

在這裡插入圖片描述

sentinel關鍵詞

資源 + 規則

sentinel實現模板套路

Entry entry = null;
// 務必保證 finally 會被執行
try {
  // 資源名可使用任意有業務語義的字串,注意數目不能太多(超過 1K),超出幾千請作為引數傳入而不要直接作為資源名
  // EntryType 代表流量型別(inbound/outbound),其中系統規則只對 IN 型別的埋點生效
  entry = SphU.entry("自定義資源名");
  // 被保護的業務邏輯
  // do something...
} catch (BlockException ex) {
  // 資源訪問阻止,被限流或被降級
  // 進行相應的處理操作
} catch (Exception ex) {
  // 若需要配置降級規則,需要通過這種方式記錄業務異常
  Tracer.traceEntry(ex, entry);
} finally {
  // 務必保證 exit,務必保證每個 entry 與 exit 配對
  if (entry != null) {
    entry.exit();
  }
}

sentinel wiki

https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5

實現思路

整體實現思路:動態代理 + sentinel實現套路模板

核心程式碼

動態代理部分

1、定義動態代理介面

public interface CircuitBreakerProxy {

    Object getProxy(Object target);

    Object getProxy(Object target,@Nullable ClassLoader classLoader);
}

2、定義JDK或者cglib具體動態實現

以jdk動態代理為例子

public class CircuitBreakerJdkProxy implements CircuitBreakerProxy, InvocationHandler {

    private Object target;

    @Override
    public Object getProxy(Object target) {
        this.target = target;
        return getProxy(target,Thread.currentThread().getContextClassLoader());
    }

    @Override
    public Object getProxy(Object target, ClassLoader classLoader) {
        this.target = target;
        return Proxy.newProxyInstance(classLoader,target.getClass().getInterfaces(),this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        CircuitBreakerInvocation invocation = new CircuitBreakerInvocation(target,method,args);
        try {
            return new CircuitBreakerInvoker().proceed(invocation);
            //用InvocationTargetException包裹是java.lang.reflect.UndeclaredThrowableException問題
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }

    }
}

3、動態代理具體呼叫

public class CircuitBreakerProxyFactory implements ProxyFactory{
    @Override
    public Object createProxy(Object target) {
        if(target.getClass().isInterface() || Proxy.isProxyClass(target.getClass())){
            return new CircuitBreakerJdkProxy().getProxy(target);
        }
        return new CircuitBreakerCglibProxy().getProxy(target);
    }
}

ps: 上面的動態代理實現思路是參考spring aop動態代理實現

具體參考類如下

org.springframework.aop.framework.AopProxy
org.springframework.aop.framework.DefaultAopProxyFactory

sentinel實現部分

public class CircuitBreakerInvoker {

    public Object proceed(CircuitBreakerInvocation circuitBreakerInvocation) throws Throwable {

       Method method = circuitBreakerInvocation.getMethod();

        if ("equals".equals(method.getName())) {
            try {
                Object otherHandler = circuitBreakerInvocation.getArgs().length > 0 && circuitBreakerInvocation.getArgs()[0] != null
                        ? Proxy.getInvocationHandler(circuitBreakerInvocation.getArgs()[0]) : null;
                return equals(otherHandler);
            } catch (IllegalArgumentException e) {
                return false;
            }
        } else if ("hashCode".equals(method.getName())) {
            return hashCode();
        } else if ("toString".equals(method.getName())) {
            return toString();
        }


        Object result = null;

        String contextName = "spi_circuit_breaker:";

        String className = ClassUtils.getClassName(circuitBreakerInvocation.getTarget());
        String resourceName = contextName + className + "." + method.getName();


        Entry entry = null;
        try {
            ContextUtil.enter(contextName);
            entry = SphU.entry(resourceName, EntryType.OUT, 1, circuitBreakerInvocation.getArgs());
            result = circuitBreakerInvocation.proceed();
        } catch (Throwable ex) {
            return doFallBack(ex, entry, circuitBreakerInvocation);
        } finally {
            if (entry != null) {
                entry.exit(1, circuitBreakerInvocation.getArgs());
            }
            ContextUtil.exit();
        }

        return result;
    }
    }

ps: 如果心細的朋友,就會發現這個邏輯就是sentinel原生的實現套路邏輯

示例演示

示例準備

1、定義介面實現類並加上熔斷註解

@CircuitBreakerActivate(spiKey = "sqlserver",fallbackFactory = SqlServerDialectFallBackFactory.class)
public class SqlServerDialect implements SqlDialect {
    @Override
    public String dialect() {
        return "sqlserver";
    }

}

ps: @CircuitBreakerActivate這個是自定義熔斷註解,用過springcloud openfeign的@FeignClient註解大概就會有種熟悉感,都是在註解上配置fallbackFactory或者fallbackBack

2、定義介面熔斷工廠

@Slf4j
@Component
public class SqlServerDialectFallBackFactory implements FallbackFactory<SqlDialect> {

    @Override
    public SqlDialect create(Throwable ex) {
        return () -> {
            log.error("{}",ex);
            return "SqlServerDialect FallBackFactory";
        };
    }
}

ps: 看這個是不是也很熟悉,這不就是springcloud hystrix的熔斷工廠嗎

3、專案中配置sentinel dashbord地址

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
      filter:
        enabled: false

示例驗證

1、瀏覽器訪問http://localhost:8082/test/ciruitbreak

此時訪問正常,看下sentinel-dashbord

2、配置限流規則

3、再次快速訪問

由圖可以看出已經觸發熔斷

本示例專案,如果不配置fallback或者fallbackFactory,當觸發限流時,它也會有個預設fallback

把示例註解的fallbackFactory去掉,如下

@CircuitBreakerActivate(spiKey = "sqlserver")
public class SqlServerDialect implements SqlDialect {
    @Override
    public String dialect() {
        return "sqlserver";
    }

}

再重複上面的訪問流程,當觸發限流時,會提示如下

總結

自定義spi的實現系列接近尾聲了,其實這個小demo並沒有多少原創的東西,大都從dubbo、shenyu、mybatis、spring、sentinel原始碼中抽出一些比較好玩的東西,東拼西湊出來的一個demo。

平時我們大部分時間還是在做業務開發,可能有些人會覺得天天crud,感覺沒啥成長空間,但只要稍微留意一下我們平時經常使用的框架,說不定就會發現一些不一樣的風景

demo連結

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-spi-enhance/springboot-spi-framework-circuitbreaker