【译】Reactor 的 Mono 对象的智能缓存

955 阅读4分钟

为Reactor的Mono对象进行巧妙的缓存

发布者::Oleg Varaks in inEnterprise Java August 8th, 2021 0 Views

数据缓存是编程中广泛使用的一种技术。它允许快速检索数据,而无需进行长时间的操作。但是,在对一些长期运行的操作所获取的数据进行缓存时存在一个问题。如果一个缓存值被错过,它将被请求。如果它被一个长期运行的HTTP请求或SQL命令所请求,那么下一次对缓存值的请求可能会导致多次HTTP请求/SQL命令的反复。我一直在寻找一种能够解决使用Project Reactor的项目中的这个问题的缓存实现。Project Reactor是建立在Reactive Streams Specification之上的,这是一个建立反应式应用的标准。你可能知道MonoFlux 对象来自Spring WebFlux。Project Reactor是Spring WebFlux的首选反应式库。

在这篇文章中,我将提出一个反应式缓存的实现,其灵感来自Reactor的addons项目的CacheMono 。我们将假设,一个长期运行的HTTP请求或SQL命令的结果被表示为一个Mono 对象。一个Mono 对象被 "物化 "并以Reactor的 [Signal](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Signal.html)对象的形式进行 "物化 "和缓存,该对象代表一个Mono 。如果一个缓存值被lookup 方法请求,信号会被 "去物质化 "到Mono的。用相同的键进行的多次查找将重新获得相同的Mono 对象,因此,一个长期运行的操作只被触发一次

让我们创建一个带有三个工厂方法的CacheMono

@Slf4j
public class CacheMono<KEY, IVALUE, OVALUE> {

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final Map<KEY, CacheMonoValue<OVALUE>> cache = new HashMap<>();

    /**
     * External value supplier which should be provided if "valuePublisher" with "keyExtractor"
     * are not set
     */
    private final Function<KEY, Mono<OVALUE>> valueSupplier;
    /**
     * External source publisher stream which should be provided if "valueSupplier" is not set
     */
    private final Flux<IVALUE> valuePublisher;
    /**
     * Key extractor for emitted items provided by "valuePublisher"
     */
    private final Function<IVALUE, KEY> keyExtractor;
    /**
     * Value extractor for emitted items provided by "valuePublisher"
     */
    private final Function<IVALUE, OVALUE> valueExtractor;

    private CacheMono(Function<KEY, Mono<OVALUE>> valueSupplier, Flux<IVALUE> valuePublisher,
            Function<IVALUE, KEY> keyExtractor, Function<IVALUE, OVALUE> valueExtractor) {
        this.valueSupplier = valueSupplier;
        this.valuePublisher = valuePublisher;
        this.keyExtractor = keyExtractor;
        this.valueExtractor = valueExtractor;
    }

    /**
     * Factory method to create a CacheMono instance from an external value supplier. The value
     * supplier is called by this CacheMono instance for retrieving values when they are missing
     * in cache ("pull" principle to retrieve not yet cached values).
     */
    public static <KEY, VALUE> CacheMono<KEY, VALUE, VALUE> fromSupplier(
            @NonNull Function<KEY, Mono<VALUE>> valueSupplier) {
        Objects.requireNonNull(valueSupplier);
        return new CacheMono<>(valueSupplier, null, null, null);
    }

    /**
     * Factory method to create a CacheMono instance from an external value publisher.
     * Published values will fill this cache (reactive "push" way).
     */
    public static <KEY, VALUE> CacheMono<KEY, VALUE, VALUE> fromPublisher(
            @NonNull Flux<VALUE> valuePublisher, @NonNull Function<VALUE, KEY> keyExtractor) {
        Objects.requireNonNull(valuePublisher);
        Objects.requireNonNull(keyExtractor);
        return createCacheMono(valuePublisher, keyExtractor, Function.identity());
    }

    /**
     * Factory method to create a CacheMono instance from an external value publisher.
     * Published values will fill this cache (reactive "push" way).
     */
    public static <KEY, IVALUE, OVALUE> CacheMono<KEY, IVALUE, OVALUE> fromPublisher(
            @NonNull Flux<IVALUE> valuePublisher,
            @NonNull Function<IVALUE, KEY> keyExtractor,
            @NonNull Function<IVALUE, OVALUE> valueExtractor) {
        Objects.requireNonNull(valuePublisher);
        Objects.requireNonNull(keyExtractor);
        return createCacheMono(valuePublisher, keyExtractor, valueExtractor);
    }
  
