Skip to content

feat: add RedissonLocalScoredSortedSet with local read cache + cross-instance sync for high-frequency ZSET reads#7104

Open
lehuuthanh5 wants to merge 13 commits into
redisson:masterfrom
lehuuthanh5:redissonlocalscoresortedset
Open

feat: add RedissonLocalScoredSortedSet with local read cache + cross-instance sync for high-frequency ZSET reads#7104
lehuuthanh5 wants to merge 13 commits into
redisson:masterfrom
lehuuthanh5:redissonlocalscoresortedset

Conversation

@lehuuthanh5

Copy link
Copy Markdown
Contributor

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:

  • Local in-memory cache for scored sorted set data:
    • value -> score map (cache)
    • score -> ordered values map (scoreCache)
  • Supports StoreMode.LOCALCACHE and StoreMode.LOCALCACHE_REDIS
  • Supports ReadMode.LOCALCACHE and ReadMode.REDIS
  • Cache providers:
  • Redisson map implementations
  • Caffeine
  • Eviction/TTL/maxIdle support via LocalScoreSortedSetOptions
  • Pub/Sub synchronization between JVM instances using LocalCachedMapUpdate
  • Preload/reload support from Redis (preloadCache, ReconnectionStrategy.PRE_LOAD)
  • Lifecycle cleanup (destroy) to remove listeners and clear local structures
  • Consistency and invalidation behavior
  • Mutations update local cache and publish invalidation/update messages.
  • Peer instances consume messages and update their local cache.

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.

@mrniko mrniko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mrniko mrniko added the feature label May 5, 2026
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]>
Comment thread redisson/src/test/java/org/redisson/RedissonLocalScoredSortedSetTest.java Outdated
use RLocalScoredSortedSet interface only for testing

Signed-off-by: Le Thanh <[email protected]>
Comment thread redisson/src/test/java/org/redisson/RedissonLocalScoredSortedSetTest.java Outdated
Comment thread redisson/src/test/java/org/redisson/RedissonLocalScoredSortedSetTest.java Outdated
Comment thread redisson/src/main/java/org/redisson/api/RedissonClient.java Outdated
Comment thread redisson/src/main/java/org/redisson/api/RLocalScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/api/RLocalScoredSortedSetReactive.java Outdated
Comment thread redisson/src/main/java/org/redisson/api/RLocalScoredSortedSetRx.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
…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]>
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/api/RLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/api/RLocalCachedScoredSortedSet.java Outdated
Comment thread redisson/src/main/java/org/redisson/RedissonLocalCachedScoredSortedSet.java Outdated
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]>
Comment thread redisson/src/main/java/org/redisson/RedissonReactive.java
@Override
public RFuture<Boolean> addAsync(double score, V value) {
boolean result = addCache(score, value);
broadcastUpdate(encode(value), encode(score));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't cast to params

*
* @return cached scores mapped to values
*/
ConcurrentMap<Double, ConcurrentSkipListSet<CachedSortedSetEntry<V>>> getScoreCache();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need this exposed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

*
* @return cached values mapped to scores
*/
Map<V, Double> getCache();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about storeCacheMiss, useObjectAsCacheKey, useTopicPattern, expirationEventPolicy settings like in RLocalCachedMap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@mrniko

mrniko commented May 28, 2026

Copy link
Copy Markdown
Member

Any updates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

2 participants