为Reactor的Mono对象进行巧妙的缓存
发布者::Oleg Varaks in inEnterprise Java August 8th, 2021 0 Views
数据缓存是编程中广泛使用的一种技术。它允许快速检索数据,而无需进行长时间的操作。但是,在对一些长期运行的操作所获取的数据进行缓存时存在一个问题。如果一个缓存值被错过,它将被请求。如果它被一个长期运行的HTTP请求或SQL命令所请求,那么下一次对缓存值的请求可能会导致多次HTTP请求/SQL命令的反复。我一直在寻找一种能够解决使用Project Reactor的项目中的这个问题的缓存实现。Project Reactor是建立在Reactive Streams Specification之上的,这是一个建立反应式应用的标准。你可能知道Mono 和Flux 对象来自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;
}
...
}
尚未缓存的值将由valueSupplier 或valuePublisher 来获取。第一个方法使用 "拉 "原则,第二个方法使用 "推 "原则来检索尚未缓存的值。这意味着,valueSupplier 或valuePublisher 以及keyExtractor 和valueExtractor 应该被设置。
请记住:如果你从同一个值发布者那里创建了多个CacheMono ,你应该传入一个Flux 流,它可以缓存历史并从一开始就向未来的订阅者发布缓存的项目。这是必要的,因为这个CacheMono 实现会订阅传入的Flux流,以便在源Flux流发布值时自动填充缓存(反应式的 "推 "方式与另一个工厂方法提供的 "拉 "相比)。从现有的流中创建一个这样的Flux ,最简单的方法是对任何Flux 流调用cache() 方法。
正如你所看到的,我们缓存了CacheMonoValue 的实例。这只是一个围绕Mono 或Signal 的包装器。我们可以把这个类作为一个内部类来实现。
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 ,漏掉的值将被上面提到的valueSupplier 或valuePublisher 重新获取。正如我所说的,该值会立即被缓存为一个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