    private static <KEY, IVALUE, OVALUE> CacheMono<KEY, IVALUE, OVALUE> createCacheMono(
            @NonNull Flux<IVALUE> valuePublisher,
            @NonNull Function<IVALUE, KEY> keyExtractor,
            @NonNull Function<IVALUE, OVALUE> valueExtractor) {
        var cacheMono = new CacheMono<>(null, valuePublisher, keyExtractor, valueExtractor);
        valuePublisher.doOnEach(signal -> {
            if (signal.hasValue()) {
                final var inputValue = signal.get();
                final var outputSignal = Signal.next(valueExtractor.apply(inputValue));
                cacheMono.cache.put(keyExtractor.apply(inputValue),
                                    new CacheMonoValue<>(outputSignal));
            } else if (signal.isOnError()) {
                if (signal.getThrowable() == null) {
                    log.error("Error from value publisher");
                } else {
                    log.error("Error from value publisher, message = {}",
                              signal.getThrowable().getMessage());
                }
            }
        }).subscribe();

        return cacheMono;
    }
    
    ...
}

尚未缓存的值将由valueSuppliervaluePublisher 来获取。第一个方法使用 "拉 "原则,第二个方法使用 "推 "原则来检索尚未缓存的值。这意味着,valueSuppliervaluePublisher 以及keyExtractorvalueExtractor 应该被设置。

请记住:如果你从同一个值发布者那里创建了多个CacheMono ,你应该传入一个Flux 流,它可以缓存历史并从一开始就向未来的订阅者发布缓存的项目。这是必要的,因为这个CacheMono 实现会订阅传入的Flux流,以便在源Flux流发布值时自动填充缓存(反应式的 "推 "方式与另一个工厂方法提供的 "拉 "相比)。从现有的流中创建一个这样的Flux ,最简单的方法是对任何Flux 流调用cache() 方法。

正如你所看到的,我们缓存了CacheMonoValue 的实例。这只是一个围绕MonoSignal 的包装器。我们可以把这个类作为一个内部类来实现。

private static class CacheMonoValue<VALUE> {

    private Mono<VALUE> mono;
    private Signal<VALUE> signal;

    CacheMonoValue(Mono<VALUE> mono) {
        this.mono = mono;
    }

    CacheMonoValue(Signal<VALUE> signal) {
        this.signal = signal;
    }

    Mono<VALUE> toMono() {
        if (mono != null) {
            return mono;
        }
        return Mono.justOrEmpty(signal).dematerialize();
    }

    Optional<VALUE> getValue() {
        if (signal == null) {
            return Optional.empty();
        }
        return Optional.ofNullable(signal.get());
    }
}

我们将看到,在一个长期运行的操作中,一个Mono 的值会立即被缓存起来。同一个Mono 实例被检索用于所有后续的相同键的查询。一旦有了Mono 的结果,真正的值就被缓存在同一键下的Signal 。好吧,一步一步来。首先看一下lookup 方法。它使用了一个众所周知的模式:如果值在缓存中被遗漏,switchIfEmpty 操作符中的逻辑将被执行。

/**
 * Finds a value by key in an in-memory cache or load it from a remote source.
 * The loaded value will be cached.
 */
public Mono<OVALUE> lookup(KEY key) {
    return Mono.defer(() -> getValueAsMono(key)
            .switchIfEmpty(Mono.defer(() -> onCacheMissResume(key)))
    );
}

private Mono<OVALUE> getValueAsMono(KEY key) {
    final Lock readLock = lock.readLock();
    readLock.lock();
    try {
        return Mono.justOrEmpty(cache.get(key)).flatMap(CacheMonoValue::toMono);
    } finally {
        readLock.unlock();
    }
}

private Mono<OVALUE> onCacheMissResume(KEY key) {
    final Lock writeLock = lock.writeLock();
    writeLock.lock();
    try {
        // check if value was already cached by another thread
        final var cachedValue = cache.get(key);
        if (cachedValue == null) {
            final Mono<OVALUE> monoValue;
            if (valuePublisher != null) {
                // get value from external value publisher
                monoValue = valuePublisher
                        .filter(value -> Objects.equals(keyExtractor.apply(value), key))
                        .map(valueExtractor)
                        .next();
            } else if (valueSupplier != null) {
                // get value from external supplier
                monoValue = valueSupplier.apply(key);
            } else {
                throw new IllegalStateException("Value can be not determined," +
                        "neither valuePublisher nor valueSupplier were set");
            }
            // cache Mono as value immediately
            cache.put(key, new CacheMonoValue<>(monoValue));

            // cache success and error values encapsulated in signal when it is available
            return monoValue.doOnEach(signal -> {
                if (signal.isOnNext()) {
                    cache.put(key, new CacheMonoValue<>(
                      Signal.next(Objects.requireNonNull(signal.get())))
                    );
                } else if (signal.isOnError()) {
                    final Signal<OVALUE> errorSignal;
                    if (signal.getThrowable() == null) {
                        errorSignal = Signal.error(
                          new Throwable("Getting value from external provider failed"));
                    } else {
                        errorSignal = Signal.error(signal.getThrowable());
                    }
                    cache.put(key, new CacheMonoValue<>(errorSignal));
                }
            });
        }
        return Mono.justOrEmpty(cachedValue).flatMap(CacheMonoValue::toMono);
    } finally {
        writeLock.unlock();
    }
}

