feat: add RedissonLocalScoredSortedSet with local read cache + cross-instance sync for high-frequency ZSET reads#7104
Conversation
Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
mrniko
left a comment
There was a problem hiding this comment.
Add RLocalScoredSortedSet interface and interfaces for Rx and Reactive.
Add methods to RedissonClient, RedissonRxClient, RedissonReactiveClient.
No new RedissonLocalScoredSortedSet() calls in tests.
Use same approach as for RLocalCachedMap.
Add RLocalScoredSortedSet interface and interfaces for Rx and Reactive. Add methods to RedissonClient, RedissonRxClient, RedissonReactiveClient. No new RedissonLocalScoredSortedSet() calls in tests. Use same approach as for RLocalCachedMap. Signed-off-by: Le Thanh <[email protected]>
use RLocalScoredSortedSet interface only for testing Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
…e.REDIS and NoOp cache leak - switch remove-sync signaling in RedissonLocalCachedScoredSortedSet.java from Dummy-string LocalCachedMapUpdate to LocalCachedScoreSortedSetInvalidate - add codec support for scored-set invalidation messages in LocalCachedMessageCodec.java - optimize score-bucket comparator path by introducing CacheValue with pre-encoded value bytes to avoid re-encoding on each skip-list compare - store score inside CacheValue and build rank-based entryRange* results from snapshot entries to remove the scoreCache traversal vs cache.get(v) race - prevent unbounded scoreCache growth when cacheSize == -1 (NoOpCacheMap) by only process addCache when the cache is not NoOpCacheMap Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
Fix addAsync return false if only score changed Change The Reactive and Rx interfaces' getScoreCache() return types Remove api depends on implementation Fix O(N) under removeCache method Signed-off-by: Le Thanh <[email protected]>
Signed-off-by: Le Thanh <[email protected]>
| @Override | ||
| public RFuture<Boolean> addAsync(double score, V value) { | ||
| boolean result = addCache(score, value); | ||
| broadcastUpdate(encode(value), encode(score)); |
There was a problem hiding this comment.
addAsync(double, V) updates local cache & broadcast before Redis confirms.
| for (Map.Entry<Double, ConcurrentSkipListSet<CachedSortedSetEntry<V>>> entry : scoreCache.headMap(score, false).entrySet()) { | ||
| rank += entry.getValue().size(); | ||
| } | ||
| ConcurrentSkipListSet<CachedSortedSetEntry<V>> sameScore = scoreCache.get(score); |
There was a problem hiding this comment.
For a set of N elements, computing the rank of a middle item walks ~N/2 buckets. Redis ZRANK is O(log N). The whole point of the cache is read speedup, but rank, revRank, addAndGetRank, addAndGetRevRank, and the valueRange(int, int) variants (which call getCacheValuesByRankRange → linear iteration) are slower than the Redis baseline once the set gets large.
| Object score = codec.getMapValueDecoder().decode(scoreBuf, null); | ||
| addCache(((Number) score).doubleValue(), (V) value); | ||
| } catch (IOException e) { | ||
| // ignore decode errors |
There was a problem hiding this comment.
Decode failures here mean either codec mismatch between peers or corrupted message.
| * Creates a set with preload=true so the cache is populated from Redis on construction. | ||
| */ | ||
| private RLocalCachedScoredSortedSet<String> createPreloadSet(String name) { | ||
| LocalCachedScoredSortedSetParams<String> params = |
| * | ||
| * @return cached scores mapped to values | ||
| */ | ||
| ConcurrentMap<Double, ConcurrentSkipListSet<CachedSortedSetEntry<V>>> getScoreCache(); |
| * | ||
| * @return cached values mapped to scores | ||
| */ | ||
| Map<V, Double> getCache(); |
There was a problem hiding this comment.
returns the live underlying Map<V, Double>. External callers can mutate it and silently corrupt the parallel scoreCache. Should be Collections.unmodifiableMap(cache).
| * @param <V> value type | ||
| * @author Nikita Koksharov | ||
| */ | ||
| public interface LocalCachedScoredSortedSetOptions<V> extends CodecOptions<LocalCachedScoredSortedSetOptions<V>, Codec> { |
There was a problem hiding this comment.
what about storeCacheMiss, useObjectAsCacheKey, useTopicPattern, expirationEventPolicy settings like in RLocalCachedMap?
There was a problem hiding this comment.
I don't use CacheKey here, so we don't have useObjectAsCacheKey
For useTopicPattern, expirationEventPolicy. I don't know the purpose, so I don't add them.
For storeCacheMiss, you can see I just introduced only SyncStrategy UPDATE, so I also ignore this key
If the config is must-have or nice-to-have, I will investigate more about them based on RLocalCachedMap and add them
|
Any updates? |
Why
In workloads with very frequent scored-sorted-set reads (range/score), using RedissonScoredSortedSet can hit Redis for every read.
In our production usage, this pattern increases latency and Redis CPU pressure.
This PR introduces RedissonLocalScoredSortedSet to reduce read amplification by serving reads from an in-process cache while keeping cache updates synchronized across instances.
What is added
New class: redisson/src/main/java/org/redisson/RedissonLocalScoredSortedSet.java
Key capabilities:
Performance intent
The goal is to improve performance for read-heavy workloads by reducing Redis reads and network round-trips for hot data, which should lower latency and reduce Redis CPU utilization.