使用Resilience4J实现断路器模式


断路器是一种模式,可以防止整个架构中单个微服务的故障级联,从而确保系统具有弹性。该模式可以通过像Hystrix或Resilience4j这样的代码库实现,或者通过底层基础设施来实现,例如使用Istio。

Hystrix vs. Resilience4j简介
Hystrix是Netflix提供的一个开源库,旨在提高分布式系统的弹性,使HTTP请求在其分布式组件之间进行通信。它通过实现断路器模式实现。
Resilience4J是一个受Hystrix启发的独立库,它建立在功能编程的原理之上。两者之间最显着的区别在于,虽然Hystrix采用面向对象的设计,其中对外部系统的调用必须包含在HystrixCommand提供多种功能中,但Resilience4J依赖于函数组合来让您堆叠所需的特定装饰器。
那些装饰器当然包括断路器,还包括速率限制器,重试和隔板。这些装饰器可以同步或异步执行,充分利用Java 8中引入的lambda。
Resilience4J的其他优点包括更精细的配置选项(例如,关闭断路器模式所需的成功执行次数)和更轻的依赖性足迹。

Java中的函数编程简介
函数组合背后的想法是:

  • 如果函数f被定义为Function<X, Y>- 将类型X作为输入并返回类型的函数Y
  • 如果函数g定义为Function<Y, Z>
  • 然后可以将新函数h定义为Function<X, Z>组成函数f和g

Java 8在其API中引入了函数编程(FP)的一些方面。上面的函数组合可以在Java中翻译:

public class F implements Function<Integer, Integer> {

    @Override
    public Integer apply(Integer x) {
        return x + 1;
    }
}

public class G implements Function<Integer, Integer> {

    @Override
    public Integer apply(Integer y) {
        return 2 * y;
    }
}

public class H implements  Function<Integer, Integer> {

    @Override
    public Integer apply(Integer x) {
        var f = new F();
        var g = new G();
        Integer y = f.apply(x);
        return g.apply(y);
    }
}

这非常麻烦,因为Java最初设计时考虑了面向对象编程(OOP)。

一切都需要属于一个类,即使这没有多大意义。因此,为了弥合OOP和FP之间的这种差距,并使FP代码更容易编写,Java 8带来了函数接口的概念:功能接口是一个带有单个抽象方法的接口,并且可选择带注释@FunctionalInterface。
可以使用lambda表示法以简化的方式编写任何功能接口。例如,Function<T, V>是一个函数接口,因为它有一个抽象方法 - apply()。因此,可以使用lambdas重写上面的代码:

Function<Integer, Integer> h = x -> {
    Function<Integer, Integer> f = y -> y + 1;
    Function<Integer, Integer> g = y -> y + 1;
    return g.apply(f.apply(x));
};

FP的另一个基础是高阶函数。这只意味着函数是类似于任何其他类型的函数,并且可以作为函数中的参数传递,并作为结果返回。
例如,Functioninterface定义以下方法:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

使用这种方法,我们可以简单地重写h函数:

var h = f.compose(g);

Resilience4J完全基于函数式编程,并且使用了很多公开的概念。重要的是要记住从Hystrix迁移,因为与通常的Java思维方式相比,这需要进行更改。

Resilience4J入门
HTTP调用可以被认为是一个函数:它接受HTTP请求作为输入,并返回HTTP响应。
同样,Circuit Breaker可以被认为是一个函数,输入相同的HTTP请求,返回值如果调用成功则返回HTTP响应,如果失败则返回默认HTTP响应。
因此,使用断路器就像用第二个“断路器”函数组成第一个“调用”函数。

这是一个示例,用于说明如何使用它:

public class Command {
    public Long run() {
        // Does the actual job of making the HTTP request
    }
}

var cb = CircuitBreaker.ofDefaults(
"circuit-breaker");
var result = cb.executeSupplier(new Command()::run);

因为Resilience4J中的每个特征都被建模为一个函数,所以组合这些特征只需要应用上述的函数组合原理。
这相当于面向对象编程中的Decorator模式:目标被“包装”到装饰器对象中。
在这里,我们应用此设计来组成三个函数调用。第一个调用HTTP端点,第二个调用Circuit Breaker,第三个调用,如果调用失败则重试。

var cb = CircuitBreaker.ofDefaults("circuit-breaker");
var retry = Retry.ofDefaults(
"retry");
var cbSupplier = CircuitBreaker.decorateSupplier(cb, new Command()::run);
var retrySupplier = Retry.decorateSupplier(retry, cbSupplier);
var result = retrySupplier.get();

自定义缓存装饰器
使用Resilience4J实现缓存功能,我们的要求是:只有在修饰函数调用失败时才应从缓存返回。设计我们自己的缓存实现功能非常简单。“函数”这个词很重要,因为根据Resilience4J的设计原则,状态 - 缓存 - 应该是外部的并传递给函数以保持其纯净。为了简化实现,缓存将保留一个值,当装饰函数成功返回时,可能会替换该值:

public class Cache<T> {

    private T value;

    static <T> Supplier<T> decorateSupplier(Cache<T> cache, Supplier<T> supplier) {
        return Try.ofSupplier(supplier)
                .fold(
                        throwable -> () -> cache.value,
                        value -> {
                            cache.value = value;
                            return () -> value;
                        });
    }
}

本Try类来自于Vavr库,函数编程API的Java语言和Resilience4J唯一的依赖。它需要两个lambdas:

  • 第一个接受一个 Throwable并返回一个返回结果的函数
  • 第二个接受该值,并返回一个返回结果的函数

请注意,两者都是惰性的:它们不直接返回结果,而是返回Supplier结果。使用此自定义缓存,现在可以装饰Circuit Breaker调用,以便在电路打开时返回缓存值:

var cb = CircuitBreaker.ofDefaults("circuit-breaker");
var cache = new Cache<Long>();
var cbDecorated = CircuitBreaker.decorateSupplier(cb, new Command()::run);
var cacheDecorated = Cache.decorateSupplier(cache, cbDecorated);
cacheDecorated.get();