onCacheMissResume ,漏掉的值将被上面提到的valueSuppliervaluePublisher 重新获取。正如我所说的,该值会立即被缓存为一个Mono 对象,并在随后的所有查找中被返回。一旦长期运行的操作的值可用,monoValue.doOnEach(...) 内的逻辑就会被执行。该值被封装在Signal ,可以通过调用signal.get() 来返回。

让我们也来实现一些方便的方法。特别是那些从缓存中返回已经存在的(缓存的)值的方法。

/**
 * Gets cached values as Java Stream. Returned stream is not sorted.
 */
public Stream<OVALUE> getValues() {
    final Lock readLock = lock.readLock();
    readLock.lock();
    try {
        return cache.values().stream().flatMap(cachedValue -> cachedValue.getValue().stream());
    } finally {
        readLock.unlock();
    }
}

/**
 * Gets cached value as Java Optional.
 */
public Optional<OVALUE> getValue(KEY key) {
    final Lock readLock = lock.readLock();
    readLock.lock();
    try {
        return Optional.ofNullable(cache.get(key)).flatMap(CacheMonoValue::getValue);
    } finally {
        readLock.unlock();
    }
}

/**
 * Removes the mapping for a key from this map if it is present.
 */
public void remove(KEY key) {
    final Lock writeLock = lock.writeLock();
    writeLock.lock();
    try {
        cache.remove(key);
    } finally {
        writeLock.unlock();
    }
}

CacheMono 类的用法很简单。只需从我目前的项目中拿出两个代码片段。第一段是通过调用CacheMono.fromSupplier 来创建一个CacheMono 实例。

@Service
@Slf4j
@RequiredArgsConstructor
public class TopologyRepository {

    private final CacheMono<TopologyRef, TopologyDto, TopologyDto> cache;
    private final TopologyLoader topologyLoader;
    private final TopologyCreator topologyCreator;

    @Autowired
    public UnoTopologyRepository(TopologyLoader topologyLoader,
                                 TopologyCreator topologyCreator) {
        this.topologyLoader = topologyLoader;
        this.topologyCreator = topologyCreator;
        cache = CacheMono.fromSupplier(this::retrieveTopology);
    }

    /**
     * Finds a topology from this repository by reference.
     */
    public Mono<TopologyDto> findUnoTopology(TopologyRef topologyRef) {
        return cache.lookup(topologyRef)
                .doOnNext(topology ->
                          log.info("Topology was found by lookup with key {}", topologyRef))
                .onErrorResume(err -> {
                    log.error("Error on lookup Topology by key {}, message: {}",
                              topologyRef, err.getMessage());
                    return Mono.empty();
                });
    }

    private Mono<TopologyDto> retrieveTopology(TopologyRef topologyRef) {
        CompletableFuture<UnoTopologyDto> future = CompletableFuture.supplyAsync(() -> {
            final var loaderContext = topologyLoader.retrieveTopology(topologyRef);
            return topologyCreator.createTopology(loaderContext);
        });
        return Mono.fromFuture(future);
    }
}

第二段是通过调用CacheMono.fromPublisher 来创建一个CacheMono 实例。

@Service
@Slf4j
@RequiredArgsConstructor
public class SspDefinitionenStore implements SspDefinitionConsumer {

    private CacheMono>VersionedId, SspDefinition, SspDefinition> sspDefinitionCache;
    private FluxSink>SspDefinition> sspDefinitionSink;

    @PostConstruct
    public void initialize() {
        sspDefinitionCache = CacheMono.fromPublisher(
                Flux.create(sink -> sspDefinitionSink = sink),
                SspDefinition::getId);
    }

    @Override
    public void accept(SspDefinition sspDefinition) {
        sspDefinitionSink.next(sspDefinition);
    }

    public Mono>SspDefinition> lookupSspDefinition(VersionedId sspId) {
        return sspDefinitionCache.lookup(sspId)
                .doOnNext(sspTopology -> log.info(
                    "SspDefinition was found by lookup with key {}", sspId))
                .onErrorResume(err -> {
                    log.error("Error on lookup SspDefinition by key {}, message: {}",
                              sspId, err.getMessage());
                    return Mono.empty();
                });
    }

    public Optional>SspDefinition> findSspDefinition(VersionedId sspId) {
        return sspDefinitionCache.getValue(sspId);
    }

    public Flux>SspDefinition> findSspDefinitions() {
        return Flux.fromStream(sspDefinitionCache.getValues().filter(Objects::nonNull));
    }

    ...
}

这就是全部。祝你玩得开心!

由我们JCG项目的合伙人Oleg Varaksin授权发表在Java Code Geeks上。点击这里查看原文。为Reactor的Mono对象进行巧妙的缓存

Java Code Geeks撰稿人所表达的观点仅代表其本人。

2021-08-08

Oleg Varaksin