Collections
Map¶
Java implementation of Valkey or Redis based Map object for Java implements ConcurrentMap interface. This object is thread-safe. Consider to use Live Object service to store POJO object as Valkey or Redis Map. Valkey or Redis uses serialized state to check key uniqueness instead of key's hashCode()/equals() methods.
If Map used mostly for read operations and/or network roundtrips are undesirable use Map with Local cache support.
Code examples:
RMap<String, SomeObject> map = redisson.getMap("anyMap");
SomeObject prevObject = map.put("123", new SomeObject());
SomeObject currentObject = map.putIfAbsent("323", new SomeObject());
SomeObject obj = map.remove("123");
// use fast* methods when previous value is not required
map.fastPut("a", new SomeObject());
map.fastPutIfAbsent("d", new SomeObject());
map.fastRemove("b");
RFuture<SomeObject> putAsyncFuture = map.putAsync("321");
RFuture<Void> fastPutAsyncFuture = map.fastPutAsync("321");
map.fastPutAsync("321", new SomeObject());
map.fastRemoveAsync("321");
RMap<MyKey, MyValue> map = redisson.getMap("anyMap");
MyKey k = new MyKey();
RLock keyLock = map.getLock(k);
keyLock.lock();
try {
MyValue v = map.get(k);
// process value ...
} finally {
keyLock.unlock();
}
RReadWriteLock rwLock = map.getReadWriteLock(k);
rwLock.readLock().lock();
try {
MyValue v = map.get(k);
// process value ...
} finally {
keyLock.readLock().unlock();
}
Eviction, local cache and data partitioning¶
Redisson provides various Map structure implementations with multiple important features:
local cache - so called near cache used to speed up read operations and avoid network roundtrips. It caches Map entries on Redisson side and executes read operations up to 45x faster in comparison with common implementation. Local cache instances with the same name connected to the same pub/sub channel. This channel is used for exchanging of update/invalidate events between all instances. Local cache store doesn't use hashCode()/equals() methods of key object, instead it uses hash of serialized state. It's recommended to use each local cached instance as a singleton per unique name since it has own state for local cache.
data partitioning - although any Map object is cluster compatible its content isn't scaled/partitioned across multiple master nodes in cluster. Data partitioning allows to scale available memory, read/write operations and entry eviction process for individual Map instance in cluster.
1. No eviction
Each object implements RMap, Async, Reactive and RxJava3 interfaces.
Available implementations:
| RedissonClient method name |
Local cache |
Data partitioning |
Ultra-fast read/write |
|---|---|---|---|
| getMap() open-source version |
❌ | ❌ | ❌ |
| getLocalCachedMap() open-source version |
✔️ | ❌ | ❌ |
| getMap() Redisson PRO version |
❌ | ❌ | ✔️ |
| getLocalCachedMap() Redisson PRO version |
✔️ | ❌ | ✔️ |
| getClusteredMap() available only in Redisson PRO |
❌ | ✔️ | ✔️ |
| getClusteredLocalCachedMap() available only in Redisson PRO |
✔️ | ✔️ | ✔️ |
2. Scripted eviction
Allows to define time to live or max idle time parameters per map entry. Eviction is done on Redisson side through a custom scheduled task which removes expired entries using Lua script. Eviction task is started once per unique object name at the moment of getting Map instance. If instance isn't used and has expired entries it should be get again to start the eviction process. This leads to extra Valkey or Redis calls and eviction task per unique map object name.
Entries are cleaned time to time by org.redisson.eviction.EvictionScheduler. By default, it removes 100 expired entries at a time. This can be changed through cleanUpKeysAmount setting. Task launch time tuned automatically and depends on expired entries amount deleted in previous time and varies between 5 second to 30 minutes by default. This time interval can be changed through minCleanUpDelay and maxCleanUpDelay. For example, if clean task deletes 100 entries each time it will be executed every 5 seconds (minimum execution delay). But if current expired entries amount is lower than previous one then execution delay will be increased by 1.5 times and decreased otherwise.
Each object implements RMapCache, Async, Reactive and RxJava3 interfaces.
Available implementations:
| RedissonClient method name |
Local cache |
Data partitioning |
Ultra-fast read/write |
|---|---|---|---|
| getMapCache() open-source version |
❌ | ❌ | ❌ |
| getMapCache() Redisson PRO version |
❌ | ❌ | ✔️ |
| getLocalCachedMapCache() available only in Redisson PRO |
✔️ | ❌ | ✔️ |
| getClusteredMapCache() available only in Redisson PRO |
❌ | ✔️ | ✔️ |
| getClusteredLocalCachedMapCache() available only in Redisson PRO |
✔️ | ✔️ | ✔️ |
3. Advanced eviction
Allows to define time to live parameter per map entry. Doesn't use an entry eviction task, entries are cleaned on Valkey or Redis side.
Each object implements RMapCacheV2, Async, Reactive and RxJava3 interfaces.
Available implementations:
| RedissonClient method name |
Local cache |
Data partitioning |
Ultra-fast read/write |
|---|---|---|---|
| getMapCacheV2() available only in Redisson PRO |
❌ | ✔️ | ✔️ |
| getLocalCachedMapCacheV2() available only in Redisson PRO |
✔️ | ✔️ | ✔️ |
4. Native eviction
Allows to define time to live parameter per map entry. Doesn't use an entry eviction task, entries are cleaned on Valkey or Redis side.
Requires Valkey 9.0+ or Redis 7.4+.
Each object implements RMapCacheNative, Async, Reactive and RxJava3 interfaces.
Available implementations:
| RedissonClient method name |
Local cache |
Data partitioning |
Ultra-fast read/write |
Size limit |
|---|---|---|---|---|
| getMapCacheNative() open-source version |
❌ | ❌ | ❌ | ❌ |
| getMapCacheNative() Redisson PRO version |
❌ | ❌ | ✔️ | ❌ |
| getLocalCachedMapCacheNative() available only in Redisson PRO |
✔️ | ❌ | ✔️ | ❌ |
| getClusteredMapCacheNative() available only in Redisson PRO |
❌ | ✔️ | ✔️ | ❌ |
| getClusteredLocalCachedMapCacheNative() available only in Redisson PRO |
✔️ | ✔️ | ✔️ | ❌ |
| getMapCacheNativeV2() available only in Redisson PRO |
❌ | ❌ | ✔️ | ✔️ |
| getLocalCachedMapCacheNativeV2() available only in Redisson PRO |
✔️ | ❌ | ✔️ | ✔️ |
| getClusteredMapCacheNativeV2() available only in Redisson PRO |
❌ | ✔️ | ✔️ | ✔️ |
| getClusteredLocalCachedMapCacheNativeV2() available only in Redisson PRO |
✔️ | ✔️ | ✔️ | ✔️ |
Redisson also provides various Cache API implementations.
Code example:
RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap");
// or
RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap", MapCacheOptions.defaults());
// or
RMapCacheV2<String, SomeObject> map = redisson.getMapCacheV2("anyMap");
// or
RMapCacheV2<String, SomeObject> map = redisson.getMapCacheV2("anyMap", MapOptions.defaults());
// or
RMapCache<String, SomeObject> map = redisson.getClusteredMapCache("anyMap");
// or
RMapCache<String, SomeObject> map = redisson.getClusteredMapCache("anyMap", MapCacheOptions.defaults());
// ttl = 10 minutes,
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES);
// ttl = 10 minutes, maxIdleTime = 10 seconds
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES, 10, TimeUnit.SECONDS);
// ttl = 3 seconds
map.putIfAbsent("key2", new SomeObject(), 3, TimeUnit.SECONDS);
// ttl = 40 seconds, maxIdleTime = 10 seconds
map.putIfAbsent("key2", new SomeObject(), 40, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);
// if object is not used anymore
map.destroy();
Local cache
Map object with local cache support implements RLocalCachedMap or RLocalCachedMapCache which extends ConcurrentMap interface. This object is thread-safe.
Follow options can be supplied during object creation:
LocalCachedMapOptions options = LocalCachedMapOptions.defaults()
// Defines whether to store a cache miss into the local cache.
// Default value is false.
.storeCacheMiss(false);
// Defines store mode of cache data.
// Follow options are available:
// LOCALCACHE - store data in local cache only and use Valkey or Redis only for data update/invalidation.
// LOCALCACHE_REDIS - store data in both Valkey or Redis and local cache.
.storeMode(StoreMode.LOCALCACHE_REDIS)
// Defines Cache provider used as local cache store.
// Follow options are available:
// REDISSON - uses Redisson own implementation
// CAFFEINE - uses Caffeine implementation
.cacheProvider(CacheProvider.REDISSON)
// Defines local cache eviction policy.
// Follow options are available:
// LFU - Counts how often an item was requested. Those that are used least often are discarded first.
// LRU - Discards the least recently used items first
// SOFT - Uses soft references, entries are removed by GC
// WEAK - Uses weak references, entries are removed by GC
// NONE - No eviction
.evictionPolicy(EvictionPolicy.NONE)
// If cache size is 0 then local cache is unbounded.
.cacheSize(1000)
// Defines strategy for load missed local cache updates after connection failure.
//
// Follow reconnection strategies are available:
// CLEAR - Clear local cache if map instance has been disconnected for a while.
// LOAD - Store invalidated entry hash in invalidation log for 10 minutes
// Cache keys for stored invalidated entry hashes will be removed
// if LocalCachedMap instance has been disconnected less than 10 minutes
// or whole cache will be cleaned otherwise.
// NONE - Default. No reconnection handling
.reconnectionStrategy(ReconnectionStrategy.NONE)
// Defines local cache synchronization strategy.
//
// Follow sync strategies are available:
// INVALIDATE - Default. Invalidate cache entry across all LocalCachedMap instances on map entry change
// UPDATE - Insert/update cache entry across all LocalCachedMap instances on map entry change
// NONE - No synchronizations on map changes
.syncStrategy(SyncStrategy.INVALIDATE)
// time to live for each entry in local cache
.timeToLive(Duration.ofSeconds(10))
// max idle time for each map entry in local cache
.maxIdle(Duration.ofSeconds(10))
// Defines how to listen expired event sent by Valkey or Redis upon this instance deletion
//
// Follow expiration policies are available:
// DONT_SUBSCRIBE - Don't subscribe on expire event
// SUBSCRIBE_WITH_KEYEVENT_PATTERN - Subscribe on expire event using `__keyevent@*:expired` pattern
// SUBSCRIBE_WITH_KEYSPACE_CHANNEL - Subscribe on expire event using `__keyspace@N__:name` channel
.expirationEventPolicy(ExpirationEventPolicy.SUBSCRIBE_WITH_KEYEVENT_PATTERN);
Warning
It's recommended to use a single instance of local cached Map instance per unique name for each Redisson instance. Same LocalCachedMapOptions object should be used across all instances with the same name.
Code example:
RLocalCachedMap<String, Integer> map = redisson.getLocalCachedMap("test", LocalCachedMapOptions.defaults());
// or
RLocalCachedMap<String, SomeObject> map = redisson.getLocalCachedMapCache("anyMap", LocalCachedMapCacheOptions.defaults());
// or
RLocalCachedMap<String, SomeObject> map = redisson.getClusteredLocalCachedMapCache("anyMap", LocalCachedMapCacheOptions.defaults());
// or
RLocalCachedMap<String, SomeObject> map = redisson.getClusteredLocalCachedMap("anyMap", LocalCachedMapOptions.defaults());
String prevObject = map.put("123", 1);
String currentObject = map.putIfAbsent("323", 2);
String obj = map.remove("123");
// use fast* methods when previous value is not required
map.fastPut("a", 1);
map.fastPutIfAbsent("d", 32);
map.fastRemove("b");
RFuture<String> putAsyncFuture = map.putAsync("321");
RFuture<Void> fastPutAsyncFuture = map.fastPutAsync("321");
map.fastPutAsync("321", new SomeObject());
map.fastRemoveAsync("321");
Object should be destroyed if it not used anymore, but it's not necessary to call destroy method if Redisson goes shutdown.
How to load data and avoid invalidation messages traffic.
Code example:
public void loadData(String cacheName, Map<String, String> data) {
RLocalCachedMap<String, String> clearMap = redisson.getLocalCachedMap(cacheName,
LocalCachedMapOptions.defaults().cacheSize(1).syncStrategy(SyncStrategy.INVALIDATE));
RLocalCachedMap<String, String> loadMap = redisson.getLocalCachedMap(cacheName,
LocalCachedMapOptions.defaults().cacheSize(1).syncStrategy(SyncStrategy.NONE));
loadMap.putAll(data);
clearMap.clearLocalCache();
}
Data partitioning
Map object with data partitioning support implements org.redisson.api.RClusteredMap which extends java.util.concurrent.ConcurrentMap interface. Read more details about data partitioning here.
Code example:
RClusteredMap<String, SomeObject> map = redisson.getClusteredMap("anyMap");
// or
RClusteredMap<String, SomeObject> map = redisson.getClusteredLocalCachedMapCache("anyMap", LocalCachedMapCacheOptions.defaults());
// or
RClusteredMap<String, SomeObject> map = redisson.getClusteredLocalCachedMap("anyMap", LocalCachedMapOptions.defaults());
// or
RClusteredMap<String, SomeObject> map = redisson.getClusteredMapCache("anyMap");
SomeObject prevObject = map.put("123", new SomeObject());
SomeObject currentObject = map.putIfAbsent("323", new SomeObject());
SomeObject obj = map.remove("123");
map.fastPut("321", new SomeObject());
map.fastRemove("321");
Persistence¶
Redisson allows to store Map data in external storage along with Valkey or Redis store.
Use cases:
- Redisson Map object as a cache between an application and external storage.
- Increase durability of Redisson Map data and life-span of evicted entries.
- Caching for databases, web services or any other data source.
Read-through strategy
If requested entry doesn't exist in the Redisson Map object when it will be loaded using provided MapLoader object. Code example:
MapLoader<String, String> mapLoader = new MapLoader<String, String>() {
@Override
public Iterable<String> loadAllKeys() {
List<String> list = new ArrayList<String>();
Statement statement = conn.createStatement();
try {
ResultSet result = statement.executeQuery("SELECT id FROM student");
while (result.next()) {
list.add(result.getString(1));
}
} finally {
statement.close();
}
return list;
}
@Override
public String load(String key) {
PreparedStatement preparedStatement = conn.prepareStatement("SELECT name FROM student where id = ?");
try {
preparedStatement.setString(1, key);
ResultSet result = preparedStatement.executeQuery();
if (result.next()) {
return result.getString(1);
}
return null;
} finally {
preparedStatement.close();
}
}
};
MapOptions<K, V> options = MapOptions.<K, V>defaults()
.loader(mapLoader);
MapCacheOptions<K, V> mcoptions = MapCacheOptions.<K, V>defaults()
.loader(mapLoader);
RMap<K, V> map = redisson.getMap("test", options);
// or
RMapCache<K, V> map = redisson.getMapCache("test", mcoptions);
// or with performance boost up to 45x times
RLocalCachedMap<K, V> map = redisson.getLocalCachedMap("test", options);
// or with performance boost up to 45x times
RLocalCachedMapCache<K, V> map = redisson.getLocalCachedMapCache("test", mcoptions);
Write-through (synchronous) strategy
When the Map entry is being updated method won't return until Redisson update it in an external storage using MapWriter object. Code example:
MapWriter<String, String> mapWriter = new MapWriter<String, String>() {
@Override
public void write(Map<String, String> map) {
PreparedStatement preparedStatement = conn.prepareStatement("INSERT INTO student (id, name) values (?, ?)");
try {
for (Entry<String, String> entry : map.entrySet()) {
preparedStatement.setString(1, entry.getKey());
preparedStatement.setString(2, entry.getValue());
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
} finally {
preparedStatement.close();
}
}
@Override
public void delete(Collection<String> keys) {
PreparedStatement preparedStatement = conn.prepareStatement("DELETE FROM student where id = ?");
try {
for (String key : keys) {
preparedStatement.setString(1, key);
preparedStatement.addBatch();
}
preparedStatement.executeBatch();
} finally {
preparedStatement.close();
}
}
};
MapOptions<K, V> options = MapOptions.<K, V>defaults()
.writer(mapWriter)
.writeMode(WriteMode.WRITE_THROUGH);
MapCacheOptions<K, V> mcoptions = MapCacheOptions.<K, V>defaults()
.writer(mapWriter)
.writeMode(WriteMode.WRITE_THROUGH);
RMap<K, V> map = redisson.getMap("test", options);
// or
RMapCache<K, V> map = redisson.getMapCache("test", mcoptions);
// or with performance boost up to 45x times
RLocalCachedMap<K, V> map = redisson.getLocalCachedMap("test", options);
// or with performance boost up to 45x times
RLocalCachedMapCache<K, V> map = redisson.getLocalCachedMapCache("test", mcoptions);
Write-behind (asynchronous) strategy
Updates of Map object are accumulated in batches and asynchronously written with defined delay to external storage through MapWriter object.
writeBehindDelay - delay of batched write or delete operation. Default value is 1000 milliseconds.
writeBehindBatchSize - size of batch. Each batch contains Map Entry write or delete commands. Default value is 50.
Configuration example:
MapOptions<K, V> options = MapOptions.<K, V>defaults()
.writer(mapWriter)
.writeMode(WriteMode.WRITE_BEHIND)
.writeBehindDelay(5000)
.writeBehindBatchSize(100);
MapCacheOptions<K, V> mcoptions = MapCacheOptions.<K, V>defaults()
.writer(mapWriter)
.writeMode(WriteMode.WRITE_BEHIND)
.writeBehindDelay(5000)
.writeBehindBatchSize(100);
RMap<K, V> map = redisson.getMap("test", options);
// or
RMapCache<K, V> map = redisson.getMapCache("test", mcoptions);
// or with performance boost up to 45x times
RLocalCachedMap<K, V> map = redisson.getLocalCachedMap("test", options);
// or with performance boost up to 45x times
RLocalCachedMapCache<K, V> map = redisson.getLocalCachedMapCache("test", mcoptions);
This feature available for RMap, RMapCache, RLocalCachedMap and RLocalCachedMapCache objects.
Usage of RLocalCachedMap and RLocalCachedMapCache objects boost Valkey or Redis read-operations up to 45x times and give almost instant speed for database, web service or any other data source.
Listeners¶
Redisson allows binding listeners per RMap object. This requires the notify-keyspace-events setting to be enabled on Valkey or Redis side.
RMap object allows to track follow events over the data.
| Listener class name | Event description | Valkey or Redisnotify-keyspace-events value |
|---|---|---|
| org.redisson.api.listener.TrackingListener | Entry created/removed/updated after read operation | - |
| org.redisson.api.listener.MapPutListener | Entry created/updated | Eh or Th |
| org.redisson.api.listener.MapRemoveListener | Entry removed | Eh or Th |
| org.redisson.api.ExpiredObjectListener | RMap object expired |
Ex |
| org.redisson.api.DeletedObjectListener | RMap object deleted |
Eg |
Usage examples:
RMap<String, SomeObject> map = redisson.getMap("anyMap");
int listenerId = map.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
// ...
}
});
int listenerId = map.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
// ...
}
});
int listenerId = map.addListener(new MapPutListener() {
@Override
public void onPut(String name, String fieldName) {
// ...
}
});
int listenerId = map.addListener(new MapRemoveListener() {
@Override
public void onRemove(String name, String fieldName) {
// ...
}
});
map.removeListener(listenerId);
RMapCache object allows to track additional events over the data.
| Listener class name | Event description |
|---|---|
| org.redisson.api.map.event.EntryCreatedListener | Entry created |
| org.redisson.api.map.event.EntryExpiredListener | Entry expired |
| org.redisson.api.map.event.EntryRemovedListener | Entry removed |
| org.redisson.api.map.event.EntryUpdatedListener | Entry updated |
Important
For optimization purposes, RMapCache entry events are emitted only when there are registered listeners. This means that listener registration affects the internal map state.
Usage examples:
RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap");
// or
RMapCache<String, SomeObject> map = redisson.getLocalCachedMapCache(LocalCachedMapCacheOptions.name("anyMap"));
// or
RMapCache<String, SomeObject> map = redisson.getClusteredLocalCachedMapCache("anyMap", LocalCachedMapOptions.defaults());
// or
RMapCache<String, SomeObject> map = redisson.getClusteredMapCache("anyMap");
int listenerId = map.addListener(new EntryUpdatedListener<Integer, Integer>() {
@Override
public void onUpdated(EntryEvent<Integer, Integer> event) {
event.getKey(); // key
event.getValue() // new value
event.getOldValue() // old value
// ...
}
});
int listenerId = map.addListener(new EntryCreatedListener<Integer, Integer>() {
@Override
public void onCreated(EntryEvent<Integer, Integer> event) {
event.getKey(); // key
event.getValue() // value
// ...
}
});
int listenerId = map.addListener(new EntryExpiredListener<Integer, Integer>() {
@Override
public void onExpired(EntryEvent<Integer, Integer> event) {
event.getKey(); // key
event.getValue() // value
// ...
}
});
int listenerId = map.addListener(new EntryRemovedListener<Integer, Integer>() {
@Override
public void onRemoved(EntryEvent<Integer, Integer> event) {
event.getKey(); // key
event.getValue() // value
// ...
}
});
map.removeListener(listenerId);
LRU/LFU bounded Map¶
Map object which implements RMapCache or RMapCacheNativeV2 interface can be bounded using Least Recently Used (LRU) or Least Frequently Used (LFU) order. Bounded Map allows to store map entries within defined limit and retire entries in defined order.
The size limit is applied either by the number of entries or the number of bytes stored in the Map object.
Available implementations:
| RedissonClient method name |
Local cache |
Data partitioning |
Ultra-fast read/write |
Size limit | Native eviction |
|---|---|---|---|---|---|
| getMapCache() open-source version |
❌ | ❌ | ❌ | ✔️ | ❌ |
| getMapCache() Redisson PRO version |
❌ | ❌ | ✔️ | ✔️ | ❌ |
| getLocalCachedMapCache() available only in Redisson PRO |
✔️ | ❌ | ✔️ | ✔️ | ❌ |
| getClusteredMapCache() available only in Redisson PRO |
❌ | ✔️ | ✔️ | ✔️ | ❌ |
| getClusteredLocalCachedMapCache() available only in Redisson PRO |
✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| getMapCacheNativeV2() available only in Redisson PRO |
❌ | ❌ | ✔️ | ✔️ | ✔️ |
| getLocalCachedMapCacheNativeV2() available only in Redisson PRO |
✔️ | ❌ | ✔️ | ✔️ | ✔️ |
| getClusteredMapCacheNativeV2() available only in Redisson PRO |
❌ | ✔️ | ✔️ | ✔️ | ✔️ |
| getClusteredLocalCachedMapCacheNativeV2() available only in Redisson PRO |
✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
Use cases: limited Valkey or Redis memory.
RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap");
// or
RMapCache<String, SomeObject> map = redisson.getLocalCachedMapCache("anyMap", LocalCachedMapOptions.defaults());
// or
RMapCache<String, SomeObject> map = redisson.getClusteredLocalCachedMapCache("anyMap", LocalCachedMapOptions.defaults());
// or
RMapCache<String, SomeObject> map = redisson.getClusteredMapCache("anyMap");
RMapCacheNativeV2<String, SomeObject> map = redisson.getMapCacheNativeV2("anyMap");
// or
RMapCacheNativeV2<String, SomeObject> map = redisson.getLocalCachedMapCacheNativeV2("anyMap", LocalCachedMapOptions.defaults());
// or
RMapCacheNativeV2<String, SomeObject> map = redisson.getClusteredLocalCachedMapCacheNativeV2("anyMap", LocalCachedMapOptions.defaults());
// or
RMapCacheNativeV2<String, SomeObject> map = redisson.getClusteredMapCacheNativeV2("anyMap");
// tries to set limit map to 10 entries using LRU eviction algorithm
map.trySetMaxSize(10);
// ... using LFU eviction algorithm
map.trySetMaxSize(10, EvictionMode.LFU);
// set or change limit map to 10 entries using LRU eviction algorithm
map.setMaxSize(10);
// ... using LFU eviction algorithm
map.setMaxSize(10, EvictionMode.LFU);
map.put("1", "2");
map.put("3", "3", 1, TimeUnit.SECONDS);
Multimap¶
Java implementation of a Valkey or Redis based Multimap maps each key to a collection of values rather than a single value, so one key can hold many entries at once. Redisson provides two implementations - set-based and list-based - that differ in whether duplicate values are allowed and whether order is preserved. The number of keys is limited to 4 294 967 295 elements, key uniqueness is determined from the serialized key state rather than the key's hashCode()/equals() methods, and the object is thread-safe.
Choosing between set-based and list-based¶
Both implementations expose the same API and differ only in the semantics of the value collection held under each key.
Set-based (RSetMultimap) |
List-based (RListMultimap) |
|
|---|---|---|
| Duplicate values per key | not allowed | allowed |
| Value order | unordered | insertion order |
get(key) returns |
live RSet | live RList |
getAll / removeAll / replaceValues return |
Set |
List |
| Backing structure | Valkey or Redis Set per key | Valkey or Redis List per key |
Prefer a multimap over a plain RMap<K, Collection<V>> when individual values have to be added, removed, or tested for membership atomically on the server: a multimap mutates the per-key collection in place, avoiding the read-modify-write race of loading a collection, changing it, and writing it back.
Basic usage¶
Both implementations are obtained from the Redisson client and share the same methods; the examples below use the set-based multimap, and the list-based one behaves identically apart from keeping duplicates and order and returning List from its read methods. The asynchronous, reactive, and RxJava3 interfaces mirror the synchronous one - their methods return RFuture, Mono, and Single - so the tabs below differ only in those wrappers.
RSetMultimap<String, String> map = redisson.getSetMultimap("myMultimap");
RFuture<Boolean> putFuture = map.putAsync("user:1", "admin");
RFuture<Boolean> putAllFuture = map.putAllAsync("user:1", List.of("editor", "viewer"));
RFuture<Integer> sizeFuture = map.sizeAsync();
RFuture<Long> removeFuture = map.fastRemoveAsync("user:1");
RedissonReactiveClient redisson = redissonClient.reactive();
RSetMultimapReactive<String, String> map = redisson.getSetMultimap("myMultimap");
Mono<Boolean> putMono = map.put("user:1", "admin");
Mono<Boolean> putAllMono = map.putAll("user:1", List.of("editor", "viewer"));
Mono<Integer> sizeMono = map.size();
Mono<Long> removeMono = map.fastRemove("user:1");
RedissonRxClient redisson = redissonClient.rxJava();
RSetMultimapRx<String, String> map = redisson.getSetMultimap("myMultimap");
Single<Boolean> putRx = map.put("user:1", "admin");
Single<Boolean> putAllRx = map.putAll("user:1", List.of("editor", "viewer"));
Single<Integer> sizeRx = map.size();
Single<Long> removeRx = map.fastRemove("user:1");
Adding and reading values¶
put adds a single value under a key and returns whether the collection changed; putAll adds several values at once. Reads come in two forms: get returns a live view of the key's values, while getAll returns a detached snapshot - a Set/List on the synchronous, reactive, and RxJava3 interfaces, and a Collection on the asynchronous one.
Note
The view returned by get is itself a full Redisson collection - an RSet for set-based multimaps or an RList for list-based ones - bound to the key. Calling its own methods, or iterating it, reads and writes straight through to the multimap, so it can be passed anywhere an RSet/RList is expected without copying the values out. The asynchronous interface has no live-view get; use getAllAsync instead.
RSetMultimap<String, String> map = redisson.getSetMultimap("myMultimap");
RFuture<Boolean> putFuture = map.putAsync("user:1", "admin");
RFuture<Boolean> putAllFuture = map.putAllAsync("user:1", List.of("editor", "viewer"));
RFuture<Collection<String>> rolesFuture = map.getAllAsync("user:1"); // detached Collection
RedissonReactiveClient redisson = redissonClient.reactive();
RSetMultimapReactive<String, String> map = redisson.getSetMultimap("myMultimap");
Mono<Boolean> putMono = map.put("user:1", "admin");
Mono<Boolean> putAllMono = map.putAll("user:1", List.of("editor", "viewer"));
RSetReactive<String> live = map.get("user:1"); // live view
Mono<Set<String>> rolesMono = map.getAll("user:1"); // detached snapshot
RedissonRxClient redisson = redissonClient.rxJava();
RSetMultimapRx<String, String> map = redisson.getSetMultimap("myMultimap");
Single<Boolean> putRx = map.put("user:1", "admin");
Single<Boolean> putAllRx = map.putAll("user:1", List.of("editor", "viewer"));
RSetRx<String> live = map.get("user:1"); // live view
Single<Set<String>> rolesRx = map.getAll("user:1"); // detached snapshot
Alongside per-key access, the multimap exposes map-wide views - keySet, values, and entries - plus size (the total number of key-value pairs) and the containsKey/containsValue/containsEntry checks. The map-wide views are read on the synchronous interface:
Set<String> keys = map.keySet();
Collection<String> allValues = map.values();
Collection<Map.Entry<String, String>> allEntries = map.entries();
int total = map.size();
boolean hasRole = map.containsEntry("user:1", "admin");
Removing and replacing values¶
remove deletes a single value from a key and reports whether it was present. removeAll deletes a key's entire collection and returns the removed values, while fastRemove deletes one or more keys without returning their values and is the most efficient way to drop keys. replaceValues swaps a key's whole collection for a new one and returns the previous values.
RSetMultimap<String, String> map = redisson.getSetMultimap("myMultimap");
RFuture<Boolean> removeFuture = map.removeAsync("user:1", "viewer");
RFuture<Collection<String>> replaceFuture = map.replaceValuesAsync("user:1", List.of("admin", "owner"));
RFuture<Collection<String>> droppedFuture = map.removeAllAsync("user:1");
RFuture<Long> countFuture = map.fastRemoveAsync("user:1", "user:2");
RedissonReactiveClient redisson = redissonClient.reactive();
RSetMultimapReactive<String, String> map = redisson.getSetMultimap("myMultimap");
Mono<Boolean> removeMono = map.remove("user:1", "viewer");
Mono<Set<String>> replaceMono = map.replaceValues("user:1", List.of("admin", "owner"));
Mono<Set<String>> droppedMono = map.removeAll("user:1");
Mono<Long> countMono = map.fastRemove("user:1", "user:2");
RedissonRxClient redisson = redissonClient.rxJava();
RSetMultimapRx<String, String> map = redisson.getSetMultimap("myMultimap");
Single<Boolean> removeRx = map.remove("user:1", "viewer");
Single<Set<String>> replaceRx = map.replaceValues("user:1", List.of("admin", "owner"));
Single<Set<String>> droppedRx = map.removeAll("user:1");
Single<Long> countRx = map.fastRemove("user:1", "user:2");
Eviction¶
The plain RSetMultimap and RListMultimap objects can be expired as a whole - they implement RExpirable, so expire and clearExpire apply to the entire multimap - but they don't support expiration of individual entries. Per-entry eviction with a time to live is provided by separate MultimapCache objects. There are RSetMultimapCache and RListMultimapCache objects for Set and List based Multimaps respectively.
With the scripted cache, an eviction task is started once per unique object name at the moment of getting the Multimap instance. If instance isn't used and has expired entries it should be get again to start the eviction process. This leads to extra Valkey or Redis calls and eviction task per unique map object name.
Entries are cleaned time to time by org.redisson.eviction.EvictionScheduler. By default, it removes 100 expired entries at a time. This can be changed through cleanUpKeysAmount setting. Task launch time tuned automatically and depends on expired entries amount deleted in previous time and varies between 5 second to 30 minutes by default. This time interval can be changed through minCleanUpDelay and maxCleanUpDelay. For example, if clean task deletes 100 entries each time it will be executed every 5 seconds (minimum execution delay). But if current expired entries amount is lower than previous one then execution delay will be increased by 1.5 times and decreased otherwise.
Redis 7.4.0 and higher version implements native eviction. It's supported by RSetMultimapCacheNative and RListMultimapCacheNative objects. Expiration is handled on the server side, so no client-side eviction task or EvictionScheduler is involved.
Code examples:
// scripted eviction implementation
RSetMultimapCache<String, String> multimap = redisson.getSetMultimapCache("myMultimap");
// or native eviction implementation (Redis 7.4.0 and higher):
// RSetMultimapCacheNative<String, String> multimap = redisson.getSetMultimapCacheNative("myMultimap");
multimap.put("1", "a");
multimap.put("1", "b");
multimap.put("1", "c");
multimap.put("2", "e");
multimap.put("2", "f");
multimap.expireKey("2", 10, TimeUnit.MINUTES);
// once the object is no longer used
multimap.destroy();
// scripted eviction implementation
RSetMultimapCacheAsync<String, String> multimap = redisson.getSetMultimapCache("myMultimap");
// or native eviction implementation (Redis 7.4.0 and higher):
// RSetMultimapCacheNativeAsync<String, String> multimap = redisson.getSetMultimapCacheNative("myMultimap");
RFuture<Boolean> f1 = multimap.putAsync("1", "a");
RFuture<Boolean> f2 = multimap.putAsync("1", "b");
RFuture<Boolean> f3 = multimap.putAsync("1", "c");
RFuture<Boolean> f4 = multimap.putAsync("2", "e");
RFuture<Boolean> f5 = multimap.putAsync("2", "f");
RFuture<Boolean> exfeature = multimap.expireKeyAsync("2", 10, TimeUnit.MINUTES);
RedissonReactiveClient redissonReactive = redisson.reactive();
// scripted eviction implementation
RSetMultimapCacheReactive<String, String> multimap = redissonReactive.getSetMultimapCache("myMultimap");
// or native eviction implementation (Redis 7.4.0 and higher):
// RSetMultimapCacheNativeReactive<String, String> multimap = redissonReactive.getSetMultimapCacheNative("myMultimap");
Mono<Boolean> f1 = multimap.put("1", "a");
Mono<Boolean> f2 = multimap.put("1", "b");
Mono<Boolean> f3 = multimap.put("1", "c");
Mono<Boolean> f4 = multimap.put("2", "e");
Mono<Boolean> f5 = multimap.put("2", "f");
Mono<Boolean> exfeature = multimap.expireKey("2", 10, TimeUnit.MINUTES);
RedissonRxClient redissonRx = redisson.rxJava();
// scripted eviction implementation
RSetMultimapCacheRx<String, String> multimap = redissonRx.getSetMultimapCache("myMultimap");
// or native eviction implementation (Redis 7.4.0 and higher):
// RSetMultimapCacheNativeRx<String, String> multimap = redissonRx.getSetMultimapCacheNative("myMultimap");
Single<Boolean> f1 = multimap.put("1", "a");
Single<Boolean> f2 = multimap.put("1", "b");
Single<Boolean> f3 = multimap.put("1", "c");
Single<Boolean> f4 = multimap.put("2", "e");
Single<Boolean> f5 = multimap.put("2", "f");
Single<Boolean> exfeature = multimap.expireKey("2", 10, TimeUnit.MINUTES);
List-based caches are obtained the same way with getListMultimapCache and getListMultimapCacheNative, and expose the identical API with List value semantics.
Listeners¶
Redisson allows binding listeners per RSetMultimap or RListMultimap object. This requires the notify-keyspace-events setting to be enabled on Valkey or Redis side.
RSetMultimap listeners:
| Listener class name | Event description | Valkey or Redisnotify-keyspace-events value |
|---|---|---|
| org.redisson.api.ExpiredObjectListener | RSetMultimap object expired |
Ex |
| org.redisson.api.DeletedObjectListener | RSetMultimap object deleted |
Eg |
| org.redisson.api.listener.SetAddListener | Element added to entry | Es |
| org.redisson.api.listener.SetRemoveListener | Element removed from entry | Es |
| org.redisson.api.listener.MapPutListener | Entry created | Eh or Th |
| org.redisson.api.listener.MapRemoveListener | Entry removed | Eh or Th |
RListMultimap listeners:
| Listener class name | Event description | Valkey or Redisnotify-keyspace-events value |
|---|---|---|
| org.redisson.api.ExpiredObjectListener | RListMultimap object expired |
Ex |
| org.redisson.api.DeletedObjectListener | RListMultimap object deleted |
Eg |
| org.redisson.api.listener.ListAddListener | Element added to entry | Es |
| org.redisson.api.listener.ListRemoveListener | Element removed from entry | Es |
| org.redisson.api.listener.MapPutListener | Entry created | Eh or Th |
| org.redisson.api.listener.MapRemoveListener | Entry removed | Eh or Th |
Listener callbacks receive the affected Redis key name - and, for entry-level events, the field - as String values, not the multimap's typed key and value.
Usage example:
RListMultimap<Integer, Integer> lmap = redisson.getListMultimap("mymap");
int listenerId = lmap.addListener(new MapPutListener() {
@Override
public void onPut(String name, String fieldName) {
// ...
}
});
// ...
lmap.removeListener(listenerId);
Use Cases¶
Multimaps fit wherever one key owns a changing collection of values - secondary indexes, one-to-many relationships, and grouping streams of items by a key - with the choice between the set- and list-based implementations deciding whether duplicates and order matter.
Tag and Label Indexes
A set-based multimap makes a natural inverted index: map each tag to the ids that carry it, look them up directly, and let set semantics keep ids unique no matter how often a tag is reapplied.
RSetMultimap<String, String> tagIndex = redisson.getSetMultimap("tag-index");
tagIndex.put("premium", "user:1001");
tagIndex.put("premium", "user:1002");
tagIndex.put("beta", "user:1001");
// every id carrying a tag - duplicates are ignored
Set<String> premiumUsers = tagIndex.getAll("premium");
One-to-Many Relationships
Parent-to-children relationships - a customer's orders, an author's posts - map cleanly to a multimap keyed by the parent id. With a set-based multimap each child appears once, and the whole group is read, replaced, or removed in a single call.
RSetMultimap<Long, Long> ordersByCustomer = redisson.getSetMultimap("orders-by-customer");
ordersByCustomer.put(1001L, 55001L);
ordersByCustomer.put(1001L, 55002L);
// all orders for a customer
Set<Long> orders = ordersByCustomer.getAll(1001L);
// remove the customer and all their orders at once
Set<Long> removed = ordersByCustomer.removeAll(1001L);
Grouping Events in Order
When the values are a sequence rather than a set - log lines per request, events per session, messages per conversation - a list-based multimap preserves insertion order and keeps duplicates. Paired with the cache variant, an idle group can expire on its own.
RListMultimapCache<String, String> eventsBySession = redisson.getListMultimapCache("events-by-session");
eventsBySession.put("session:abc", "login");
eventsBySession.put("session:abc", "view:home");
eventsBySession.put("session:abc", "view:home"); // duplicates kept, in order
// expire the whole group if the session goes idle
eventsBySession.expireKey("session:abc", 30, TimeUnit.MINUTES);
List<String> timeline = eventsBySession.getAll("session:abc");
JSON Store¶
This feature is available only in Redisson PRO edition.
RJsonStore is a distributed store of JSON documents, compatible with Valkey and Redis, and thread-safe. Each value is a POJO serialized to JSON and stored under a key with native JSON.* commands, so a stored document is not an opaque blob: individual fields, array elements, and numeric counters can be read and updated in place on the server - addressed by JSONPath - without transferring the whole value. Operations run on a single key or on a group of keys in one round trip.
Entries can carry a per-entry time to live that is cleaned up on the server side with no eviction task, and can be indexed and queried by their fields through RediSearch integration. A local cache variant serves hot reads without a network round trip.
When to use JSON Store¶
Reach for RJsonStore instead of an RMap or RBucket of serialized objects when you need any of the following: updating part of a document in place - setting a field, pushing onto an array, incrementing a counter - without loading and rewriting the whole value, which also removes the read-modify-write race; a per-entry time to live; or field-level search through RediSearch.
Basic usage¶
An instance is obtained from the Redisson client with the document codec (and optionally a key codec). The synchronous, asynchronous, reactive, and RxJava3 interfaces share the same methods and differ only in their return types - RFuture, Mono, and Single/Maybe/Completable.
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
MyObject obj = ...;
Mono<Void> setMono = store.set("1", obj);
Mono<MyObject> getMono = store.get("1");
Mono<Boolean> deleteMono = store.delete("1");
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
MyObject obj = ...;
Completable setRx = store.set("1", obj);
Maybe<MyObject> getRx = store.get("1");
Single<Boolean> deleteRx = store.delete("1");
Storing documents¶
set writes a document, overwriting any previous value, and set(Map) stores many documents in a single call. A Duration overload attaches a per-entry time to live. setIfAbsent writes only when the key is new and setIfExists only when it already exists; both also have Duration overloads.
RJsonStore<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
store.set("1", t1);
store.set(Map.of("1", t1, "2", t2));
store.set("1", t1, Duration.ofSeconds(100)); // per-entry TTL
boolean stored = store.setIfAbsent("1", t1);
boolean updated = store.setIfExists("1", t1);
RJsonStoreAsync<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
RFuture<Void> f1 = store.setAsync("1", t1);
RFuture<Void> f2 = store.setAsync(Map.of("1", t1, "2", t2));
RFuture<Void> f3 = store.setAsync("1", t1, Duration.ofSeconds(100));
RFuture<Boolean> f4 = store.setIfAbsentAsync("1", t1);
RFuture<Boolean> f5 = store.setIfExistsAsync("1", t1);
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Mono<Void> m1 = store.set("1", t1);
Mono<Void> m2 = store.set(Map.of("1", t1, "2", t2));
Mono<Void> m3 = store.set("1", t1, Duration.ofSeconds(100));
Mono<Boolean> m4 = store.setIfAbsent("1", t1);
Mono<Boolean> m5 = store.setIfExists("1", t1);
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Completable c1 = store.set("1", t1);
Completable c2 = store.set(Map.of("1", t1, "2", t2));
Completable c3 = store.set("1", t1, Duration.ofSeconds(100));
Single<Boolean> s4 = store.setIfAbsent("1", t1);
Single<Boolean> s5 = store.setIfExists("1", t1);
Reading documents¶
get returns a single document, or null if the key is absent; get(Set) reads many keys in one round trip and returns a map of the present entries; and getAndDelete reads a document and removes it atomically. readAllKeySet returns every key at once, while the synchronous keySet iterates keys lazily with the server's SCAN cursor and accepts an optional pattern and batch count.
RJsonStoreAsync<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
RFuture<MyObject> getFuture = store.getAsync("1");
RFuture<Map<String, MyObject>> manyFuture = store.getAsync(Set.of("1", "2"));
RFuture<MyObject> takenFuture = store.getAndDeleteAsync("1");
RFuture<Set<String>> keysFuture = store.readAllKeySetAsync();
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Mono<MyObject> getMono = store.get("1");
Mono<Map<String, MyObject>> manyMono = store.get(Set.of("1", "2"));
Mono<MyObject> takenMono = store.getAndDelete("1");
Mono<Set<String>> keysMono = store.readAllKeySet();
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Maybe<MyObject> getRx = store.get("1");
Single<Map<String, MyObject>> manyRx = store.get(Set.of("1", "2"));
Maybe<MyObject> takenRx = store.getAndDelete("1");
Single<Set<String>> keysRx = store.readAllKeySet();
Deleting documents¶
delete removes a single key and reports whether it existed; delete(Set) removes many keys in one call and returns how many were present.
Working inside a document¶
Because each value is a JSON document, parts of it can be read and modified by JSONPath - evaluated entirely on the server, without fetching or rewriting the whole value. Each operation takes a path; the typed reads (get, arrayPop, and similar) take a JsonCodec - a JacksonCodec of the extracted type - to decode the sub-value. Most operations have a *Multi companion (arrayAppendMulti, incrementAndGetMulti, ...) that applies to every location a multi-valued path matches and returns one result per match.
Fields and structure
set(key, path, value) writes a single field, merge merges a partial object into the document, the path form of get extracts a typed sub-value, and getType/countKeys/getKeys inspect structure.
RJsonStore<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
store.set("1", "$.name", "name2");
store.merge("1", "$", new MyObject());
String name = store.get("1", new JacksonCodec<>(String.class), "$.name");
JsonType type = store.getType("1", "$.name");
long fields = store.countKeys("1");
RJsonStoreAsync<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
RFuture<Void> setFuture = store.setAsync("1", "$.name", "name2");
RFuture<Void> mergeFuture = store.mergeAsync("1", "$", new MyObject());
RFuture<String> nameFuture = store.getAsync("1", new JacksonCodec<>(String.class), "$.name");
RFuture<JsonType> typeFuture = store.getTypeAsync("1", "$.name");
RFuture<Long> fieldsFuture = store.countKeysAsync("1");
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Mono<Void> setMono = store.set("1", "$.name", "name2");
Mono<Void> mergeMono = store.merge("1", "$", new MyObject());
Mono<String> nameMono = store.get("1", new JacksonCodec<>(String.class), "$.name");
Mono<JsonType> typeMono = store.getType("1", "$.name");
Mono<Long> fieldsMono = store.countKeys("1");
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Completable setRx = store.set("1", "$.name", "name2");
Completable mergeRx = store.merge("1", "$", new MyObject());
Maybe<String> nameRx = store.get("1", new JacksonCodec<>(String.class), "$.name");
Single<JsonType> typeRx = store.getType("1", "$.name");
Maybe<Long> fieldsRx = store.countKeys("1");
Arrays
arrayAppend and arrayInsert add elements, arraySize reports the length, and arrayPop removes and returns an element at an index (-1 for the last).
RJsonStore<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
long size = store.arrayAppend("1", "$.tags", "premium");
long size2 = store.arrayInsert("1", "$.tags", 0, "vip");
long length = store.arraySize("1", "$.tags");
String last = store.arrayPop("1", new JacksonCodec<>(String.class), "$.tags", -1);
RJsonStoreAsync<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
RFuture<Long> appendFuture = store.arrayAppendAsync("1", "$.tags", "premium");
RFuture<Long> insertFuture = store.arrayInsertAsync("1", "$.tags", 0, "vip");
RFuture<Long> lengthFuture = store.arraySizeAsync("1", "$.tags");
RFuture<String> lastFuture = store.arrayPopAsync("1", new JacksonCodec<>(String.class), "$.tags", -1);
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Mono<Long> appendMono = store.arrayAppend("1", "$.tags", "premium");
Mono<Long> insertMono = store.arrayInsert("1", "$.tags", 0L, "vip");
Mono<Long> lengthMono = store.arraySize("1", "$.tags");
Mono<String> lastMono = store.arrayPop("1", new JacksonCodec<>(String.class), "$.tags", -1L);
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Single<Long> appendRx = store.arrayAppend("1", "$.tags", "premium");
Single<Long> insertRx = store.arrayInsert("1", "$.tags", 0L, "vip");
Single<Long> lengthRx = store.arraySize("1", "$.tags");
Maybe<String> lastRx = store.arrayPop("1", new JacksonCodec<>(String.class), "$.tags", -1L);
Numbers, booleans, and strings
incrementAndGet atomically bumps a numeric field, toggle flips a boolean, and stringAppend appends to a string field, returning its new length.
RJsonStoreAsync<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
RFuture<Integer> viewsFuture = store.incrementAndGetAsync("1", "$.views", 1);
RFuture<Boolean> activeFuture = store.toggleAsync("1", "$.active");
RFuture<Long> lengthFuture = store.stringAppendAsync("1", "$.name", " (verified)");
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Mono<Integer> viewsMono = store.incrementAndGet("1", "$.views", 1);
Mono<Boolean> activeMono = store.toggle("1", "$.active");
Mono<Long> lengthMono = store.stringAppend("1", "$.name", " (verified)");
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Maybe<Integer> viewsRx = store.incrementAndGet("1", "$.views", 1);
Single<Boolean> activeRx = store.toggle("1", "$.active");
Single<Long> lengthRx = store.stringAppend("1", "$.name", " (verified)");
Atomic and conditional updates¶
compareAndSet swaps a document only if it currently equals an expected value, and its path form does the same for a single field. getAndSet replaces a document and returns the previous value. The conditional setIfAbsent/setIfExists shown under Storing documents round out this group.
RJsonStoreAsync<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
RFuture<Boolean> swapFuture = store.compareAndSetAsync("1", t1, t2);
RFuture<Boolean> fieldFuture = store.compareAndSetAsync("1", "$.name", "name1", "name2");
RFuture<MyObject> prevFuture = store.getAndSetAsync("1", t2);
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Mono<Boolean> swapMono = store.compareAndSet("1", t1, t2);
Mono<Boolean> fieldMono = store.compareAndSet("1", "$.name", "name1", "name2");
Mono<MyObject> prevMono = store.getAndSet("1", t2);
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Single<Boolean> swapRx = store.compareAndSet("1", t1, t2);
Single<Boolean> fieldRx = store.compareAndSet("1", "$.name", "name1", "name2");
Maybe<MyObject> prevRx = store.getAndSet("1", t2);
Expiration¶
A per-entry time to live is attached when writing (set with a Duration); expiration is handled on the Valkey or Redis side with no eviction task. getAndExpire reads a document and (re)sets its expiration in one step, remainTimeToLive reports the milliseconds left, and setAndKeepTTL overwrites a value while preserving its current expiration.
RJsonStoreAsync<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
RFuture<Void> setFuture = store.setAsync("1", t1, Duration.ofSeconds(100));
RFuture<MyObject> getFuture = store.getAndExpireAsync("1", Duration.ofMinutes(5));
RFuture<Long> ttlFuture = store.remainTimeToLiveAsync("1");
RFuture<Void> keepFuture = store.setAndKeepTTLAsync("1", t2);
RedissonReactiveClient redisson = redissonClient.reactive();
RJsonStoreReactive<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Mono<Void> setMono = store.set("1", t1, Duration.ofSeconds(100));
Mono<MyObject> getMono = store.getAndExpire("1", Duration.ofMinutes(5));
Mono<Long> ttlMono = store.remainTimeToLive("1");
Mono<Void> keepMono = store.setAndKeepTTL("1", t2);
RedissonRxClient redisson = redissonClient.rxJava();
RJsonStoreRx<String, MyObject> store = redisson.getJsonStore("test", new JacksonCodec<>(MyObject.class));
Completable setRx = store.set("1", t1, Duration.ofSeconds(100));
Maybe<MyObject> getRx = store.getAndExpire("1", Duration.ofMinutes(5));
Single<Long> ttlRx = store.remainTimeToLive("1");
Completable keepRx = store.setAndKeepTTL("1", t2);
These TTLs apply to individual entries. The store itself implements RExpirable, so expire and clearExpire set an expiration on the entire store, which is a separate mechanism from the per-entry expiration above.
Search by Object properties¶
Because values are stored as JSON, a JSON Store can be indexed and queried by the fields of its documents through RediSearch. Point lookups by key and ad-hoc field queries then run against the same data, with no separate search system to keep in sync.
For data searching, index prefix should be defined in <object_name>: format. For example for object name "test" prefix is "test:".
StringCodec should be used as object codec to enable searching by field.
Data search code example:
RSearch s = redisson.getSearch();
s.createIndex("idx", IndexOptions.defaults()
.on(IndexType.JSON)
.prefix(Arrays.asList("test:")),
FieldIndex.text("name"));
RJsonStore<String, MyObject> store = redisson.getJsonStore("test", StringCodec.INSTANCE, new JacksonCodec<>(MyObject.class));
MyObject t1 = new MyObject();
t1.setName("name1");
MyObject t2 = new MyObject();
t2.setName("name2");
Map<String, MyObject> entries = new HashMap<>();
entries.put("1", t1);
entries.put("2", t2);
store.set(entries);
// search
SearchResult r = s.search("idx", "*", QueryOptions.defaults()
.returnAttributes(new ReturnAttribute("name")));
// aggregation
AggregationResult ar = s.aggregate("idx", "*", AggregationOptions.defaults()
.withCursor().load("name"));
Local Cache¶
Redisson provides JSON Store implementation with local cache.
local cache - so called near cache used to speed up read operations and avoid network roundtrips. It caches JSON Store entries on Redisson side and executes read operations up to 45x faster in comparison with regular implementation. Local cached instances with the same name are connected to the same pub/sub channel. This channel is used for exchanging of update/invalidate events between all instances. Local cache store doesn't use hashCode()/equals() methods of key object, instead it uses hash of serialized state. It's recommended to use each local cached instance as a singleton per unique name since it has own state for local cache.
Warning
It's recommended to use a single instance of local cached JsonStore instance per unique name for each Redisson instance. Same LocalCachedJsonStoreOptions object should be used across all instances with the same name.
Follow options can be supplied during object creation:
LocalCachedJsonStoreOptions options = LocalCachedJsonStoreOptions.name("object_name_example")
// Defines codec used for key
.keyCodec(codec)
// Defines codec used for JSON value
.valueCodec(codec)
// Defines whether to store a cache miss into the local cache.
// Default value is false.
.storeCacheMiss(false);
// Defines store mode of cache data.
// Follow options are available:
// LOCALCACHE - store data in local cache only and use Valkey or Redis only for data update/invalidation.
// LOCALCACHE_REDIS - store data in both Valkey or Redis and local cache.
.storeMode(StoreMode.LOCALCACHE_REDIS)
// Defines Cache provider used as local cache store.
// Follow options are available:
// REDISSON - uses Redisson own implementation
// CAFFEINE - uses Caffeine implementation
.cacheProvider(CacheProvider.REDISSON)
// Defines local cache eviction policy.
// Follow options are available:
// LFU - Counts how often an item was requested. Those that are used least often are discarded first.
// LRU - Discards the least recently used items first
// SOFT - Uses soft references, entries are removed by GC
// WEAK - Uses weak references, entries are removed by GC
// NONE - No eviction
.evictionPolicy(EvictionPolicy.NONE)
// If cache size is 0 then local cache is unbounded.
.cacheSize(1000)
// Defines strategy for load missed local cache updates after connection failure.
//
// Follow reconnection strategies are available:
// CLEAR - Clear local cache if map instance has been disconnected for a while.
// NONE - Default. No reconnection handling
.reconnectionStrategy(ReconnectionStrategy.NONE)
// Defines local cache synchronization strategy.
//
// Follow sync strategies are available:
// INVALIDATE - Default. Invalidate cache entry across all RLocalCachedJsonStore instances on map entry change
// UPDATE - Insert/update cache entry across all RLocalCachedJsonStore instances on map entry change
// NONE - No synchronizations on map changes
.syncStrategy(SyncStrategy.INVALIDATE)
// time to live for each entry in local cache
.timeToLive(Duration.ofSeconds(10))
// max idle time for each entry in local cache
.maxIdle(Duration.ofSeconds(10));
// Defines how to listen expired event sent by Valkey or Redis upon this instance deletion
//
// Follow expiration policies are available:
// DONT_SUBSCRIBE - Don't subscribe on expire event
// SUBSCRIBE_WITH_KEYEVENT_PATTERN - Subscribe on expire event using `__keyevent@*:expired` pattern
// SUBSCRIBE_WITH_KEYSPACE_CHANNEL - Subscribe on expire event using `__keyspace@N__:name` channel
.expirationEventPolicy(ExpirationEventPolicy.SUBSCRIBE_WITH_KEYEVENT_PATTERN)
Data write code example:
LocalCachedJsonStoreOptions ops = LocalCachedJsonStoreOptions.name("test")
.keyCodec(StringCodec.INSTANCE)
.valueCodec(new JacksonCodec<>(MyObject.class));
RLocalCachedJsonStore<String, MyObject> store = redisson.getLocalCachedJsonStore(ops);
MyObject t1 = new MyObject();
t1.setName("name1");
MyObject t2 = new MyObject();
t2.setName("name2");
Map<String, MyObject> entries = new HashMap<>();
entries.put("1", t1);
entries.put("2", t2);
// multiple entries at once
store.set(entries);
// or set entry per call
store.set("1", t1);
store.set("2", t2);
// with ttl
store.set("1", t1, Duration.ofSeconds(100));
// set if not set previously
store.setIfAbsent("1", t1);
// set if entry already exists
store.setIfExists("1", t1);
Data read code example:
LocalCachedJsonStoreOptions ops = LocalCachedJsonStoreOptions.name("test")
.keyCodec(StringCodec.INSTANCE)
.valueCodec(new JacksonCodec<>(MyObject.class));
RLocalCachedJsonStore<String, MyObject> store = redisson.getLocalCachedJsonStore(ops);
// multiple entries at once
Map<String, MyObject> entries = store.get(Set.of("1", "2"));
// or read entry per call
MyObject value1 = store.get("1");
MyObject value2 = store.get("2");
Data deletion code example:
LocalCachedJsonStoreOptions ops = LocalCachedJsonStoreOptions.name("test")
.keyCodec(StringCodec.INSTANCE)
.valueCodec(new JacksonCodec<>(MyObject.class));
RLocalCachedJsonStore<String, MyObject> store = redisson.getLocalCachedJsonStore(ops);
// multiple entries at once
long deleted = store.delete(Set.of("1", "2"));
// or delete entry per call
boolean status = store.delete("1");
boolean status = store.delete("2");
Use Cases¶
JSON Store fits applications that keep many structured records — each a self-contained JSON document addressed by a key — where individual entries are read, written, expired, and queried independently. Values are stored with JSON.* commands, expiration is handled on the Valkey or Redis side without an eviction task, many keys can be read or written in a single round trip, and entries can be indexed and queried by their fields through RediSearch integration.
Session and Token Store
Web sessions, refresh tokens, and short-lived authorization grants are rich objects (user id, roles, device metadata, issue/expiry timestamps) that map one-to-one to a key and should disappear automatically once they lapse. A time to live is attached per entry so cleanup happens on the Valkey or Redis side with no eviction task, while setIfExists refreshes only sessions that are still active.
RJsonStore<String, Session> sessions =
redisson.getJsonStore("session", new JacksonCodec<>(Session.class));
Session s = new Session(userId, roles, deviceId, Instant.now());
// store the session with a 30-minute TTL, expiry handled on the Valkey or Redis side
sessions.set("sess:" + sessionId, s, Duration.ofMinutes(30));
// validate by key on each incoming request
Session current = sessions.get("sess:" + sessionId);
// sliding refresh - only extend a session that still exists
sessions.setIfExists("sess:" + sessionId, current);
// explicit logout
sessions.delete("sess:" + sessionId);
Searchable Document Store
User profiles, product catalogs, and other entity records are stored as JSON documents and queried by field rather than only by key. With a JSON index defined over the store's key prefix, the same data backs both point lookups by id and ad-hoc field queries, full-text matching, and aggregation, without copying it into a separate search system. StringCodec is used for keys so fields are indexable.
RSearch search = redisson.getSearch();
search.createIndex("idx:product", IndexOptions.defaults()
.on(IndexType.JSON)
.prefix(Arrays.asList("product:")),
FieldIndex.text("name"));
RJsonStore<String, Product> products =
redisson.getJsonStore("product", StringCodec.INSTANCE, new JacksonCodec<>(Product.class));
products.set("product:1001", new Product("Wireless Mouse"));
// point lookup by id
Product p = products.get("product:1001");
// field query backed by the same data - full-text match on the name field
SearchResult found = search.search("idx:product", "@name:wireless", QueryOptions.defaults()
.returnAttributes(new ReturnAttribute("name")));
Shopping Carts and Workflow State
Carts, checkout sessions, multi-step form drafts, and long-running workflow state are JSON documents that change over their lifetime and are often touched in groups - load every cart in a batch job, expire abandoned ones, or purge a customer's drafts at once. Bulk set, get, and delete over a set of keys collapse these into a single round trip, while a per-entry time to live reclaims abandoned state automatically.
RJsonStore<String, Cart> carts =
redisson.getJsonStore("cart", new JacksonCodec<>(Cart.class));
// write several carts at once
Map<String, Cart> batch = new HashMap<>();
batch.put("cart:a1", cartA);
batch.put("cart:b2", cartB);
carts.set(batch);
// abandoned-cart expiry handled on the Valkey or Redis side
carts.set("cart:a1", cartA, Duration.ofHours(24));
// read or purge a group of carts in one call
Map<String, Cart> loaded = carts.get(Set.of("cart:a1", "cart:b2"));
long removed = carts.delete(Set.of("cart:a1", "cart:b2"));
Read-Heavy Reference Data with Local Cache
Feature configuration, pricing tables, and catalog metadata are read constantly but updated rarely, and for these the network round trip dominates cost. The local cached JSON Store keeps entries on the Redisson side for reads up to 45x faster than the regular implementation, while a shared pub/sub channel invalidates cached copies across all instances whenever an entry changes, so every node converges on the latest value.
LocalCachedJsonStoreOptions options = LocalCachedJsonStoreOptions.name("pricing")
.keyCodec(StringCodec.INSTANCE)
.valueCodec(new JacksonCodec<>(PricingRule.class))
.syncStrategy(SyncStrategy.INVALIDATE)
.evictionPolicy(EvictionPolicy.LRU)
.cacheSize(10000);
RLocalCachedJsonStore<String, PricingRule> pricing = redisson.getLocalCachedJsonStore(options);
// served from the local cache after the first read, no network round trip
PricingRule rule = pricing.get("rule:default");
// this update is propagated to every other instance's local cache
pricing.set("rule:default", updatedRule);
Set¶
Redisson's RSet is a distributed implementation of Java's Set interface, backed by a Valkey or Redis set. It is thread-safe, cluster-compatible, holds up to 4,294,967,295 elements, and is available through synchronous, asynchronous, reactive, and RxJava3 interfaces.
Element uniqueness is determined by the serialized form of the element rather than by its hashCode()/equals() methods. RSet also implements RExpirable, so an expiration can be set on the set as a whole; expiring individual elements is a separate feature, described under Entry eviction and TTL.
Variants add per-element eviction and data partitioning across a cluster, summarized under Choosing a Set implementation.
Basic operations¶
add, remove, and contains operate on single elements, while addAll, removeAll, retainAll, and containsAll operate on collections. tryAdd adds one or more elements only if all of them are absent, returning whether the set changed.
RSetAsync<SomeObject> set = redisson.getSet("mySet");
RFuture<Boolean> added = set.addAsync(new SomeObject());
RFuture<Boolean> removed = set.removeAsync(new SomeObject());
RFuture<Boolean> exists = set.containsAsync(new SomeObject());
RFuture<Boolean> changed = set.tryAddAsync(new SomeObject(), new SomeObject());
RFuture<Integer> size = set.sizeAsync();
RedissonReactiveClient redisson = redissonClient.reactive();
RSetReactive<SomeObject> set = redisson.getSet("mySet");
Mono<Boolean> added = set.add(new SomeObject());
Mono<Boolean> removed = set.remove(new SomeObject());
Mono<Boolean> exists = set.contains(new SomeObject());
Mono<Boolean> changed = set.tryAdd(new SomeObject(), new SomeObject());
Mono<Integer> size = set.size();
RedissonRxClient redisson = redissonClient.rxJava();
RSetRx<SomeObject> set = redisson.getSet("mySet");
Single<Boolean> added = set.add(new SomeObject());
Single<Boolean> removed = set.remove(new SomeObject());
Single<Boolean> exists = set.contains(new SomeObject());
Single<Boolean> changed = set.tryAdd(new SomeObject(), new SomeObject());
Single<Integer> size = set.size();
Set algebra¶
Sets can be combined with other named sets on the server. The read* methods return the result and combine the named sets with this set, leaving it unchanged; union, diff, and intersection instead compute the combination of the named sets, overwrite this set with the result, and return its new size. countIntersection returns the size of an intersection without materializing it.
Note
union, diff, and intersection overwrite this set with the result. Use readUnion, readDiff, and readIntersection to combine sets without changing this one.
RSet<SomeObject> set = redisson.getSet("mySet");
// non-destructive: returns the result, this set is unchanged
Set<SomeObject> u = set.readUnion("set2", "set3");
Set<SomeObject> i = set.readIntersection("set2");
Set<SomeObject> d = set.readDiff("set2");
// destructive: overwrites this set, returns the new size
int unionSize = set.union("set2", "set3");
Integer common = set.countIntersection("set2");
RSetAsync<SomeObject> set = redisson.getSet("mySet");
RFuture<Set<SomeObject>> u = set.readUnionAsync("set2", "set3");
RFuture<Set<SomeObject>> i = set.readIntersectionAsync("set2");
RFuture<Set<SomeObject>> d = set.readDiffAsync("set2");
RFuture<Integer> unionSize = set.unionAsync("set2", "set3");
RFuture<Integer> common = set.countIntersectionAsync("set2");
RedissonReactiveClient redisson = redissonClient.reactive();
RSetReactive<SomeObject> set = redisson.getSet("mySet");
Mono<Set<SomeObject>> u = set.readUnion("set2", "set3");
Mono<Set<SomeObject>> i = set.readIntersection("set2");
Mono<Set<SomeObject>> d = set.readDiff("set2");
Mono<Integer> unionSize = set.union("set2", "set3");
Mono<Integer> common = set.countIntersection("set2");
RedissonRxClient redisson = redissonClient.rxJava();
RSetRx<SomeObject> set = redisson.getSet("mySet");
Maybe<Set<SomeObject>> u = set.readUnion("set2", "set3");
Maybe<Set<SomeObject>> i = set.readIntersection("set2");
Maybe<Set<SomeObject>> d = set.readDiff("set2");
Single<Integer> unionSize = set.union("set2", "set3");
Single<Integer> common = set.countIntersection("set2");
Random elements and moving¶
random returns a random element, or a subset, without removing it; removeRandom pops one or several elements at random; and move atomically transfers an element to another set.
RSetAsync<SomeObject> set = redisson.getSet("mySet");
RFuture<SomeObject> one = set.randomAsync();
RFuture<Set<SomeObject>> some = set.randomAsync(3);
RFuture<SomeObject> popped = set.removeRandomAsync();
RFuture<Set<SomeObject>> poppedMany = set.removeRandomAsync(2);
RFuture<Boolean> moved = set.moveAsync("otherSet", one);
RedissonReactiveClient redisson = redissonClient.reactive();
RSetReactive<SomeObject> set = redisson.getSet("mySet");
Mono<SomeObject> one = set.random();
Mono<Set<SomeObject>> some = set.random(3);
Mono<SomeObject> popped = set.removeRandom();
Mono<Set<SomeObject>> poppedMany = set.removeRandom(2);
Mono<Boolean> moved = set.move("otherSet", one);
RedissonRxClient redisson = redissonClient.rxJava();
RSetRx<SomeObject> set = redisson.getSet("mySet");
Maybe<SomeObject> one = set.random();
Maybe<Set<SomeObject>> some = set.random(3);
Maybe<SomeObject> popped = set.removeRandom();
Maybe<Set<SomeObject>> poppedMany = set.removeRandom(2);
Single<Boolean> moved = set.move("otherSet", one);
Iterating¶
readAll pulls the whole set into memory in a single call - convenient for small sets, expensive for large ones.
To traverse a large set without loading it all at once, the synchronous iterator() streams elements through the server's SCAN cursor, and distributedIterator spreads the scan across a cluster.
Per-value locks¶
An RSet can bind a lock, read/write lock, or semaphore to an individual element, which is convenient for guarding work on one member.
RSet<MyObject> set = redisson.getSet("anySet");
MyObject value = new MyObject();
RLock lock = set.getLock(value);
lock.lock();
try {
// process value ...
} finally {
lock.unlock();
}
Choosing a Set implementation¶
Every implementation shares the operations above and differs in two capabilities - per-element eviction and data partitioning across a cluster - plus whether it requires Redisson PRO. Data-partitioned sets (getClusteredSet and getClusteredSetCache) implement RClusteredSet and scale a single logical set across master nodes; see data partitioning. The table lists the main entry points, and the feature comparison enumerates every variant; in PRO, all types additionally provide ultra-fast read/write.
| Client method | Data partitioning | Per-element eviction | Availability |
|---|---|---|---|
getSet() |
❌ | ❌ | open-source |
getSetCache() |
❌ | ✔️ (scripted) | open-source |
getSetCacheV2() |
✔️ | ✔️ (server-side) | PRO |
getClusteredSet() |
✔️ | ❌ | PRO |
getClusteredSetCache() |
✔️ | ✔️ | PRO |
Entry eviction and TTL¶
Beyond the whole-set expiration that RSet inherits from RExpirable, the cache implementations attach a time to live to individual elements. RSetCache removes expired elements with a Redisson eviction task (one task per unique name, with extra calls; call destroy() when the instance is no longer used), while RSetCacheV2 cleans them on the Valkey or Redis side without a task. add takes the TTL as arguments.
Listeners¶
Redisson allows binding listeners per RSet object. This requires the notify-keyspace-events setting to be enabled on Valkey or Redis side.
| Listener class name | Event description | Valkey or Redisnotify-keyspace-events value |
|---|---|---|
| org.redisson.api.listener.TrackingListener | Element added/removed/updated after read operation | - |
| org.redisson.api.ExpiredObjectListener | RSet object expired |
Ex |
| org.redisson.api.DeletedObjectListener | RSet object deleted |
Eg |
| org.redisson.api.listener.SetAddListener | Element added | Es |
| org.redisson.api.listener.SetRemoveListener | Element removed | Es |
| org.redisson.api.listener.SetRemoveRandomListener | Element randomly removed | Es |
Usage example:
RSet<String> set = redisson.getSet("anySet");
int listenerId = set.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
// ...
}
});
// ...
set.removeListener(listenerId);
SortedSet¶
Redisson's RSortedSet is a distributed implementation of Java's SortedSet interface, backed by Valkey or Redis. It is thread-safe and keeps its elements in sorted order - by their natural ordering, or by a Comparator supplied through trySetComparator - using that ordering to enforce uniqueness.
RSortedSet maintains order on the client side and exposes only the synchronous interface together with a few asynchronous methods; it has no reactive or RxJava3 variant, and the SortedSet range views (subSet, headSet, tailSet) are not supported. For most needs one of the purpose-built sorted structures is a better fit - see Choosing between the sorted structures.
Choosing between the sorted structures¶
Redisson offers three sorted structures with different strengths:
| Structure | Ordering | Elements | Interfaces |
|---|---|---|---|
RSortedSet |
natural ordering or a Comparator |
any serializable object | synchronous (plus some async) |
RLexSortedSet |
lexicographic | String only |
sync, async, reactive, RxJava3 |
RScoredSortedSet |
by an explicit numeric score | any serializable object | sync, async, reactive, RxJava3 |
RLexSortedSet and RScoredSortedSet are backed by a native sorted set and scale far better, so prefer LexSortedSet for ordered String data and ScoredSortedSet when elements are ranked by a score. Reach for RSortedSet only when you specifically need Comparator-based ordering of custom objects over a small set.
Basic usage¶
If you need a custom ordering, call trySetComparator before adding any elements; it returns false if the set already contains elements (otherwise the natural ordering of Comparable elements is used). add, remove, and contains then behave as on any set.
RSortedSet<Integer> set = redisson.getSortedSet("mySet");
set.trySetComparator(Comparator.reverseOrder()); // optional; before the first add
set.add(3);
set.add(1);
set.add(2);
boolean removed = set.remove(1);
boolean exists = set.contains(2);
add, remove, readAll, and the polling methods also have asynchronous (RFuture) forms, for example addAsync and removeAsync.
Reading in order¶
first and last return the lowest and highest elements, readAll returns every element in sorted order, and the set can be traversed lazily with iterator() or, across a cluster, distributedIterator().
Integer lowest = set.first();
Integer highest = set.last();
Collection<Integer> ordered = set.readAll(); // all elements, in order
int size = set.size();
for (Integer value : set) {
// iterates in sorted order
}
Polling¶
pollFirst and pollLast remove and return the lowest or highest element. Count variants return several at once, and Duration variants block until an element is available or the timeout elapses.
Integer first = set.pollFirst(); // remove and return the lowest
Collection<Integer> firstThree = set.pollFirst(3);
Integer last = set.pollLast(); // remove and return the highest
// block up to 10 seconds for an element to appear
Integer awaited = set.pollFirst(Duration.ofSeconds(10));
Limitations¶
RSortedSet keeps elements ordered on the client side, so insertions and reads grow more expensive as the set grows; it is not suited to large or high-churn data. It also has no reactive or RxJava3 interface, and the java.util.SortedSet range views - subSet, headSet, and tailSet - throw UnsupportedOperationException. For ordered String data use LexSortedSet, and for score-ranked data use ScoredSortedSet.
ScoredSortedSet¶
Redisson's RScoredSortedSet is a distributed sorted set (a Valkey or Redis sorted set): every element is stored with an associated double score, and the set is kept ordered by that score. Elements are unique by their serialized state, the set holds up to 4,294,967,295 of them. This object is thread-safe.
Elements can be queried two ways - by rank (their 0-based position in score order) or by score range. Read methods come in a value* form that returns the elements and an entry* form that returns ScoredEntry objects pairing each value with its score.
For lexicographic ordering of String elements see LexSortedSet, and for Comparator-based ordering of arbitrary objects see SortedSet.
Basic operations¶
add stores an element with a score (replacing the score if the element already exists), and addAll stores many at once. addScore atomically increments an element's score and returns the new value, getScore reads it, and remove deletes an element. The conditional forms addIfAbsent, addIfExists, addIfGreater, addIfLess, and tryAdd add only when their condition holds.
RScoredSortedSet<SomeObject> set = redisson.getScoredSortedSet("mySet");
SomeObject value = new SomeObject();
boolean added = set.add(1.5, value);
set.addAll(Map.of(new SomeObject(), 2.0, new SomeObject(), 3.0));
Double newScore = set.addScore(value, 0.5); // increment the score
Double score = set.getScore(value);
boolean removed = set.remove(value);
RScoredSortedSetAsync<SomeObject> set = redisson.getScoredSortedSet("mySet");
SomeObject value = new SomeObject();
RFuture<Boolean> added = set.addAsync(1.5, value);
RFuture<Integer> count = set.addAllAsync(Map.of(new SomeObject(), 2.0));
RFuture<Double> newScore = set.addScoreAsync(value, 0.5);
RFuture<Double> score = set.getScoreAsync(value);
RFuture<Boolean> removed = set.removeAsync(value);
RedissonReactiveClient redisson = redissonClient.reactive();
RScoredSortedSetReactive<SomeObject> set = redisson.getScoredSortedSet("mySet");
SomeObject value = new SomeObject();
Mono<Boolean> added = set.add(1.5, value);
Mono<Integer> count = set.addAll(Map.of(new SomeObject(), 2.0));
Mono<Double> newScore = set.addScore(value, 0.5);
Mono<Double> score = set.getScore(value);
Mono<Boolean> removed = set.remove(value);
RedissonRxClient redisson = redissonClient.rxJava();
RScoredSortedSetRx<SomeObject> set = redisson.getScoredSortedSet("mySet");
SomeObject value = new SomeObject();
Single<Boolean> added = set.add(1.5, value);
Single<Integer> count = set.addAll(Map.of(new SomeObject(), 2.0));
Single<Double> newScore = set.addScore(value, 0.5);
Maybe<Double> score = set.getScore(value);
Single<Boolean> removed = set.remove(value);
Ranking¶
rank returns the 0-based position of an element in ascending score order and revRank in descending order; both are empty when the element is absent. addAndGetRank adds an element at an absolute score and returns its rank, while addScoreAndGetRank increments an element's score and returns its new rank - the typical leaderboard update. rankEntry/revRankEntry return the rank together with the score.
RScoredSortedSet<SomeObject> set = redisson.getScoredSortedSet("mySet");
Integer rank = set.rank(value); // lowest score = 0
Integer revRank = set.revRank(value); // highest score = 0
Integer newRank = set.addAndGetRank(2.5, value);
Integer afterBump = set.addScoreAndGetRank(value, 0.5); // increment, get new rank
RScoredSortedSetAsync<SomeObject> set = redisson.getScoredSortedSet("mySet");
RFuture<Integer> rank = set.rankAsync(value);
RFuture<Integer> revRank = set.revRankAsync(value);
RFuture<Integer> newRank = set.addAndGetRankAsync(2.5, value);
RFuture<Integer> afterBump = set.addScoreAndGetRankAsync(value, 0.5);
RedissonReactiveClient redisson = redissonClient.reactive();
RScoredSortedSetReactive<SomeObject> set = redisson.getScoredSortedSet("mySet");
Mono<Integer> rank = set.rank(value);
Mono<Integer> revRank = set.revRank(value);
Mono<Integer> newRank = set.addAndGetRank(2.5, value);
Mono<Integer> afterBump = set.addScoreAndGetRank(value, 0.5);
RedissonRxClient redisson = redissonClient.rxJava();
RScoredSortedSetRx<SomeObject> set = redisson.getScoredSortedSet("mySet");
Maybe<Integer> rank = set.rank(value);
Maybe<Integer> revRank = set.revRank(value);
Single<Integer> newRank = set.addAndGetRank(2.5, value);
Single<Integer> afterBump = set.addScoreAndGetRank(value, 0.5);
Range queries¶
Elements can be read by rank or by score. Rank ranges use 0-based indices, where negative values count back from the end (-1 is the last element). Score ranges take a lower and upper bound, each with a flag marking it inclusive or exclusive, and accept an optional offset/count for paging. Every query has a Reversed form that walks from the highest score down, and an entryRange counterpart that returns ScoredEntry results carrying the scores.
RScoredSortedSet<SomeObject> set = redisson.getScoredSortedSet("mySet");
// by rank (position)
Collection<SomeObject> top3 = set.valueRangeReversed(0, 2); // 3 highest
Collection<SomeObject> all = set.valueRange(0, -1); // ascending
Collection<ScoredEntry<SomeObject>> withScores = set.entryRange(0, -1);
// by score: 1.0 <= score < 5.0
Collection<SomeObject> band = set.valueRange(1.0, true, 5.0, false);
Collection<SomeObject> page = set.valueRange(1.0, true, 5.0, false, 0, 25); // first 25 in band
int inBand = set.count(1.0, true, 5.0, false);
RScoredSortedSetAsync<SomeObject> set = redisson.getScoredSortedSet("mySet");
RFuture<Collection<SomeObject>> top3 = set.valueRangeReversedAsync(0, 2);
RFuture<Collection<SomeObject>> all = set.valueRangeAsync(0, -1);
RFuture<Collection<ScoredEntry<SomeObject>>> withScores = set.entryRangeAsync(0, -1);
RFuture<Collection<SomeObject>> band = set.valueRangeAsync(1.0, true, 5.0, false);
RFuture<Collection<SomeObject>> page = set.valueRangeAsync(1.0, true, 5.0, false, 0, 25);
RFuture<Integer> inBand = set.countAsync(1.0, true, 5.0, false);
RedissonReactiveClient redisson = redissonClient.reactive();
RScoredSortedSetReactive<SomeObject> set = redisson.getScoredSortedSet("mySet");
Mono<Collection<SomeObject>> top3 = set.valueRangeReversed(0, 2);
Mono<Collection<SomeObject>> all = set.valueRange(0, -1);
Mono<Collection<ScoredEntry<SomeObject>>> withScores = set.entryRange(0, -1);
Mono<Collection<SomeObject>> band = set.valueRange(1.0, true, 5.0, false);
Mono<Collection<SomeObject>> page = set.valueRange(1.0, true, 5.0, false, 0, 25);
Mono<Integer> inBand = set.count(1.0, true, 5.0, false);
RedissonRxClient redisson = redissonClient.rxJava();
RScoredSortedSetRx<SomeObject> set = redisson.getScoredSortedSet("mySet");
Maybe<Collection<SomeObject>> top3 = set.valueRangeReversed(0, 2);
Maybe<Collection<SomeObject>> all = set.valueRange(0, -1);
Maybe<Collection<ScoredEntry<SomeObject>>> withScores = set.entryRange(0, -1);
Maybe<Collection<SomeObject>> band = set.valueRange(1.0, true, 5.0, false);
Maybe<Collection<SomeObject>> page = set.valueRange(1.0, true, 5.0, false, 0, 25);
Single<Integer> inBand = set.count(1.0, true, 5.0, false);
First, last, and polling¶
first/last and firstScore/lastScore read the extreme elements and their scores, and firstEntry/lastEntry return both as a ScoredEntry. pollFirst/pollLast remove and return the lowest or highest element, with count variants for several at once.
RScoredSortedSet<SomeObject> set = redisson.getScoredSortedSet("mySet");
SomeObject lowest = set.first();
SomeObject highest = set.last();
Double lowestScore = set.firstScore();
ScoredEntry<SomeObject> firstEntry = set.firstEntry();
SomeObject popped = set.pollFirst(); // remove and return the lowest
Collection<SomeObject> poppedFew = set.pollFirst(3);
RScoredSortedSetAsync<SomeObject> set = redisson.getScoredSortedSet("mySet");
RFuture<SomeObject> lowest = set.firstAsync();
RFuture<SomeObject> highest = set.lastAsync();
RFuture<Double> lowestScore = set.firstScoreAsync();
RFuture<ScoredEntry<SomeObject>> firstEntry = set.firstEntryAsync();
RFuture<SomeObject> popped = set.pollFirstAsync();
RFuture<Collection<SomeObject>> poppedFew = set.pollFirstAsync(3);
RedissonReactiveClient redisson = redissonClient.reactive();
RScoredSortedSetReactive<SomeObject> set = redisson.getScoredSortedSet("mySet");
Mono<SomeObject> lowest = set.first();
Mono<SomeObject> highest = set.last();
Mono<Double> lowestScore = set.firstScore();
Mono<ScoredEntry<SomeObject>> firstEntry = set.firstEntry();
Mono<SomeObject> popped = set.pollFirst();
Mono<Collection<SomeObject>> poppedFew = set.pollFirst(3);
RedissonRxClient redisson = redissonClient.rxJava();
RScoredSortedSetRx<SomeObject> set = redisson.getScoredSortedSet("mySet");
Maybe<SomeObject> lowest = set.first();
Maybe<SomeObject> highest = set.last();
Maybe<Double> lowestScore = set.firstScore();
Maybe<ScoredEntry<SomeObject>> firstEntry = set.firstEntry();
Maybe<SomeObject> popped = set.pollFirst();
Maybe<Collection<SomeObject>> poppedFew = set.pollFirst(3);
pollFirst/pollLast also have blocking forms that take a Duration timeout, pollFirstEntry/pollLastEntry return the popped element with its score, and pollFirstFromAny/pollLastFromAny pop across several sets in one call.
Removing by rank or score¶
removeRangeByRank and removeRangeByScore delete a whole slice of the set in one call and return how many elements were removed - the natural complement to the range queries above.
Set algebra¶
A scored sorted set can be combined with other named sets. The read* methods return the result and leave this set unchanged, while union, diff, and intersection overwrite this set with the result and return its size. countIntersection returns the size of an intersection without materializing it.
Note
union, diff, and intersection overwrite this set with the result. Use readUnion, readDiff, and readIntersection to combine sets without changing this one.
RScoredSortedSet<SomeObject> set = redisson.getScoredSortedSet("mySet");
// non-destructive: returns the result, this set is unchanged
Collection<SomeObject> u = set.readUnion("set2", "set3");
Collection<SomeObject> i = set.readIntersection("set2");
// SUM scores across sets, weighting set3 twice
Collection<SomeObject> weighted = set.readUnion(Aggregate.SUM, Map.of("set2", 1.0, "set3", 2.0));
// destructive: overwrites this set, returns the new size
int unionSize = set.union("set2", "set3");
Integer common = set.countIntersection("set2");
RScoredSortedSetAsync<SomeObject> set = redisson.getScoredSortedSet("mySet");
RFuture<Collection<SomeObject>> u = set.readUnionAsync("set2", "set3");
RFuture<Collection<SomeObject>> i = set.readIntersectionAsync("set2");
RFuture<Collection<SomeObject>> weighted = set.readUnionAsync(Aggregate.SUM, Map.of("set2", 1.0, "set3", 2.0));
RFuture<Integer> unionSize = set.unionAsync("set2", "set3");
RFuture<Integer> common = set.countIntersectionAsync("set2");
RedissonReactiveClient redisson = redissonClient.reactive();
RScoredSortedSetReactive<SomeObject> set = redisson.getScoredSortedSet("mySet");
Mono<Collection<SomeObject>> u = set.readUnion("set2", "set3");
Mono<Collection<SomeObject>> i = set.readIntersection("set2");
Mono<Collection<SomeObject>> weighted = set.readUnion(Aggregate.SUM, Map.of("set2", 1.0, "set3", 2.0));
Mono<Integer> unionSize = set.union("set2", "set3");
Mono<Integer> common = set.countIntersection("set2");
RedissonRxClient redisson = redissonClient.rxJava();
RScoredSortedSetRx<SomeObject> set = redisson.getScoredSortedSet("mySet");
Maybe<Collection<SomeObject>> u = set.readUnion("set2", "set3");
Maybe<Collection<SomeObject>> i = set.readIntersection("set2");
Maybe<Collection<SomeObject>> weighted = set.readUnion(Aggregate.SUM, Map.of("set2", 1.0, "set3", 2.0));
Single<Integer> unionSize = set.union("set2", "set3");
Single<Integer> common = set.countIntersection("set2");
Overloads accept an Aggregate (SUM, MIN, or MAX) and per-set weights, and readUnionEntries/readIntersectionEntries/readDiffEntries return ScoredEntry results; see the javadoc for the full set.
Data partitioning¶
Although 'RScoredSortedSet' object is cluster compatible its content isn't scaled across multiple master nodes. RScoredSortedSet data partitioning available only in cluster mode and implemented by separate RClusteredScoredSortedSet object. Size is limited by whole Cluster memory. More about partitioning here.
Below is the list of all available RScoredSortedSet implementations:
| RedissonClient method name |
Data partitioning support |
Ultra-fast read/write |
|---|---|---|
| getScoredSortedSet() open-source version |
❌ | ❌ |
| getScoredSortedSet() Redisson PRO version |
❌ | ✔️ |
| getClusteredScoredSortedSet() available only in Redisson PRO |
✔️ | ✔️ |
Code example:
RClusteredScoredSortedSet set = redisson.getClusteredScoredSortedSet("myScoredSet");
set.add(1.1, "v1");
set.add(1.2, "v2");
set.add(1.3, "v3");
ScoredEntry<String> s = set.firstEntry();
ScoredEntry<String> e = set.pollFirstEntry();
Listeners¶
Redisson allows binding listeners per RScoredSortedSet object. This requires the notify-keyspace-events setting to be enabled on Valkey or Redis side.
| Listener class name | Event description | Valkey or Redisnotify-keyspace-events value |
|---|---|---|
| org.redisson.api.listener.TrackingListener | Element created/removed/updated after read operation | - |
| org.redisson.api.listener.ScoredSortedSetAddListener | Element created/updated | Ez |
| org.redisson.api.listener.ScoredSortedSetRemoveListener | Element removed | Ez |
| org.redisson.api.ExpiredObjectListener | RScoredSortedSet object expired |
Ex |
| org.redisson.api.DeletedObjectListener | RScoredSortedSet object deleted |
Eg |
Usage example:
RScoredSortedSet<String> set = redisson.getScoredSortedSet("anySet");
int listenerId = set.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
// ...
}
});
// ...
set.removeListener(listenerId);
LexSortedSet¶
Valkey or Redis based distributed LexSortedSet object for Java stores String elements only and implements java.util.Set<String> interface. Unlike regular sets, LexSortedSet maintains elements in lexicographical (alphabetical dictionary) order automatically. This makes it ideal for use cases requiring sorted string data such as autocomplete systems, alphabetical indexes, and dictionary-style lookups.
This object is thread-safe. Set size is limited to 4 294 967 295 elements. Valkey or Redis uses serialized state to check value uniqueness instead of value's hashCode()/equals() methods. It is an RSortedSet<String>, so first, last, and ordered iteration come for free; the inherited java.util.SortedSet range views (subSet, headSet, tailSet) are not supported - use the range methods below instead.
Lexicographical Ordering¶
LexSortedSet stores all elements in lexicographical order based on byte-by-byte comparison of their string values. Elements are automatically sorted when added, with no additional configuration required. This ordering follows standard dictionary sorting rules where, for example, "apple" comes before "banana", and "a1" comes before "a2".
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// Add elements - automatically sorted lexicographically
set.add("banana");
set.add("apple");
set.add("cherry");
set.add("apricot");
// Elements are stored in order: apple, apricot, banana, cherry
// Get the first (lexicographically smallest) element
String first = set.first(); // returns "apple"
// Get the last (lexicographically largest) element
String last = set.last(); // returns "cherry"
// Poll (retrieve and remove) first element
String polledFirst = set.pollFirst(); // returns "apple"
// Poll (retrieve and remove) last element
String polledLast = set.pollLast(); // returns "cherry"
// Get count of all elements
int count = set.count("a", true, "z", true);
// Check element existence
boolean exists = set.contains("banana");
// Remove element
boolean removed = set.remove("banana");
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// Add elements asynchronously
RFuture<Boolean> f1 = set.addAsync("banana");
RFuture<Boolean> f2 = set.addAsync("apple");
RFuture<Boolean> f3 = set.addAsync("cherry");
RFuture<Boolean> f4 = set.addAsync("apricot");
// Get the first (lexicographically smallest) element
RFuture<String> firstFuture = set.firstAsync();
// Get the last (lexicographically largest) element
RFuture<String> lastFuture = set.lastAsync();
// Poll (retrieve and remove) first element
RFuture<String> polledFirstFuture = set.pollFirstAsync();
// Poll (retrieve and remove) last element
RFuture<String> polledLastFuture = set.pollLastAsync();
// Get count of all elements in range
RFuture<Integer> countFuture = set.countAsync("a", true, "z", true);
// Check element existence
RFuture<Boolean> existsFuture = set.containsAsync("banana");
// Remove element
RFuture<Boolean> removedFuture = set.removeAsync("banana");
RedissonReactiveClient redisson = redissonClient.reactive();
RLexSortedSetReactive set = redisson.getLexSortedSet("myLexSet");
// Add elements reactively
Mono<Boolean> m1 = set.add("banana");
Mono<Boolean> m2 = set.add("apple");
Mono<Boolean> m3 = set.add("cherry");
Mono<Boolean> m4 = set.add("apricot");
// Get the first (lexicographically smallest) element
Mono<String> firstMono = set.first();
// Get the last (lexicographically largest) element
Mono<String> lastMono = set.last();
// Poll (retrieve and remove) first element
Mono<String> polledFirstMono = set.pollFirst();
// Poll (retrieve and remove) last element
Mono<String> polledLastMono = set.pollLast();
// Get count of all elements in range
Mono<Integer> countMono = set.count("a", true, "z", true);
// Check element existence
Mono<Boolean> existsMono = set.contains("banana");
// Remove element
Mono<Boolean> removedMono = set.remove("banana");
RedissonRxClient redisson = redissonClient.rxJava();
RLexSortedSetRx set = redisson.getLexSortedSet("myLexSet");
// Add elements using RxJava
Single<Boolean> s1 = set.add("banana");
Single<Boolean> s2 = set.add("apple");
Single<Boolean> s3 = set.add("cherry");
Single<Boolean> s4 = set.add("apricot");
// Get the first (lexicographically smallest) element
Maybe<String> firstMaybe = set.first();
// Get the last (lexicographically largest) element
Maybe<String> lastMaybe = set.last();
// Poll (retrieve and remove) first element
Maybe<String> polledFirstMaybe = set.pollFirst();
// Poll (retrieve and remove) last element
Maybe<String> polledLastMaybe = set.pollLast();
// Get count of all elements in range
Single<Integer> countSingle = set.count("a", true, "z", true);
// Check element existence
Single<Boolean> existsSingle = set.contains("banana");
// Remove element
Single<Boolean> removedSingle = set.remove("banana");
Range Operations¶
LexSortedSet provides powerful range query capabilities for retrieving elements within specified lexicographical boundaries. Range operations support both inclusive and exclusive bounds, allowing precise control over which elements are included in the results. These operations are highly efficient as they leverage Valkey or Redis's native sorted set commands.
Retrieving Elements by Range
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
set.add("apple");
set.add("apricot");
set.add("banana");
set.add("blueberry");
set.add("cherry");
set.add("cranberry");
// Get elements from start up to specified value (inclusive)
// Returns: apple, apricot, banana
Collection<String> head = set.rangeHead("banana", true);
// Get elements from start up to specified value (exclusive)
// Returns: apple, apricot
Collection<String> headExclusive = set.rangeHead("banana", false);
// Get elements from specified value to end (inclusive)
// Returns: cherry, cranberry
Collection<String> tail = set.rangeTail("cherry", true);
// Get elements from specified value to end (exclusive)
// Returns: cranberry
Collection<String> tailExclusive = set.rangeTail("cherry", false);
// Get elements between two values (both inclusive)
// Returns: apricot, banana, blueberry
Collection<String> range = set.range("apricot", true, "blueberry", true);
// Get elements between two values (start inclusive, end exclusive)
// Returns: apricot, banana
Collection<String> rangeMixed = set.range("apricot", true, "blueberry", false);
// Get elements in reverse lexicographical order
Collection<String> rangeReversed = set.rangeReversed("apricot", true, "cherry", true);
Collection<String> headReversed = set.rangeHeadReversed("cherry", true);
Collection<String> tailReversed = set.rangeTailReversed("banana", true);
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// Add elements
set.addAsync("apple");
set.addAsync("apricot");
set.addAsync("banana");
set.addAsync("blueberry");
set.addAsync("cherry");
set.addAsync("cranberry");
// Get elements from start up to specified value (inclusive)
RFuture<Collection<String>> headFuture = set.rangeHeadAsync("banana", true);
// Get elements from start up to specified value (exclusive)
RFuture<Collection<String>> headExclusiveFuture = set.rangeHeadAsync("banana", false);
// Get elements from specified value to end (inclusive)
RFuture<Collection<String>> tailFuture = set.rangeTailAsync("cherry", true);
// Get elements from specified value to end (exclusive)
RFuture<Collection<String>> tailExclusiveFuture = set.rangeTailAsync("cherry", false);
// Get elements between two values (both inclusive)
RFuture<Collection<String>> rangeFuture = set.rangeAsync("apricot", true, "blueberry", true);
// Get elements between two values (start inclusive, end exclusive)
RFuture<Collection<String>> rangeMixedFuture = set.rangeAsync("apricot", true, "blueberry", false);
// Get elements in reverse lexicographical order
RFuture<Collection<String>> rangeReversedFuture = set.rangeReversedAsync("apricot", true, "cherry", true);
RFuture<Collection<String>> headReversedFuture = set.rangeHeadReversedAsync("cherry", true);
RFuture<Collection<String>> tailReversedFuture = set.rangeTailReversedAsync("banana", true);
RedissonReactiveClient redisson = redissonClient.reactive();
RLexSortedSetReactive set = redisson.getLexSortedSet("myLexSet");
// Add elements
set.add("apple").subscribe();
set.add("apricot").subscribe();
set.add("banana").subscribe();
set.add("blueberry").subscribe();
set.add("cherry").subscribe();
set.add("cranberry").subscribe();
// Get elements from start up to specified value (inclusive)
Mono<Collection<String>> headMono = set.rangeHead("banana", true);
// Get elements from start up to specified value (exclusive)
Mono<Collection<String>> headExclusiveMono = set.rangeHead("banana", false);
// Get elements from specified value to end (inclusive)
Mono<Collection<String>> tailMono = set.rangeTail("cherry", true);
// Get elements from specified value to end (exclusive)
Mono<Collection<String>> tailExclusiveMono = set.rangeTail("cherry", false);
// Get elements between two values (both inclusive)
Mono<Collection<String>> rangeMono = set.range("apricot", true, "blueberry", true);
// Get elements between two values (start inclusive, end exclusive)
Mono<Collection<String>> rangeMixedMono = set.range("apricot", true, "blueberry", false);
// Get elements in reverse lexicographical order
Mono<Collection<String>> rangeReversedMono = set.rangeReversed("apricot", true, "cherry", true);
Mono<Collection<String>> headReversedMono = set.rangeHeadReversed("cherry", true);
Mono<Collection<String>> tailReversedMono = set.rangeTailReversed("banana", true);
RedissonRxClient redisson = redissonClient.rxJava();
RLexSortedSetRx set = redisson.getLexSortedSet("myLexSet");
// Add elements
set.add("apple").subscribe();
set.add("apricot").subscribe();
set.add("banana").subscribe();
set.add("blueberry").subscribe();
set.add("cherry").subscribe();
set.add("cranberry").subscribe();
// Get elements from start up to specified value (inclusive)
Single<Collection<String>> headSingle = set.rangeHead("banana", true);
// Get elements from start up to specified value (exclusive)
Single<Collection<String>> headExclusiveSingle = set.rangeHead("banana", false);
// Get elements from specified value to end (inclusive)
Single<Collection<String>> tailSingle = set.rangeTail("cherry", true);
// Get elements from specified value to end (exclusive)
Single<Collection<String>> tailExclusiveSingle = set.rangeTail("cherry", false);
// Get elements between two values (both inclusive)
Single<Collection<String>> rangeSingle = set.range("apricot", true, "blueberry", true);
// Get elements between two values (start inclusive, end exclusive)
Single<Collection<String>> rangeMixedSingle = set.range("apricot", true, "blueberry", false);
// Get elements in reverse lexicographical order
Single<Collection<String>> rangeReversedSingle = set.rangeReversed("apricot", true, "cherry", true);
Single<Collection<String>> headReversedSingle = set.rangeHeadReversed("cherry", true);
Single<Collection<String>> tailReversedSingle = set.rangeTailReversed("banana", true);
Paginating Range Results
Every range method has an overload taking an offset and a count that skip and limit the results - the basis for autocomplete and paged browsing. For example, fetching matches ten at a time:
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// First 10 elements at or after "ap" - e.g. autocomplete page 1
Collection<String> page1 = set.rangeTail("ap", true, 0, 10);
// The next 10
Collection<String> page2 = set.rangeTail("ap", true, 10, 10);
// Paging within a bounded range
Collection<String> firstTen = set.range("a", true, "z", true, 0, 10);
RedissonReactiveClient redisson = redissonClient.reactive();
RLexSortedSetReactive set = redisson.getLexSortedSet("myLexSet");
Mono<Collection<String>> page1 = set.rangeTail("ap", true, 0, 10);
Mono<Collection<String>> page2 = set.rangeTail("ap", true, 10, 10);
Mono<Collection<String>> firstTen = set.range("a", true, "z", true, 0, 10);
RedissonRxClient redisson = redissonClient.rxJava();
RLexSortedSetRx set = redisson.getLexSortedSet("myLexSet");
Single<Collection<String>> page1 = set.rangeTail("ap", true, 0, 10);
Single<Collection<String>> page2 = set.rangeTail("ap", true, 10, 10);
Single<Collection<String>> firstTen = set.range("a", true, "z", true, 0, 10);
There is also a positional range(int startIndex, int endIndex) (with a rangeAsync form) that selects by index rather than by value - for example range(0, 9) returns the first ten elements. It is available on the synchronous and asynchronous interfaces.
Element Position
rank returns the 0-based position of an element in lexicographical order and revRank its position from the end; both are empty when the element is absent, which is handy for showing where a term falls in an index.
Counting Elements in Range
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
set.add("apple");
set.add("apricot");
set.add("banana");
set.add("blueberry");
set.add("cherry");
// Count elements from start up to specified value (inclusive)
int headCount = set.countHead("banana", true); // returns 3
// Count elements from start up to specified value (exclusive)
int headCountExclusive = set.countHead("banana", false); // returns 2
// Count elements from specified value to end (inclusive)
int tailCount = set.countTail("banana", true); // returns 3
// Count elements from specified value to end (exclusive)
int tailCountExclusive = set.countTail("banana", false); // returns 2
// Count elements between two values (both inclusive)
int rangeCount = set.count("apricot", true, "cherry", true); // returns 4
// Count elements between two values (start inclusive, end exclusive)
int rangeCountMixed = set.count("apricot", true, "cherry", false); // returns 3
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// Count elements from start up to specified value (inclusive)
RFuture<Integer> headCountFuture = set.countHeadAsync("banana", true);
// Count elements from start up to specified value (exclusive)
RFuture<Integer> headCountExclusiveFuture = set.countHeadAsync("banana", false);
// Count elements from specified value to end (inclusive)
RFuture<Integer> tailCountFuture = set.countTailAsync("banana", true);
// Count elements from specified value to end (exclusive)
RFuture<Integer> tailCountExclusiveFuture = set.countTailAsync("banana", false);
// Count elements between two values (both inclusive)
RFuture<Integer> rangeCountFuture = set.countAsync("apricot", true, "cherry", true);
// Count elements between two values (start inclusive, end exclusive)
RFuture<Integer> rangeCountMixedFuture = set.countAsync("apricot", true, "cherry", false);
RedissonReactiveClient redisson = redissonClient.reactive();
RLexSortedSetReactive set = redisson.getLexSortedSet("myLexSet");
// Count elements from start up to specified value (inclusive)
Mono<Integer> headCountMono = set.countHead("banana", true);
// Count elements from start up to specified value (exclusive)
Mono<Integer> headCountExclusiveMono = set.countHead("banana", false);
// Count elements from specified value to end (inclusive)
Mono<Integer> tailCountMono = set.countTail("banana", true);
// Count elements from specified value to end (exclusive)
Mono<Integer> tailCountExclusiveMono = set.countTail("banana", false);
// Count elements between two values (both inclusive)
Mono<Integer> rangeCountMono = set.count("apricot", true, "cherry", true);
// Count elements between two values (start inclusive, end exclusive)
Mono<Integer> rangeCountMixedMono = set.count("apricot", true, "cherry", false);
RedissonRxClient redisson = redissonClient.rxJava();
RLexSortedSetRx set = redisson.getLexSortedSet("myLexSet");
// Count elements from start up to specified value (inclusive)
Single<Integer> headCountSingle = set.countHead("banana", true);
// Count elements from start up to specified value (exclusive)
Single<Integer> headCountExclusiveSingle = set.countHead("banana", false);
// Count elements from specified value to end (inclusive)
Single<Integer> tailCountSingle = set.countTail("banana", true);
// Count elements from specified value to end (exclusive)
Single<Integer> tailCountExclusiveSingle = set.countTail("banana", false);
// Count elements between two values (both inclusive)
Single<Integer> rangeCountSingle = set.count("apricot", true, "cherry", true);
// Count elements between two values (start inclusive, end exclusive)
Single<Integer> rangeCountMixedSingle = set.count("apricot", true, "cherry", false);
Removing Elements by Range
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
set.add("apple");
set.add("apricot");
set.add("banana");
set.add("blueberry");
set.add("cherry");
set.add("cranberry");
// Remove elements from start up to specified value (inclusive)
int removedHead = set.removeRangeHead("apricot", true); // removes apple, apricot
// Remove elements from start up to specified value (exclusive)
int removedHeadExclusive = set.removeRangeHead("banana", false); // removes elements before banana
// Remove elements from specified value to end (inclusive)
int removedTail = set.removeRangeTail("cherry", true); // removes cherry, cranberry
// Remove elements from specified value to end (exclusive)
int removedTailExclusive = set.removeRangeTail("cherry", false); // removes elements after cherry
// Remove elements between two values (both inclusive)
int removedRange = set.removeRange("banana", true, "cherry", true);
// Remove elements between two values (start inclusive, end exclusive)
int removedRangeMixed = set.removeRange("banana", true, "cherry", false);
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// Remove elements from start up to specified value (inclusive)
RFuture<Integer> removedHeadFuture = set.removeRangeHeadAsync("apricot", true);
// Remove elements from start up to specified value (exclusive)
RFuture<Integer> removedHeadExclusiveFuture = set.removeRangeHeadAsync("banana", false);
// Remove elements from specified value to end (inclusive)
RFuture<Integer> removedTailFuture = set.removeRangeTailAsync("cherry", true);
// Remove elements from specified value to end (exclusive)
RFuture<Integer> removedTailExclusiveFuture = set.removeRangeTailAsync("cherry", false);
// Remove elements between two values (both inclusive)
RFuture<Integer> removedRangeFuture = set.removeRangeAsync("banana", true, "cherry", true);
// Remove elements between two values (start inclusive, end exclusive)
RFuture<Integer> removedRangeMixedFuture = set.removeRangeAsync("banana", true, "cherry", false);
RedissonReactiveClient redisson = redissonClient.reactive();
RLexSortedSetReactive set = redisson.getLexSortedSet("myLexSet");
// Remove elements from start up to specified value (inclusive)
Mono<Integer> removedHeadMono = set.removeRangeHead("apricot", true);
// Remove elements from start up to specified value (exclusive)
Mono<Integer> removedHeadExclusiveMono = set.removeRangeHead("banana", false);
// Remove elements from specified value to end (inclusive)
Mono<Integer> removedTailMono = set.removeRangeTail("cherry", true);
// Remove elements from specified value to end (exclusive)
Mono<Integer> removedTailExclusiveMono = set.removeRangeTail("cherry", false);
// Remove elements between two values (both inclusive)
Mono<Integer> removedRangeMono = set.removeRange("banana", true, "cherry", true);
// Remove elements between two values (start inclusive, end exclusive)
Mono<Integer> removedRangeMixedMono = set.removeRange("banana", true, "cherry", false);
RedissonRxClient redisson = redissonClient.rxJava();
RLexSortedSetRx set = redisson.getLexSortedSet("myLexSet");
// Remove elements from start up to specified value (inclusive)
Single<Integer> removedHeadSingle = set.removeRangeHead("apricot", true);
// Remove elements from start up to specified value (exclusive)
Single<Integer> removedHeadExclusiveSingle = set.removeRangeHead("banana", false);
// Remove elements from specified value to end (inclusive)
Single<Integer> removedTailSingle = set.removeRangeTail("cherry", true);
// Remove elements from specified value to end (exclusive)
Single<Integer> removedTailExclusiveSingle = set.removeRangeTail("cherry", false);
// Remove elements between two values (both inclusive)
Single<Integer> removedRangeSingle = set.removeRange("banana", true, "cherry", true);
// Remove elements between two values (start inclusive, end exclusive)
Single<Integer> removedRangeMixedSingle = set.removeRange("banana", true, "cherry", false);
Listeners¶
Redisson allows binding listeners per RLexSortedSet object to receive notifications on set modifications. This requires the notify-keyspace-events setting to be enabled on Valkey or Redis side.
| Listener class name | Event description | Valkey or Redis notify-keyspace-events value |
|---|---|---|
| org.redisson.api.listener.TrackingListener | Element created/removed/updated after read operation | - |
| org.redisson.api.listener.ScoredSortedSetAddListener | Element added | Ez |
| org.redisson.api.listener.ScoredSortedSetRemoveListener | Element removed | Ez |
| org.redisson.api.ExpiredObjectListener | RLexSortedSet object expired |
Ex |
| org.redisson.api.DeletedObjectListener | RLexSortedSet object deleted |
Eg |
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// Listener for element additions
int addListenerId = set.addListener(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
System.out.println("Element added to set: " + name);
}
});
// Listener for element removals
int removeListenerId = set.addListener(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
System.out.println("Element removed from set: " + name);
}
});
// Listener for set expiration
int expireListenerId = set.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("Set expired: " + name);
}
});
// Listener for set deletion
int deleteListenerId = set.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("Set deleted: " + name);
}
});
// Listener for tracking changes after read operations
int trackingListenerId = set.addListener(new TrackingListener() {
@Override
public void onChange(String name) {
System.out.println("Set changed after read: " + name);
}
});
// Remove listeners when no longer needed
set.removeListener(addListenerId);
set.removeListener(removeListenerId);
set.removeListener(expireListenerId);
set.removeListener(deleteListenerId);
set.removeListener(trackingListenerId);
RLexSortedSet set = redisson.getLexSortedSet("myLexSet");
// Add listener for element additions
RFuture<Integer> addListenerFuture = set.addListenerAsync(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
System.out.println("Element added to set: " + name);
}
});
// Add listener for element removals
RFuture<Integer> removeListenerFuture = set.addListenerAsync(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
System.out.println("Element removed from set: " + name);
}
});
// Add listener for set expiration
RFuture<Integer> expireListenerFuture = set.addListenerAsync(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("Set expired: " + name);
}
});
// Add listener for set deletion
RFuture<Integer> deleteListenerFuture = set.addListenerAsync(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("Set deleted: " + name);
}
});
// Remove listeners
addListenerFuture.thenAccept(listenerId -> {
set.removeListenerAsync(listenerId);
});
RedissonReactiveClient redisson = redissonClient.reactive();
RLexSortedSetReactive set = redisson.getLexSortedSet("myLexSet");
// Add listener for element additions
Mono<Integer> addListenerMono = set.addListener(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
System.out.println("Element added to set: " + name);
}
});
// Add listener for element removals
Mono<Integer> removeListenerMono = set.addListener(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
System.out.println("Element removed from set: " + name);
}
});
// Add listener for set expiration
Mono<Integer> expireListenerMono = set.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("Set expired: " + name);
}
});
// Add listener for set deletion
Mono<Integer> deleteListenerMono = set.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("Set deleted: " + name);
}
});
// Remove listener
addListenerMono
.flatMap(listenerId -> set.removeListener(listenerId))
.subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RLexSortedSetRx set = redisson.getLexSortedSet("myLexSet");
// Add listener for element additions
Single<Integer> addListenerSingle = set.addListener(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
System.out.println("Element added to set: " + name);
}
});
// Add listener for element removals
Single<Integer> removeListenerSingle = set.addListener(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
System.out.println("Element removed from set: " + name);
}
});
// Add listener for set expiration
Single<Integer> expireListenerSingle = set.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("Set expired: " + name);
}
});
// Add listener for set deletion
Single<Integer> deleteListenerSingle = set.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("Set deleted: " + name);
}
});
// Remove listener
addListenerSingle
.flatMapCompletable(listenerId -> set.removeListener(listenerId))
.subscribe();
List¶
Redisson's RList object implements the List interface, providing a distributed and concurrent list backed by Valkey or Redis. This allows multiple applications or servers to share and manipulate list data seamlessly. This object is thread-safe.
Basic Operations¶
RList provides all standard Java List operations including adding, removing, and checking for elements.
Code example of creating and adding elements:
RList<String> list = redisson.getList("myList");
// Add elements to the end
list.add("element1");
list.add("element2");
// Add multiple elements at once
list.addAll(Arrays.asList("element3", "element4", "element5"));
// Add element at specific position
list.add(0, "firstElement");
// Add element before another element
list.addBefore("element2", "beforeElement2");
// Add element after another element
list.addAfter("element2", "afterElement2");
RList<String> list = redisson.getList("myList");
// Add elements to the end
RFuture<Boolean> future1 = list.addAsync("element1");
RFuture<Boolean> future2 = list.addAsync("element2");
// Add multiple elements at once
RFuture<Boolean> future3 = list.addAllAsync(Arrays.asList("element3", "element4", "element5"));
// Add element at specific position
RFuture<Void> future4 = list.addAsync(0, "firstElement");
// Add element before another element
RFuture<Integer> future5 = list.addBeforeAsync("element2", "beforeElement2");
// Add element after another element
RFuture<Integer> future6 = list.addAfterAsync("element2", "afterElement2");
RedissonReactiveClient redisson = redissonClient.reactive();
RListReactive<String> list = redisson.getList("myList");
// Add elements to the end
Mono<Boolean> mono1 = list.add("element1");
Mono<Boolean> mono2 = list.add("element2");
// Add multiple elements at once
Mono<Boolean> mono3 = list.addAll(Arrays.asList("element3", "element4", "element5"));
// Add element at specific position
Mono<Void> mono4 = list.add(0, "firstElement");
// Add element before another element
Mono<Integer> mono5 = list.addBefore("element2", "beforeElement2");
// Add element after another element
Mono<Integer> mono6 = list.addAfter("element2", "afterElement2");
RedissonRxClient redisson = redissonClient.rxJava();
RListRx<String> list = redisson.getList("myList");
// Add elements to the end
Single<Boolean> single1 = list.add("element1");
Single<Boolean> single2 = list.add("element2");
// Add multiple elements at once
Single<Boolean> single3 = list.addAll(Arrays.asList("element3", "element4", "element5"));
// Add element at specific position
Completable completable = list.add(0, "firstElement");
// Add element before another element
Single<Integer> single4 = list.addBefore("element2", "beforeElement2");
// Add element after another element
Single<Integer> single5 = list.addAfter("element2", "afterElement2");
Code example of removing elements:
RList<String> list = redisson.getList("myList");
// Remove by object
boolean removed = list.remove("element1");
// Remove by index
String removedElement = list.remove(0);
// Remove multiple elements
list.removeAll(Arrays.asList("element2", "element3"));
// Remove elements not in the specified collection
list.retainAll(Arrays.asList("element4", "element5"));
// Fast remove the element at the given index (no return value)
list.fastRemove(0);
// Remove up to N occurrences of a value (LREM)
boolean removedOccurrences = list.remove("element2", 2);
// Clear all elements
list.clear();
RList<String> list = redisson.getList("myList");
// Remove by object
RFuture<Boolean> future1 = list.removeAsync("element1");
// Remove by index
RFuture<String> future2 = list.removeAsync(0);
// Remove multiple elements
RFuture<Boolean> future3 = list.removeAllAsync(Arrays.asList("element2", "element3"));
// Remove elements not in the specified collection
RFuture<Boolean> future4 = list.retainAllAsync(Arrays.asList("element4", "element5"));
// Fast remove the element at the given index (no return value)
RFuture<Void> future5 = list.fastRemoveAsync(0);
// Remove up to N occurrences of a value (LREM)
RFuture<Boolean> future7 = list.removeAsync("element2", 2);
// Clear all elements
RFuture<Boolean> future6 = list.deleteAsync();
RedissonReactiveClient redisson = redissonClient.reactive();
RListReactive<String> list = redisson.getList("myList");
// Remove by object
Mono<Boolean> mono1 = list.remove("element1");
// Remove by index
Mono<String> mono2 = list.remove(0);
// Remove multiple elements
Mono<Boolean> mono3 = list.removeAll(Arrays.asList("element2", "element3"));
// Remove elements not in the specified collection
Mono<Boolean> mono4 = list.retainAll(Arrays.asList("element4", "element5"));
// Fast remove the element at the given index (no return value)
Mono<Void> mono5 = list.fastRemove(0);
// Clear all elements
Mono<Boolean> mono6 = list.delete();
RedissonRxClient redisson = redissonClient.rxJava();
RListRx<String> list = redisson.getList("myList");
// Remove by object
Single<Boolean> single1 = list.remove("element1");
// Remove by index
Single<String> single2 = list.remove(0);
// Remove multiple elements
Single<Boolean> single3 = list.removeAll(Arrays.asList("element2", "element3"));
// Remove elements not in the specified collection
Single<Boolean> single4 = list.retainAll(Arrays.asList("element4", "element5"));
// Fast remove the element at the given index (no return value)
Completable completable = list.fastRemove(0);
// Clear all elements
Single<Boolean> single5 = list.delete();
The remove(element, count) form shown above removes up to a given number of occurrences of a value and is available on the synchronous and asynchronous interfaces only.
Code example of checking and searching:
RList<String> list = redisson.getList("myList");
// Check if element exists
boolean contains = list.contains("element1");
// Check if all elements exist
boolean containsAll = list.containsAll(Arrays.asList("element1", "element2"));
// Check if list is empty
boolean isEmpty = list.isEmpty();
// Get list size
int size = list.size();
// Find index of element
int index = list.indexOf("element1");
int lastIndex = list.lastIndexOf("element1");
RList<String> list = redisson.getList("myList");
// Check if element exists
RFuture<Boolean> future1 = list.containsAsync("element1");
// Check if all elements exist
RFuture<Boolean> future2 = list.containsAllAsync(Arrays.asList("element1", "element2"));
// Get list size
RFuture<Integer> future3 = list.sizeAsync();
// Find index of element
RFuture<Integer> future4 = list.indexOfAsync("element1");
RFuture<Integer> future5 = list.lastIndexOfAsync("element1");
RedissonReactiveClient redisson = redissonClient.reactive();
RListReactive<String> list = redisson.getList("myList");
// Check if element exists
Mono<Boolean> mono1 = list.contains("element1");
// Check if all elements exist
Mono<Boolean> mono2 = list.containsAll(Arrays.asList("element1", "element2"));
// Get list size
Mono<Integer> mono3 = list.size();
// Find index of element
Mono<Integer> mono4 = list.indexOf("element1");
Mono<Integer> mono5 = list.lastIndexOf("element1");
RedissonRxClient redisson = redissonClient.rxJava();
RListRx<String> list = redisson.getList("myList");
// Check if element exists
Single<Boolean> single1 = list.contains("element1");
// Check if all elements exist
Single<Boolean> single2 = list.containsAll(Arrays.asList("element1", "element2"));
// Get list size
Single<Integer> single3 = list.size();
// Find index of element
Single<Integer> single4 = list.indexOf("element1");
Single<Integer> single5 = list.lastIndexOf("element1");
Indexing and Access Patterns¶
RList provides multiple ways to access elements by index, including bulk operations for efficient data retrieval.
Code example of single element access:
RList<String> list = redisson.getList("myList");
// Get element at index (0-based)
RFuture<String> future1 = list.getAsync(0);
// Set element at index
RFuture<String> future2 = list.setAsync(0, "newElement");
// Fast set without returning old value
RFuture<Void> future3 = list.fastSetAsync(0, "anotherElement");
RedissonReactiveClient redisson = redissonClient.reactive();
RListReactive<String> list = redisson.getList("myList");
// Get element at index (0-based)
Mono<String> mono1 = list.get(0);
// Set element at index
Mono<String> mono2 = list.set(0, "newElement");
// Fast set without returning old value
Mono<Void> mono3 = list.fastSet(0, "anotherElement");
RedissonRxClient redisson = redissonClient.rxJava();
RListRx<String> list = redisson.getList("myList");
// Get element at index (0-based)
Single<String> single1 = list.get(0);
// Set element at index
Single<String> single2 = list.set(0, "newElement");
// Fast set without returning old value
Completable completable = list.fastSet(0, "anotherElement");
Code example of bulk element access:
RList<String> list = redisson.getList("myList");
// Get elements at multiple indexes
List<String> elements = list.get(0, 2, 4, 6);
// Read all elements at once
List<String> allElements = list.readAll();
// Get range of elements (inclusive indexes)
// Negative indexes supported: -1 = last element, -2 = second to last, etc.
List<String> rangeEnd = list.range(5); // Elements from index 0 to 5
List<String> rangeFromTo = list.range(2, 8); // Elements from index 2 to 8
RList<String> list = redisson.getList("myList");
// Get elements at multiple indexes
RFuture<List<String>> future1 = list.getAsync(0, 2, 4, 6);
// Read all elements at once
RFuture<List<String>> future2 = list.readAllAsync();
// Get range of elements (inclusive indexes)
RFuture<List<String>> future3 = list.rangeAsync(5); // Elements from index 0 to 5
RFuture<List<String>> future4 = list.rangeAsync(2, 8); // Elements from index 2 to 8
RedissonReactiveClient redisson = redissonClient.reactive();
RListReactive<String> list = redisson.getList("myList");
// Get elements at multiple indexes
Mono<List<String>> mono1 = list.get(0, 2, 4, 6);
// Read all elements at once
Mono<List<String>> mono2 = list.readAll();
// Get range of elements (inclusive indexes)
Mono<List<String>> mono3 = list.range(5); // Elements from index 0 to 5
Mono<List<String>> mono4 = list.range(2, 8); // Elements from index 2 to 8
RedissonRxClient redisson = redissonClient.rxJava();
RListRx<String> list = redisson.getList("myList");
// Get elements at multiple indexes
Single<List<String>> single1 = list.get(0, 2, 4, 6);
// Read all elements at once
Single<List<String>> single2 = list.readAll();
// Get range of elements (inclusive indexes)
Single<List<String>> single3 = list.range(5); // Elements from index 0 to 5
Single<List<String>> single4 = list.range(2, 8); // Elements from index 2 to 8
Code example of subList and trimming:
RList<String> list = redisson.getList("myList");
// Get a sublist (returns a view backed by the original list)
RList<String> subList = list.subList(2, 5);
// Trim list to keep only elements in specified range (inclusive)
// All other elements are removed
list.trim(0, 9); // Keep only first 10 elements
Code example of iteration:
RList<String> list = redisson.getList("myList");
// Standard iteration (bulk-loaded for efficiency)
for (String element : list) {
System.out.println(element);
}
// Using ListIterator
ListIterator<String> iterator = list.listIterator();
while (iterator.hasNext()) {
String element = iterator.next();
// Can also use iterator.previous(), iterator.set(), etc.
}
// Distributed iterator (shared across multiple applications)
Iterator<String> distIterator = list.distributedIterator("iteratorName", 100);
while (distIterator.hasNext()) {
String element = distIterator.next();
}
Sorting¶
RList is sortable. readSort returns the elements ordered numerically without modifying the list, while sortTo sorts and stores the result into another list, returning the destination size. The read method is named readSorted on the Reactive and RxJava3 interfaces.
By default readSort orders elements numerically; the readSortAlpha variants sort lexicographically, and further overloads sort by an external key pattern, fetch other keys per element, and page the result - see the RSortable javadoc.
Listeners¶
Redisson allows binding listeners to RList objects to receive notifications on list modifications.
This requires the notify-keyspace-events setting to be enabled on Valkey or Redis side.
Available Listener Types:
| Listener | Event | Description | Valkey or Redis notify-keyspace-events value |
|---|---|---|---|
ListAddListener |
onListAdd |
Triggered when an element is added to the list | El |
ListInsertListener |
onListInsert |
Triggered when an element is inserted into the list | El |
ListSetListener |
onListSet |
Triggered when an element value is changed | El |
ListRemoveListener |
onListRemove |
Triggered when an element is removed from the list | El |
ListTrimListener |
onListTrim |
Triggered when the list is trimmed | El |
ExpiredObjectListener |
onExpired |
Triggered when the list expires | Ex |
DeletedObjectListener |
onDeleted |
Triggered when the list is deleted | Eg |
Code example of adding Listeners:
RList<String> list = redisson.getList("myList");
// Add listener for element additions
int addListenerId = list.addListener(new ListAddListener() {
@Override
public void onListAdd(String name) {
System.out.println("Element added to list: " + name);
}
});
// Add listener for element insertions
int insertListenerId = list.addListener(new ListInsertListener() {
@Override
public void onListInsert(String name) {
System.out.println("Element inserted into list: " + name);
}
});
// Add listener for element value changes
int setListenerId = list.addListener(new ListSetListener() {
@Override
public void onListSet(String name) {
System.out.println("Element value changed in list: " + name);
}
});
// Add listener for element removals
int removeListenerId = list.addListener(new ListRemoveListener() {
@Override
public void onListRemove(String name) {
System.out.println("Element removed from list: " + name);
}
});
// Add listener for list trimming
int trimListenerId = list.addListener(new ListTrimListener() {
@Override
public void onListTrim(String name) {
System.out.println("List trimmed: " + name);
}
});
// Add listener for list expiration
int expireListenerId = list.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("List expired: " + name);
}
});
// Add listener for list deletion
int deleteListenerId = list.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("List deleted: " + name);
}
});
// Remove listeners when no longer needed
list.removeListener(addListenerId);
list.removeListener(insertListenerId);
list.removeListener(setListenerId);
list.removeListener(removeListenerId);
list.removeListener(trimListenerId);
list.removeListener(expireListenerId);
list.removeListener(deleteListenerId);
RList<String> list = redisson.getList("myList");
// Add listener for element additions
RFuture<Integer> addListenerFuture = list.addListenerAsync(new ListAddListener() {
@Override
public void onListAdd(String name) {
System.out.println("Element added to list: " + name);
}
});
// Add listener for element removals
RFuture<Integer> removeListenerFuture = list.addListenerAsync(new ListRemoveListener() {
@Override
public void onListRemove(String name) {
System.out.println("Element removed from list: " + name);
}
});
// Add listener for list expiration
RFuture<Integer> expireListenerFuture = list.addListenerAsync(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("List expired: " + name);
}
});
// Add listener for list deletion
RFuture<Integer> deleteListenerFuture = list.addListenerAsync(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("List deleted: " + name);
}
});
// Remove listeners
addListenerFuture.thenAccept(listenerId -> {
list.removeListenerAsync(listenerId);
});
RedissonReactiveClient redisson = redissonClient.reactive();
RListReactive<String> list = redisson.getList("myList");
// Add listener for element additions
Mono<Integer> addListenerMono = list.addListener(new ListAddListener() {
@Override
public void onListAdd(String name) {
System.out.println("Element added to list: " + name);
}
});
// Add listener for element removals
Mono<Integer> removeListenerMono = list.addListener(new ListRemoveListener() {
@Override
public void onListRemove(String name) {
System.out.println("Element removed from list: " + name);
}
});
// Add listener for list expiration
Mono<Integer> expireListenerMono = list.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("List expired: " + name);
}
});
// Add listener for list deletion
Mono<Integer> deleteListenerMono = list.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("List deleted: " + name);
}
});
// Remove listener
addListenerMono
.flatMap(listenerId -> list.removeListener(listenerId))
.subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RListRx<String> list = redisson.getList("myList");
// Add listener for element additions
Single<Integer> addListenerSingle = list.addListener(new ListAddListener() {
@Override
public void onListAdd(String name) {
System.out.println("Element added to list: " + name);
}
});
// Add listener for element removals
Single<Integer> removeListenerSingle = list.addListener(new ListRemoveListener() {
@Override
public void onListRemove(String name) {
System.out.println("Element removed from list: " + name);
}
});
// Add listener for list expiration
Single<Integer> expireListenerSingle = list.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
System.out.println("List expired: " + name);
}
});
// Add listener for list deletion
Single<Integer> deleteListenerSingle = list.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
System.out.println("List deleted: " + name);
}
});
// Remove listener
addListenerSingle
.flatMapCompletable(listenerId -> list.removeListener(listenerId))
.subscribe();
Array¶
Redisson's RArray is a distributed array that stores values by sparse, non-negative integer index, backed by a Redis array. This object is thread-safe.
Indexes are sparse: only the indexes that are written occupy space, so an array can hold values at widely separated positions without allocating the gaps between them.
Requires Redis 8.8+.
Setting and reading values¶
set writes a value at a single index, a run of values starting at an index, or a batch of index/value pairs supplied as a map; each form returns the number of values written. get reads the value at one index or, given several indexes, the values at each of them. isSet reports whether an index currently holds a value.
RArray<Integer> array = redisson.getArray("myArray");
long written = array.set(0, 100);
array.set(10, 200, 300); // values at indexes 10 and 11
array.set(Map.of(50L, 500, 99L, 999)); // values at indexes 50 and 99
Integer value = array.get(0);
List<Integer> values = array.get(0, 10, 50);
boolean present = array.isSet(0);
RArrayAsync<Integer> array = redisson.getArray("myArray");
RFuture<Long> written = array.setAsync(0, 100);
RFuture<Long> run = array.setAsync(10, 200, 300);
RFuture<Long> batch = array.setAsync(Map.of(50L, 500, 99L, 999));
RFuture<Integer> value = array.getAsync(0);
RFuture<List<Integer>> values = array.getAsync(0, 10, 50);
RFuture<Boolean> present = array.isSetAsync(0);
RedissonReactiveClient redisson = redissonClient.reactive();
RArrayReactive<Integer> array = redisson.getArray("myArray");
Mono<Long> written = array.set(0, 100);
Mono<Long> run = array.set(10, 200, 300);
Mono<Long> batch = array.set(Map.of(50L, 500, 99L, 999));
Mono<Integer> value = array.get(0);
Mono<List<Integer>> values = array.get(0, 10, 50);
Mono<Boolean> present = array.isSet(0);
RedissonRxClient redisson = redissonClient.rxJava();
RArrayRx<Integer> array = redisson.getArray("myArray");
Single<Long> written = array.set(0, 100);
Single<Long> run = array.set(10, 200, 300);
Single<Long> batch = array.set(Map.of(50L, 500, 99L, 999));
Maybe<Integer> value = array.get(0);
Single<List<Integer>> values = array.get(0, 10, 50);
Single<Boolean> present = array.isSet(0);
Ranges and counting¶
range returns the values stored in an inclusive index range, while scan returns the same range as ArrayEntry objects and accepts an optional limit. count returns the number of stored values, either across the whole array or within a range, and countMatches counts values equal to a given value in a range. length returns the array length, one position past the highest occupied index.
RArray<Integer> array = redisson.getArray("myArray");
List<Integer> values = array.range(0, 100);
List<ArrayEntry<Integer>> entries = array.scan(0, 100);
List<ArrayEntry<Integer>> firstTen = array.scan(0, 100, 10);
long total = array.count();
long inRange = array.count(0, 100);
long matches = array.countMatches(0, 100, 200);
long length = array.length();
RArrayAsync<Integer> array = redisson.getArray("myArray");
RFuture<List<Integer>> values = array.rangeAsync(0, 100);
RFuture<List<ArrayEntry<Integer>>> entries = array.scanAsync(0, 100);
RFuture<List<ArrayEntry<Integer>>> firstTen = array.scanAsync(0, 100, 10);
RFuture<Long> total = array.countAsync();
RFuture<Long> inRange = array.countAsync(0, 100);
RFuture<Long> matches = array.countMatchesAsync(0, 100, 200);
RFuture<Long> length = array.lengthAsync();
RedissonReactiveClient redisson = redissonClient.reactive();
RArrayReactive<Integer> array = redisson.getArray("myArray");
Mono<List<Integer>> values = array.range(0, 100);
Mono<List<ArrayEntry<Integer>>> entries = array.scan(0, 100);
Mono<List<ArrayEntry<Integer>>> firstTen = array.scan(0, 100, 10);
Mono<Long> total = array.count();
Mono<Long> inRange = array.count(0, 100);
Mono<Long> matches = array.countMatches(0, 100, 200);
Mono<Long> length = array.length();
RedissonRxClient redisson = redissonClient.rxJava();
RArrayRx<Integer> array = redisson.getArray("myArray");
Single<List<Integer>> values = array.range(0, 100);
Single<List<ArrayEntry<Integer>>> entries = array.scan(0, 100);
Single<List<ArrayEntry<Integer>>> firstTen = array.scan(0, 100, 10);
Single<Long> total = array.count();
Single<Long> inRange = array.count(0, 100);
Single<Long> matches = array.countMatches(0, 100, 200);
Single<Long> length = array.length();
Deleting values¶
delete removes the values at the given indexes, deleteRange removes everything in an inclusive index range, and deleteRanges removes several ranges at once, taking start/end index pairs. Each returns the number of values removed.
Appending and the insert cursor¶
Instead of choosing indexes explicitly, insert appends values at consecutive indexes starting from the array's current insert cursor and returns the index of the last value written. next returns the index the cursor will use next, or null when it is exhausted, and seek repositions the cursor. ring appends values into a fixed-size ring buffer, wrapping around once the size is reached. lastItems returns the most recently inserted values, optionally in reverse order.
RArray<Integer> array = redisson.getArray("myArray");
long lastIndex = array.insert(1, 2, 3); // appended at the current cursor
Long nextIndex = array.next();
boolean moved = array.seek(0);
array.ring(1000, 10, 20, 30); // ring buffer of size 1000
List<Integer> recent = array.lastItems(5);
List<Integer> recentReversed = array.lastItems(5, true);
RArrayAsync<Integer> array = redisson.getArray("myArray");
RFuture<Long> lastIndex = array.insertAsync(1, 2, 3);
RFuture<Long> nextIndex = array.nextAsync();
RFuture<Boolean> moved = array.seekAsync(0);
RFuture<Long> ringIndex = array.ringAsync(1000, 10, 20, 30);
RFuture<List<Integer>> recent = array.lastItemsAsync(5);
RFuture<List<Integer>> recentReversed = array.lastItemsAsync(5, true);
RedissonReactiveClient redisson = redissonClient.reactive();
RArrayReactive<Integer> array = redisson.getArray("myArray");
Mono<Long> lastIndex = array.insert(1, 2, 3);
Mono<Long> nextIndex = array.next();
Mono<Boolean> moved = array.seek(0);
Mono<Long> ringIndex = array.ring(1000, 10, 20, 30);
Mono<List<Integer>> recent = array.lastItems(5);
Mono<List<Integer>> recentReversed = array.lastItems(5, true);
RedissonRxClient redisson = redissonClient.rxJava();
RArrayRx<Integer> array = redisson.getArray("myArray");
Single<Long> lastIndex = array.insert(1, 2, 3);
Maybe<Long> nextIndex = array.next();
Single<Boolean> moved = array.seek(0);
Single<Long> ringIndex = array.ring(1000, 10, 20, 30);
Single<List<Integer>> recent = array.lastItems(5);
Single<List<Integer>> recentReversed = array.lastItems(5, true);
Iterating¶
The synchronous API exposes the entries as an Iterator (optionally with a page-size hint that maps to the ARSCAN COUNT option) and as a Stream. The other APIs provide the same traversal as an AsyncIterator, a Reactor Flux, and an RxJava3 Flowable. Entries are always returned in ascending index order.
Searching with grep¶
grep returns the indexes of values matching an ArrayGrepArgs predicate, and grepEntries returns the matching entries; both can be restricted to an index range. Predicates are built with the static factories exact, match (substring), glob, and regex, and can be combined and tuned with and, or, limit, and noCase.
RArrayAsync<Integer> array = redisson.getArray("myArray");
RFuture<List<Long>> indexes = array.grepAsync(ArrayGrepArgs.exact(200));
RFuture<List<Long>> inRange = array.grepAsync(0, 100, ArrayGrepArgs.glob("2*").limit(10));
RFuture<List<ArrayEntry<Integer>>> entries = array.grepEntriesAsync(ArrayGrepArgs.match(20));
RedissonReactiveClient redisson = redissonClient.reactive();
RArrayReactive<Integer> array = redisson.getArray("myArray");
Mono<List<Long>> indexes = array.grep(ArrayGrepArgs.exact(200));
Mono<List<Long>> inRange = array.grep(0, 100, ArrayGrepArgs.glob("2*").limit(10));
Mono<List<ArrayEntry<Integer>>> entries = array.grepEntries(ArrayGrepArgs.match(20));
RedissonRxClient redisson = redissonClient.rxJava();
RArrayRx<Integer> array = redisson.getArray("myArray");
Single<List<Long>> indexes = array.grep(ArrayGrepArgs.exact(200));
Single<List<Long>> inRange = array.grep(0, 100, ArrayGrepArgs.glob("2*").limit(10));
Single<List<ArrayEntry<Integer>>> entries = array.grepEntries(ArrayGrepArgs.match(20));
Aggregations¶
For numeric values, sum, min, and max compute the corresponding aggregate over an index range, and bitAnd, bitOr, and bitXor compute bitwise reductions over the same range. In the RxJava3 API these return Maybe, since a range may contain no values.
RArrayAsync<Integer> array = redisson.getArray("myArray");
RFuture<Double> sum = array.sumAsync(0, 100);
RFuture<Double> min = array.minAsync(0, 100);
RFuture<Double> max = array.maxAsync(0, 100);
RFuture<Long> and = array.bitAndAsync(0, 100);
RFuture<Long> or = array.bitOrAsync(0, 100);
RFuture<Long> xor = array.bitXorAsync(0, 100);
RedissonReactiveClient redisson = redissonClient.reactive();
RArrayReactive<Integer> array = redisson.getArray("myArray");
Mono<Double> sum = array.sum(0, 100);
Mono<Double> min = array.min(0, 100);
Mono<Double> max = array.max(0, 100);
Mono<Long> and = array.bitAnd(0, 100);
Mono<Long> or = array.bitOr(0, 100);
Mono<Long> xor = array.bitXor(0, 100);
RedissonRxClient redisson = redissonClient.rxJava();
RArrayRx<Integer> array = redisson.getArray("myArray");
Maybe<Double> sum = array.sum(0, 100);
Maybe<Double> min = array.min(0, 100);
Maybe<Double> max = array.max(0, 100);
Maybe<Long> and = array.bitAnd(0, 100);
Maybe<Long> or = array.bitOr(0, 100);
Maybe<Long> xor = array.bitXor(0, 100);
Array information¶
getInfo returns an ArrayInfo snapshot describing the array, including the number of stored values, the array length, and the next insert index. Calling it with true includes fuller internal statistics about the array's storage layout.
Use Cases¶
RArray fits data that is naturally addressed by a numeric index and is often sparse — only some positions are populated — or that is appended and read back as a recent window. Values can be read or aggregated over index ranges in a single call, and numeric ranges support sum, min, max, and bitwise reductions.
Sparse Vectors and Feature Stores
Machine-learning feature vectors and embeddings are frequently sparse: only a few of many possible dimensions carry a value. Storing them by dimension index keeps the gaps free of cost, while individual dimensions or whole ranges can be read back directly.
RArray<Double> features = redisson.getArray("features:user:1001");
// only populated dimensions occupy space
features.set(7, 0.91);
features.set(2048, 0.34);
features.set(50000, 0.12);
Double weight = features.get(7);
List<Double> window = features.range(0, 1024);
Ring Buffers for Recent Items
A fixed-size ring buffer keeps the latest values and overwrites the oldest as new ones arrive — a natural fit for recent readings, rolling logs, or the last events seen. ring writes into the buffer and lastItems reads the most recent values, newest first.
RArray<Integer> recent = redisson.getArray("readings:sensor:42");
// keep the last 1000 readings
recent.ring(1000, 21, 22, 23);
// the five most recent readings, newest first
List<Integer> latest = recent.lastItems(5, true);
Range Analytics over Numeric Data
When values are numeric, an index range can be reduced without fetching the data: sum, min, and max compute aggregates, while bitAnd, bitOr, and bitXor combine values bitwise. grep locates the indexes whose values match a predicate.
RArray<Integer> metrics = redisson.getArray("metrics:day");
Double total = metrics.sum(0, 1439); // e.g. one value per minute of the day
Double peak = metrics.max(0, 1439);
List<Long> spikes = metrics.grep(0, 1439, ArrayGrepArgs.glob("9*").limit(10));
Time Series¶
Redisson's RTimeSeries object is a distributed structure for storing and querying time-stamped data on Valkey or Redis. Each entry pairs a unique long timestamp with a value and, optionally, a label - a secondary value stored and returned alongside the entry. Entries are kept ordered by timestamp, which makes RTimeSeries a natural fit for metrics, sensor readings, financial ticks, and event logs.
The object is typed as RTimeSeries<V, L>, where V is the value type and L the label type (use any type, such as Object, when labels aren't needed). It is thread-safe and implements Iterable<V>.
Storing entries¶
Each value is stored against a unique timestamp; adding a value at an existing timestamp overwrites the previous one. A label can be attached per entry, and whole batches can be added in a single call.
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
// Add a value at a timestamp
ts.add(201908110501L, "10%");
ts.add(201908110502L, "30%");
// Add a value together with a label
ts.add(201908110504L, "10%", "host-1");
// Add several values at once
ts.addAll(Map.of(201908110601L, "15%",
201908110602L, "25%",
201908110603L, "35%"));
// Add several labelled entries at once
ts.addAll(List.of(new TimeSeriesEntry<>(201908110701L, "40%", "host-1"),
new TimeSeriesEntry<>(201908110702L, "45%", "host-2")));
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
RFuture<Void> f1 = ts.addAsync(201908110501L, "10%");
RFuture<Void> f2 = ts.addAsync(201908110504L, "10%", "host-1");
RFuture<Void> f3 = ts.addAllAsync(Map.of(201908110601L, "15%",
201908110602L, "25%"));
RFuture<Void> f4 = ts.addAllAsync(List.of(
new TimeSeriesEntry<>(201908110701L, "40%", "host-1"),
new TimeSeriesEntry<>(201908110702L, "45%", "host-2")));
RedissonReactiveClient redisson = redissonClient.reactive();
RTimeSeriesReactive<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Mono<Void> m1 = ts.add(201908110501L, "10%");
Mono<Void> m2 = ts.add(201908110504L, "10%", "host-1");
Mono<Void> m3 = ts.addAll(Map.of(201908110601L, "15%",
201908110602L, "25%"));
Mono<Void> m4 = ts.addAll(List.of(
new TimeSeriesEntry<>(201908110701L, "40%", "host-1"),
new TimeSeriesEntry<>(201908110702L, "45%", "host-2")));
RedissonRxClient redisson = redissonClient.rxJava();
RTimeSeriesRx<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Completable c1 = ts.add(201908110501L, "10%");
Completable c2 = ts.add(201908110504L, "10%", "host-1");
Completable c3 = ts.addAll(Map.of(201908110601L, "15%",
201908110602L, "25%"));
Completable c4 = ts.addAll(List.of(
new TimeSeriesEntry<>(201908110701L, "40%", "host-1"),
new TimeSeriesEntry<>(201908110702L, "45%", "host-2")));
Per-entry TTL¶
Each entry can be given its own time-to-live, after which Valkey or Redis removes it automatically - useful for retention policies. A TTL can be expressed as an amount plus TimeUnit or as a Duration, and applies to single entries or whole batches. A label and a TTL are combined through the Duration overload.
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
// TTL as amount + TimeUnit
ts.add(201908110510L, "85%", 10, TimeUnit.HOURS);
// TTL as Duration
ts.add(201908110530L, "95%", Duration.ofDays(1));
// Label together with a TTL
ts.add(201908110540L, "99%", "host-1", Duration.ofHours(6));
// Same TTL for a whole batch
ts.addAll(Map.of(201908110601L, "15%",
201908110602L, "25%"), 1, TimeUnit.HOURS);
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
RFuture<Void> f1 = ts.addAsync(201908110510L, "85%", 10, TimeUnit.HOURS);
RFuture<Void> f2 = ts.addAsync(201908110530L, "95%", Duration.ofDays(1));
RFuture<Void> f3 = ts.addAsync(201908110540L, "99%", "host-1", Duration.ofHours(6));
RFuture<Void> f4 = ts.addAllAsync(Map.of(201908110601L, "15%",
201908110602L, "25%"), 1, TimeUnit.HOURS);
RedissonReactiveClient redisson = redissonClient.reactive();
RTimeSeriesReactive<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Mono<Void> m1 = ts.add(201908110510L, "85%", 10, TimeUnit.HOURS);
Mono<Void> m2 = ts.add(201908110530L, "95%", Duration.ofDays(1));
Mono<Void> m3 = ts.add(201908110540L, "99%", "host-1", Duration.ofHours(6));
Mono<Void> m4 = ts.addAll(Map.of(201908110601L, "15%",
201908110602L, "25%"), 1, TimeUnit.HOURS);
RedissonRxClient redisson = redissonClient.rxJava();
RTimeSeriesRx<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Completable c1 = ts.add(201908110510L, "85%", 10, TimeUnit.HOURS);
Completable c2 = ts.add(201908110530L, "95%", Duration.ofDays(1));
Completable c3 = ts.add(201908110540L, "99%", "host-1", Duration.ofHours(6));
Completable c4 = ts.addAll(Map.of(201908110601L, "15%",
201908110602L, "25%"), 1, TimeUnit.HOURS);
Reading and removing entries¶
get returns a value by timestamp, while getEntry returns the full TimeSeriesEntry (its timestamp, value, and label). getAndRemove reads and deletes in one step, and size reports the entry count.
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
// Value at a timestamp
String value = ts.get(201908110504L);
// Full entry: timestamp, value, label
TimeSeriesEntry<String, String> entry = ts.getEntry(201908110504L);
String label = entry.getLabel();
// Number of entries
int size = ts.size();
// Remove by timestamp
boolean removed = ts.remove(201908110504L);
// Read and remove in one step
String taken = ts.getAndRemove(201908110501L);
TimeSeriesEntry<String, String> takenEntry = ts.getAndRemoveEntry(201908110502L);
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
RFuture<String> value = ts.getAsync(201908110504L);
RFuture<TimeSeriesEntry<String, String>> entry = ts.getEntryAsync(201908110504L);
RFuture<Integer> size = ts.sizeAsync();
RFuture<Boolean> removed = ts.removeAsync(201908110504L);
RFuture<String> taken = ts.getAndRemoveAsync(201908110501L);
RFuture<TimeSeriesEntry<String, String>> takenEntry = ts.getAndRemoveEntryAsync(201908110502L);
RedissonReactiveClient redisson = redissonClient.reactive();
RTimeSeriesReactive<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Mono<String> value = ts.get(201908110504L);
Mono<TimeSeriesEntry<String, String>> entry = ts.getEntry(201908110504L);
Mono<Integer> size = ts.size();
Mono<Boolean> removed = ts.remove(201908110504L);
Mono<String> taken = ts.getAndRemove(201908110501L);
Mono<TimeSeriesEntry<String, String>> takenEntry = ts.getAndRemoveEntry(201908110502L);
RedissonRxClient redisson = redissonClient.rxJava();
RTimeSeriesRx<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Maybe<String> value = ts.get(201908110504L);
Maybe<TimeSeriesEntry<String, String>> entry = ts.getEntry(201908110504L);
Single<Integer> size = ts.size();
Single<Boolean> removed = ts.remove(201908110504L);
Maybe<String> taken = ts.getAndRemove(201908110501L);
Maybe<TimeSeriesEntry<String, String>> takenEntry = ts.getAndRemoveEntry(201908110502L);
Range queries¶
Entries can be read over an inclusive timestamp range, in forward or reverse order, as values (range/rangeReversed) or as full entries (entryRange/entryRangeReversed). Each of these methods also has an overload that caps the number of results with a trailing limit argument.
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
// Values within an inclusive timestamp range
Collection<String> values = ts.range(201908110501L, 201908110508L);
// Values in reverse order
Collection<String> reversed = ts.rangeReversed(201908110501L, 201908110508L);
// Cap the number of results
Collection<String> firstThree = ts.range(201908110501L, 201908110508L, 3);
// Full entries within a range (each carries timestamp, value, label)
Collection<TimeSeriesEntry<String, String>> entries =
ts.entryRange(201908110501L, 201908110508L);
Collection<TimeSeriesEntry<String, String>> entriesReversed =
ts.entryRangeReversed(201908110501L, 201908110508L);
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
RFuture<Collection<String>> values = ts.rangeAsync(201908110501L, 201908110508L);
RFuture<Collection<String>> reversed = ts.rangeReversedAsync(201908110501L, 201908110508L);
RFuture<Collection<String>> firstThree = ts.rangeAsync(201908110501L, 201908110508L, 3);
RFuture<Collection<TimeSeriesEntry<String, String>>> entries =
ts.entryRangeAsync(201908110501L, 201908110508L);
RFuture<Collection<TimeSeriesEntry<String, String>>> entriesReversed =
ts.entryRangeReversedAsync(201908110501L, 201908110508L);
RedissonReactiveClient redisson = redissonClient.reactive();
RTimeSeriesReactive<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Mono<Collection<String>> values = ts.range(201908110501L, 201908110508L);
Mono<Collection<String>> reversed = ts.rangeReversed(201908110501L, 201908110508L);
Mono<Collection<String>> firstThree = ts.range(201908110501L, 201908110508L, 3);
Mono<Collection<TimeSeriesEntry<String, String>>> entries =
ts.entryRange(201908110501L, 201908110508L);
Mono<Collection<TimeSeriesEntry<String, String>>> entriesReversed =
ts.entryRangeReversed(201908110501L, 201908110508L);
RedissonRxClient redisson = redissonClient.rxJava();
RTimeSeriesRx<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Single<Collection<String>> values = ts.range(201908110501L, 201908110508L);
Single<Collection<String>> reversed = ts.rangeReversed(201908110501L, 201908110508L);
Single<Collection<String>> firstThree = ts.range(201908110501L, 201908110508L, 3);
Single<Collection<TimeSeriesEntry<String, String>>> entries =
ts.entryRange(201908110501L, 201908110508L);
Single<Collection<TimeSeriesEntry<String, String>>> entriesReversed =
ts.entryRangeReversed(201908110501L, 201908110508L);
Head, tail, and polling¶
first/last return the value at the earliest/latest timestamp; firstEntry/lastEntry return the full entries, and firstTimestamp/lastTimestamp return just the timestamps. first(count)/last(count) read several head or tail values without removing them. The poll* methods, by contrast, remove what they return, and removeRange deletes every entry within a timestamp range.
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
// Earliest / latest value
String firstValue = ts.first();
String lastValue = ts.last();
// Earliest / latest full entry
TimeSeriesEntry<String, String> firstEntry = ts.firstEntry();
TimeSeriesEntry<String, String> lastEntry = ts.lastEntry();
// Earliest / latest timestamp
Long firstTs = ts.firstTimestamp();
Long lastTs = ts.lastTimestamp();
// Peek at several head / tail values (no removal)
Collection<String> head = ts.first(3);
Collection<String> tail = ts.last(3);
// Poll = read AND remove, from the head or tail
Collection<String> polledHead = ts.pollFirst(2);
Collection<TimeSeriesEntry<String, String>> polledTail = ts.pollLastEntries(2);
String oldest = ts.pollFirst();
// Delete every entry within a timestamp range
int deleted = ts.removeRange(201908110501L, 201908110508L);
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
RFuture<String> firstValue = ts.firstAsync();
RFuture<String> lastValue = ts.lastAsync();
RFuture<TimeSeriesEntry<String, String>> firstEntry = ts.firstEntryAsync();
RFuture<TimeSeriesEntry<String, String>> lastEntry = ts.lastEntryAsync();
RFuture<Long> firstTs = ts.firstTimestampAsync();
RFuture<Long> lastTs = ts.lastTimestampAsync();
RFuture<Collection<String>> head = ts.firstAsync(3);
RFuture<Collection<String>> tail = ts.lastAsync(3);
RFuture<Collection<String>> polledHead = ts.pollFirstAsync(2);
RFuture<Collection<TimeSeriesEntry<String, String>>> polledTail = ts.pollLastEntriesAsync(2);
RFuture<String> oldest = ts.pollFirstAsync();
RFuture<Integer> deleted = ts.removeRangeAsync(201908110501L, 201908110508L);
RedissonReactiveClient redisson = redissonClient.reactive();
RTimeSeriesReactive<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Mono<String> firstValue = ts.first();
Mono<String> lastValue = ts.last();
Mono<TimeSeriesEntry<String, String>> firstEntry = ts.firstEntry();
Mono<TimeSeriesEntry<String, String>> lastEntry = ts.lastEntry();
Mono<Long> firstTs = ts.firstTimestamp();
Mono<Long> lastTs = ts.lastTimestamp();
Mono<Collection<String>> head = ts.first(3);
Mono<Collection<String>> tail = ts.last(3);
Mono<Collection<String>> polledHead = ts.pollFirst(2);
Mono<Collection<TimeSeriesEntry<String, String>>> polledTail = ts.pollLastEntries(2);
Mono<String> oldest = ts.pollFirst();
Mono<Integer> deleted = ts.removeRange(201908110501L, 201908110508L);
RedissonRxClient redisson = redissonClient.rxJava();
RTimeSeriesRx<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Maybe<String> firstValue = ts.first();
Maybe<String> lastValue = ts.last();
Maybe<TimeSeriesEntry<String, String>> firstEntry = ts.firstEntry();
Maybe<TimeSeriesEntry<String, String>> lastEntry = ts.lastEntry();
Single<Long> firstTs = ts.firstTimestamp();
Single<Long> lastTs = ts.lastTimestamp();
Single<Collection<String>> head = ts.first(3);
Single<Collection<String>> tail = ts.last(3);
Single<Collection<String>> polledHead = ts.pollFirst(2);
Single<Collection<TimeSeriesEntry<String, String>>> polledTail = ts.pollLastEntries(2);
Maybe<String> oldest = ts.pollFirst();
Single<Integer> deleted = ts.removeRange(201908110501L, 201908110508L);
Iteration¶
RTimeSeries implements Iterable<V>, so the synchronous interface can be looped over directly or streamed with stream(); iterator(int count) controls the batch size used to fetch elements. On the Reactive and RxJava3 interfaces, iterator() returns a Flux<V> and Flowable<V> respectively. (There is no streaming iterator on the asynchronous interface.)
Listeners¶
Redisson allows binding listeners per RTimeSeries object. This requires the notify-keyspace-events setting to be enabled on Valkey or Redis side.
| Listener class name | Event description | Valkey or Redis notify-keyspace-events value |
|---|---|---|
| org.redisson.api.listener.TrackingListener | Element created/removed/updated after read operation | - |
| org.redisson.api.listener.ScoredSortedSetAddListener | Entry created/updated | Ez |
| org.redisson.api.listener.ScoredSortedSetRemoveListener | Entry removed | Ez |
| org.redisson.api.ExpiredObjectListener | RTimeSeries object expired |
Ex |
| org.redisson.api.DeletedObjectListener | RTimeSeries object deleted |
Eg |
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
int listenerId = ts.addListener(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
// entry added or updated
}
});
int listenerId = ts.addListener(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
// entry removed
}
});
int listenerId = ts.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
// time series expired
}
});
int listenerId = ts.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
// time series deleted
}
});
// remove listener
ts.removeListener(listenerId);
RTimeSeries<String, String> ts = redisson.getTimeSeries("myTimeSeries");
RFuture<Integer> listenerFuture = ts.addListenerAsync(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
// entry added or updated
}
});
RFuture<Integer> listenerFuture = ts.addListenerAsync(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
// entry removed
}
});
RFuture<Integer> listenerFuture = ts.addListenerAsync(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
// time series expired
}
});
RFuture<Integer> listenerFuture = ts.addListenerAsync(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
// time series deleted
}
});
// remove listener
RFuture<Void> removeFuture = ts.removeListenerAsync(listenerId);
RedissonReactiveClient redisson = redissonClient.reactive();
RTimeSeriesReactive<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Mono<Integer> listenerMono = ts.addListener(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
// entry added or updated
}
});
Mono<Integer> listenerMono = ts.addListener(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
// entry removed
}
});
Mono<Integer> listenerMono = ts.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
// time series expired
}
});
Mono<Integer> listenerMono = ts.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
// time series deleted
}
});
// remove listener
Mono<Void> removeMono = ts.removeListener(listenerId);
RedissonRxClient redisson = redissonClient.rxJava();
RTimeSeriesRx<String, String> ts = redisson.getTimeSeries("myTimeSeries");
Single<Integer> listenerSingle = ts.addListener(new ScoredSortedSetAddListener() {
@Override
public void onAdd(String name) {
// entry added or updated
}
});
Single<Integer> listenerSingle = ts.addListener(new ScoredSortedSetRemoveListener() {
@Override
public void onRemove(String name) {
// entry removed
}
});
Single<Integer> listenerSingle = ts.addListener(new ExpiredObjectListener() {
@Override
public void onExpired(String name) {
// time series expired
}
});
Single<Integer> listenerSingle = ts.addListener(new DeletedObjectListener() {
@Override
public void onDeleted(String name) {
// time series deleted
}
});
// remove listener
Completable removeCompletable = ts.removeListener(listenerId);
Vector Set¶
Java implementation of Redis based Vector Set object is a specialized data type designed for managing high-dimensional vector data and enabling fast vector similarity search. Vector sets are similar to sorted sets but instead of a score, each element has a string representation of a vector, making them ideal for AI applications, machine learning models, and semantic search use cases. This object is thread-safe.
Requires Redis 8.0+.
High-Dimensional Vector Data¶
Vector sets are optimized for storing and querying high-dimensional vectors commonly used in modern applications. Each element in a vector set consists of an element name (string identifier), a vector (list of floating-point values representing the element in vector space), and optional JSON attributes (metadata associated with the element).
Vector sets support configurable dimensionality to match your embedding model output, efficient storage of dense vector representations, and automatic vector normalization for cosine similarity calculations.
Usage examples:
RVectorSet<String> vectorSet = redisson.getVectorSet("embeddings");
// Add text embeddings (e.g., from OpenAI, Sentence Transformers, etc.)
vectorSet.add(VectorAddArgs.element("doc1")
.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)); // 1536-dim for OpenAI
vectorSet.add(VectorAddArgs.element("doc2")
.vector(-0.23, 0.45, -0.67, 0.89, 0.12, ...));
// Retrieve stored vector
List<Double> vector = vectorSet.getVector("doc1");
// Get vector dimensions
long dimensions = vectorSet.dim();
// Get total number of elements
long count = vectorSet.size();
RVectorSet<String> vectorSet = redisson.getVectorSet("embeddings");
// Add text embeddings
RFuture<Boolean> f1 = vectorSet.addAsync(VectorAddArgs.element("doc1")
.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...));
RFuture<Boolean> f2 = vectorSet.addAsync(VectorAddArgs.element("doc2")
.vector(-0.23, 0.45, -0.67, 0.89, 0.12, ...));
// Retrieve stored vector
RFuture<List<Double>> vectorFuture = vectorSet.getVectorAsync("doc1");
// Get vector dimensions
RFuture<Long> dimFuture = vectorSet.dimAsync();
// Get total number of elements
RFuture<Integer> sizeFuture = vectorSet.sizeAsync();
vectorFuture.whenComplete((vector, exception) -> {
// ...
});
RedissonReactiveClient redisson = redissonClient.reactive();
RVectorSetReactive<String> vectorSet = redisson.getVectorSet("embeddings");
// Add text embeddings
Mono<Boolean> m1 = vectorSet.add(VectorAddArgs.element("doc1")
.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...));
Mono<Boolean> m2 = vectorSet.add(VectorAddArgs.element("doc2")
.vector(-0.23, 0.45, -0.67, 0.89, 0.12, ...));
// Retrieve stored vector
Mono<List<Double>> vectorMono = vectorSet.getVector("doc1");
// Get vector dimensions
Mono<Long> dimMono = vectorSet.dim();
// Get total number of elements
Mono<Integer> sizeMono = vectorSet.size();
vectorMono.doOnNext(vector -> {
// ...
}).subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RVectorSetRx<String> vectorSet = redisson.getVectorSet("embeddings");
// Add text embeddings
Single<Boolean> s1 = vectorSet.add(VectorAddArgs.element("doc1")
.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...));
Single<Boolean> s2 = vectorSet.add(VectorAddArgs.element("doc2")
.vector(-0.23, 0.45, -0.67, 0.89, 0.12, ...));
// Retrieve stored vector
Maybe<List<Double>> vectorMaybe = vectorSet.getVector("doc1");
// Get vector dimensions
Single<Long> dimSingle = vectorSet.dim();
// Get total number of elements
Single<Integer> sizeSingle = vectorSet.size();
vectorMaybe.doOnSuccess(vector -> {
// ...
}).subscribe();
Similarity Search¶
Vector sets use the Hierarchical Navigable Small World (HNSW) algorithm for approximate nearest neighbor search. HNSW provides sub-linear query time complexity and excellent recall rates, making it suitable for large-scale similarity search applications.
The similarity search returns elements ordered by their proximity to the query vector using cosine similarity metrics.
Usage examples:
RVectorSet<String> vectorSet = redisson.getVectorSet("embeddings");
// Find similar elements by vector
List<String> similar = vectorSet.getSimilar(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10)); // Return top 10 most similar
// Find similar elements by existing element name
List<String> similarToDoc = vectorSet.getSimilar(
VectorSimilarArgs.element("doc1")
.count(5));
// Get similarity scores along with elements
List<ScoredEntry<String>> similarWithScores = vectorSet.getSimilarWithScores(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10));
for (ScoredEntry<String> entry : similarWithScores) {
String element = entry.getValue();
Double score = entry.getScore(); // Similarity score
}
RVectorSet<String> vectorSet = redisson.getVectorSet("embeddings");
// Find similar elements by vector
RFuture<List<String>> similarFuture = vectorSet.getSimilarAsync(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10));
// Find similar elements by existing element name
RFuture<List<String>> similarToDocFuture = vectorSet.getSimilarAsync(
VectorSimilarArgs.element("doc1")
.count(5));
// Get similarity scores along with elements
RFuture<List<ScoredEntry<String>>> scoresFeature = vectorSet.getSimilarWithScoresAsync(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10));
scoresFeature.whenComplete((entries, exception) -> {
for (ScoredEntry<String> entry : entries) {
String element = entry.getValue();
Double score = entry.getScore();
}
});
RedissonReactiveClient redisson = redissonClient.reactive();
RVectorSetReactive<String> vectorSet = redisson.getVectorSet("embeddings");
// Find similar elements by vector
Mono<List<String>> similarMono = vectorSet.getSimilar(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10));
// Find similar elements by existing element name
Mono<List<String>> similarToDocMono = vectorSet.getSimilar(
VectorSimilarArgs.element("doc1")
.count(5));
// Get similarity scores along with elements
Mono<List<ScoredEntry<String>>> scoresMono = vectorSet.getSimilarWithScores(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10));
scoresMono.doOnNext(entries -> {
for (ScoredEntry<String> entry : entries) {
String element = entry.getValue();
Double score = entry.getScore();
}
}).subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RVectorSetRx<String> vectorSet = redisson.getVectorSet("embeddings");
// Find similar elements by vector
Single<List<String>> similarSingle = vectorSet.getSimilar(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10));
// Find similar elements by existing element name
Single<List<String>> similarToDocSingle = vectorSet.getSimilar(
VectorSimilarArgs.element("doc1")
.count(5));
// Get similarity scores along with elements
Single<List<ScoredEntry<String>>> scoresSingle = vectorSet.getSimilarWithScores(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, 0.78, -0.91, ...)
.count(10));
scoresSingle.doOnSuccess(entries -> {
for (ScoredEntry<String> entry : entries) {
String element = entry.getValue();
Double score = entry.getScore();
}
}).subscribe();
Attribute Management¶
Vector sets support attaching JSON attributes to each element, enabling rich metadata storage and filtered similarity searches. Attributes can be used to store additional information about vectors and to filter search results based on specific criteria.
Usage examples:
RVectorSet<String> vectorSet = redisson.getVectorSet("products");
// Add element with attributes
vectorSet.add(VectorAddArgs.element("product-123")
.vector(0.12, -0.34, 0.56, ...)
.attributes("{\"category\": \"electronics\", \"price\": 299.99, \"inStock\": true}"));
// Update attributes for existing element
vectorSet.setAttributes("product-123",
"{\"category\": \"electronics\", \"price\": 249.99, \"inStock\": true, \"onSale\": true}");
// Retrieve attributes
String attrs = vectorSet.getAttributes("product-123");
// Filtered similarity search using attribute expressions
List<String> filtered = vectorSet.getSimilar(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, ...)
.count(10)
.filter(".price < 300 and .inStock == true"));
// Complex filter expressions
List<String> results = vectorSet.getSimilar(
VectorSimilarArgs.element("query-product")
.count(20)
.filter(".category == \"electronics\" and .price >= 100 and .price <= 500"));
RVectorSet<String> vectorSet = redisson.getVectorSet("products");
// Add element with attributes
RFuture<Boolean> addFuture = vectorSet.addAsync(VectorAddArgs.element("product-123")
.vector(0.12, -0.34, 0.56, ...)
.attributes("{\"category\": \"electronics\", \"price\": 299.99, \"inStock\": true}"));
// Update attributes for existing element
RFuture<Boolean> setAttrFuture = vectorSet.setAttributesAsync("product-123",
"{\"category\": \"electronics\", \"price\": 249.99, \"inStock\": true, \"onSale\": true}");
// Retrieve attributes
RFuture<String> attrsFuture = vectorSet.getAttributesAsync("product-123");
// Filtered similarity search using attribute expressions
RFuture<List<String>> filteredFuture = vectorSet.getSimilarAsync(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, ...)
.count(10)
.filter(".price < 300 and .inStock == true"));
// Complex filter expressions
RFuture<List<String>> resultsFuture = vectorSet.getSimilarAsync(
VectorSimilarArgs.element("query-product")
.count(20)
.filter(".category == \"electronics\" and .price >= 100 and .price <= 500"));
filteredFuture.whenComplete((results, exception) -> {
// ...
});
RedissonReactiveClient redisson = redissonClient.reactive();
RVectorSetReactive<String> vectorSet = redisson.getVectorSet("products");
// Add element with attributes
Mono<Boolean> addMono = vectorSet.add(VectorAddArgs.element("product-123")
.vector(0.12, -0.34, 0.56, ...)
.attributes("{\"category\": \"electronics\", \"price\": 299.99, \"inStock\": true}"));
// Update attributes for existing element
Mono<Boolean> setAttrMono = vectorSet.setAttributes("product-123",
"{\"category\": \"electronics\", \"price\": 249.99, \"inStock\": true, \"onSale\": true}");
// Retrieve attributes
Mono<String> attrsMono = vectorSet.getAttributes("product-123");
// Filtered similarity search using attribute expressions
Mono<List<String>> filteredMono = vectorSet.getSimilar(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, ...)
.count(10)
.filter(".price < 300 and .inStock == true"));
// Complex filter expressions
Mono<List<String>> resultsMono = vectorSet.getSimilar(
VectorSimilarArgs.element("query-product")
.count(20)
.filter(".category == \"electronics\" and .price >= 100 and .price <= 500"));
filteredMono.doOnNext(results -> {
// ...
}).subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RVectorSetRx<String> vectorSet = redisson.getVectorSet("products");
// Add element with attributes
Single<Boolean> addSingle = vectorSet.add(VectorAddArgs.element("product-123")
.vector(0.12, -0.34, 0.56, ...)
.attributes("{\"category\": \"electronics\", \"price\": 299.99, \"inStock\": true}"));
// Update attributes for existing element
Single<Boolean> setAttrSingle = vectorSet.setAttributes("product-123",
"{\"category\": \"electronics\", \"price\": 249.99, \"inStock\": true, \"onSale\": true}");
// Retrieve attributes
Maybe<String> attrsMaybe = vectorSet.getAttributes("product-123");
// Filtered similarity search using attribute expressions
Single<List<String>> filteredSingle = vectorSet.getSimilar(
VectorSimilarArgs.vector(0.12, -0.34, 0.56, ...)
.count(10)
.filter(".price < 300 and .inStock == true"));
// Complex filter expressions
Single<List<String>> resultsSingle = vectorSet.getSimilar(
VectorSimilarArgs.element("query-product")
.count(20)
.filter(".category == \"electronics\" and .price >= 100 and .price <= 500"));
filteredSingle.doOnSuccess(results -> {
// ...
}).subscribe();
Use Cases¶
Vector sets are particularly well-suited for AI and machine learning applications that require efficient similarity search over high-dimensional embeddings.
Semantic Search
Store text embeddings from language models and find semantically similar documents, enabling natural language search that understands meaning rather than just keywords.
Usage examples:
RVectorSet<String> vectorSet = redisson.getVectorSet("documents");
// Store document embeddings
vectorSet.add(VectorAddArgs.element("doc-" + docId)
.vector(embeddingModel.encode(documentText))
.attributes("{\"title\": \"" + title + "\", \"date\": \"" + date + "\"}"));
// Search by query embedding
List<String> relevantDocs = vectorSet.getSimilar(
VectorSimilarArgs.vector(embeddingModel.encode(userQuery))
.count(10));
RVectorSet<String> vectorSet = redisson.getVectorSet("documents");
// Store document embeddings
RFuture<Boolean> addFuture = vectorSet.addAsync(VectorAddArgs.element("doc-" + docId)
.vector(embeddingModel.encode(documentText))
.attributes("{\"title\": \"" + title + "\", \"date\": \"" + date + "\"}"));
// Search by query embedding
RFuture<List<String>> searchFuture = vectorSet.getSimilarAsync(
VectorSimilarArgs.vector(embeddingModel.encode(userQuery))
.count(10));
searchFuture.whenComplete((relevantDocs, exception) -> {
// Process relevant documents
});
RedissonReactiveClient redisson = redissonClient.reactive();
RVectorSetReactive<String> vectorSet = redisson.getVectorSet("documents");
// Store document embeddings
Mono<Boolean> addMono = vectorSet.add(VectorAddArgs.element("doc-" + docId)
.vector(embeddingModel.encode(documentText))
.attributes("{\"title\": \"" + title + "\", \"date\": \"" + date + "\"}"));
// Search by query embedding
Mono<List<String>> searchMono = vectorSet.getSimilar(
VectorSimilarArgs.vector(embeddingModel.encode(userQuery))
.count(10));
searchMono.doOnNext(relevantDocs -> {
// Process relevant documents
}).subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RVectorSetRx<String> vectorSet = redisson.getVectorSet("documents");
// Store document embeddings
Single<Boolean> addSingle = vectorSet.add(VectorAddArgs.element("doc-" + docId)
.vector(embeddingModel.encode(documentText))
.attributes("{\"title\": \"" + title + "\", \"date\": \"" + date + "\"}"));
// Search by query embedding
Single<List<String>> searchSingle = vectorSet.getSimilar(
VectorSimilarArgs.vector(embeddingModel.encode(userQuery))
.count(10));
searchSingle.doOnSuccess(relevantDocs -> {
// Process relevant documents
}).subscribe();
Recommendation Systems
Store user and item embeddings to provide personalized recommendations based on vector similarity.
Usage examples:
RVectorSet<String> vectorSet = redisson.getVectorSet("products");
// Find similar products for recommendations
RFuture<List<String>> recFuture = vectorSet.getSimilarAsync(
VectorSimilarArgs.element("user-" + userId + "-preferences")
.count(20)
.filter(".category == \"" + preferredCategory + "\""));
recFuture.whenComplete((recommendations, exception) -> {
// Process recommendations
});
RedissonReactiveClient redisson = redissonClient.reactive();
RVectorSetReactive<String> vectorSet = redisson.getVectorSet("products");
// Find similar products for recommendations
Mono<List<String>> recMono = vectorSet.getSimilar(
VectorSimilarArgs.element("user-" + userId + "-preferences")
.count(20)
.filter(".category == \"" + preferredCategory + "\""));
recMono.doOnNext(recommendations -> {
// Process recommendations
}).subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RVectorSetRx<String> vectorSet = redisson.getVectorSet("products");
// Find similar products for recommendations
Single<List<String>> recSingle = vectorSet.getSimilar(
VectorSimilarArgs.element("user-" + userId + "-preferences")
.count(20)
.filter(".category == \"" + preferredCategory + "\""));
recSingle.doOnSuccess(recommendations -> {
// Process recommendations
}).subscribe();
Image Similarity
Store image feature vectors extracted from computer vision models to find visually similar images.
Usage examples:
RVectorSet<String> vectorSet = redisson.getVectorSet("images");
// Find similar images
List<ScoredEntry<String>> similarImages = vectorSet.getSimilarWithScores(
VectorSimilarArgs.vector(imageEncoder.encode(queryImage))
.count(50));
for (ScoredEntry<String> entry : similarImages) {
String imageId = entry.getValue();
Double similarity = entry.getScore();
}
RVectorSet<String> vectorSet = redisson.getVectorSet("images");
// Find similar images
RFuture<List<ScoredEntry<String>>> imageFuture = vectorSet.getSimilarWithScoresAsync(
VectorSimilarArgs.vector(imageEncoder.encode(queryImage))
.count(50));
imageFuture.whenComplete((similarImages, exception) -> {
for (ScoredEntry<String> entry : similarImages) {
String imageId = entry.getValue();
Double similarity = entry.getScore();
}
});
RedissonReactiveClient redisson = redissonClient.reactive();
RVectorSetReactive<String> vectorSet = redisson.getVectorSet("images");
// Find similar images
Mono<List<ScoredEntry<String>>> imageMono = vectorSet.getSimilarWithScores(
VectorSimilarArgs.vector(imageEncoder.encode(queryImage))
.count(50));
imageMono.doOnNext(similarImages -> {
for (ScoredEntry<String> entry : similarImages) {
String imageId = entry.getValue();
Double similarity = entry.getScore();
}
}).subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RVectorSetRx<String> vectorSet = redisson.getVectorSet("images");
// Find similar images
Single<List<ScoredEntry<String>>> imageSingle = vectorSet.getSimilarWithScores(
VectorSimilarArgs.vector(imageEncoder.encode(queryImage))
.count(50));
imageSingle.doOnSuccess(similarImages -> {
for (ScoredEntry<String> entry : similarImages) {
String imageId = entry.getValue();
Double similarity = entry.getScore();
}
}).subscribe();
RAG (Retrieval-Augmented Generation)
Use vector sets as a knowledge base for LLM applications, retrieving relevant context based on semantic similarity.
Usage examples:
RVectorSet<String> vectorSet = redisson.getVectorSet("knowledge-base");
// Retrieve relevant context for RAG
List<String> contextChunks = vectorSet.getSimilar(
VectorSimilarArgs.vector(embeddingModel.encode(userQuestion))
.count(5));
String context = contextChunks.stream()
.map(chunkId -> getChunkContent(chunkId))
.collect(Collectors.joining("\n"));
// Pass context to LLM for answer generation
String answer = llm.generate(userQuestion, context);
RVectorSet<String> vectorSet = redisson.getVectorSet("knowledge-base");
// Retrieve relevant context for RAG
RFuture<List<String>> contextFuture = vectorSet.getSimilarAsync(
VectorSimilarArgs.vector(embeddingModel.encode(userQuestion))
.count(5));
contextFuture.thenApply(contextChunks -> {
String context = contextChunks.stream()
.map(chunkId -> getChunkContent(chunkId))
.collect(Collectors.joining("\n"));
// Pass context to LLM for answer generation
return llm.generate(userQuestion, context);
});
RedissonReactiveClient redisson = redissonClient.reactive();
RVectorSetReactive<String> vectorSet = redisson.getVectorSet("knowledge-base");
// Retrieve relevant context for RAG
Mono<List<String>> contextMono = vectorSet.getSimilar(
VectorSimilarArgs.vector(embeddingModel.encode(userQuestion))
.count(5));
contextMono.map(contextChunks -> {
String context = contextChunks.stream()
.map(chunkId -> getChunkContent(chunkId))
.collect(Collectors.joining("\n"));
// Pass context to LLM for answer generation
return llm.generate(userQuestion, context);
}).subscribe();
RedissonRxClient redisson = redissonClient.rxJava();
RVectorSetRx<String> vectorSet = redisson.getVectorSet("knowledge-base");
// Retrieve relevant context for RAG
Single<List<String>> contextSingle = vectorSet.getSimilar(
VectorSimilarArgs.vector(embeddingModel.encode(userQuestion))
.count(5));
contextSingle.map(contextChunks -> {
String context = contextChunks.stream()
.map(chunkId -> getChunkContent(chunkId))
.collect(Collectors.joining("\n"));
// Pass context to LLM for answer generation
return llm.generate(userQuestion, context);
}).subscribe();
BitVector Store¶
This feature is available only in Redisson PRO edition.
Java implementation of Valkey or Redis based RBitVectorStore object is a distributed store of 64-bit vectors mapped by keys, with fast server-side filtering by bitmask. Each entry packs up to 64 independent boolean attributes into a single long, and queries select entries by testing those bits - all evaluated on the server, so a query never transfers the whole data set to the client. Two distinct keys may map to identical vector values yet remain separate entries. The number of stored vectors is limited to 4 294 967 295, and the object is thread-safe.
Beyond plain put and get, a vector can be modified atomically a few bits at a time - setting, clearing, flipping, or replacing masked bits, with read-before/after and compare-and-set variants - so concurrent clients can update disjoint attributes of the same entry without overwriting each other. Stored vectors are queried with four bitmask predicates and either counted or iterated.
Requires Redis 7.0.0+ or any Valkey version.
Code examples:
RBitVectorStore<String> store = redisson.getBitVectorStore("flags");
store.put("user-1", 0b011001L);
store.put("user-2", 0b100101L);
store.put("user-3", 0b101000L);
Long vector = store.get("user-1");
// keys whose vectors have every bit of the mask set
Iterable<String> ids = store.matchAll(MatchArgs.mask(0b001000L));
// count vectors with at least one of the mask's bits set
long count = store.countMatchAny(0b011000L);
RBitVectorStoreAsync<String> store = redisson.getBitVectorStore("flags");
RFuture<Long> putFuture = store.putAsync("user-1", 0b011001L);
RFuture<Long> getFuture = store.getAsync("user-1");
AsyncIterator<String> matchIterator = store.matchAllAsync(MatchArgs.mask(0b001000L));
RFuture<Long> countFuture = store.countMatchAnyAsync(0b011000L);
RedissonReactiveClient redisson = redissonClient.reactive();
RBitVectorStoreReactive<String> store = redisson.getBitVectorStore("flags");
Mono<Long> putMono = store.put("user-1", 0b011001L);
Mono<Long> getMono = store.get("user-1");
Flux<String> matchFlux = store.matchAll(MatchArgs.mask(0b001000L));
Mono<Long> countMono = store.countMatchAny(0b011000L);
RedissonRxClient redisson = redissonClient.rxJava();
RBitVectorStoreRx<String> store = redisson.getBitVectorStore("flags");
Maybe<Long> putRx = store.put("user-1", 0b011001L);
Maybe<Long> getRx = store.get("user-1");
Flowable<String> matchRx = store.matchAll(MatchArgs.mask(0b001000L));
Single<Long> countRx = store.countMatchAny(0b011000L);
Storing, reading, and removing vectors¶
put stores a 64-bit vector under a key and returns the previous value, or null if the key was new. get returns the stored vector, or null when the key is absent. contains tests for presence, remove deletes an entry and reports whether one existed, and size returns the number of stored vectors.
RBitVectorStore<String> store = redisson.getBitVectorStore("flags");
Long previous = store.put("user-1", 0b011001L); // null if "user-1" was new
Long vector = store.get("user-1"); // null if absent
boolean present = store.contains("user-1");
boolean removed = store.remove("user-1");
long total = store.size();
Bulk operations¶
Bulk variants apply the same operation to many keys in a single round-trip. put(Map) stores many entries at once, get(Set) returns the vectors of the present keys (absent keys are omitted from the map rather than mapped to null), and remove(Set) deletes many keys and returns how many existed. setBits(Set, mask) ORs a mask into every key that is already present, skipping absent ones, and returns how many were updated.
store.put(Map.of("user-1", 0b011001L, "user-2", 0b100101L));
Map<String, Long> vectors = store.get(Set.of("user-1", "user-2", "user-3"));
long removed = store.remove(Set.of("user-1", "user-2"));
long updated = store.setBits(Set.of("user-3", "user-4"), 0b000010L); // present keys only
Atomic mask updates¶
These operations change selected bits of an entry atomically, leaving the rest untouched, and create the entry (treating the old value as 0) when the key is absent. setBits turns masked bits on, clearBits turns them off, flipBits toggles them, and replaceBits overwrites the masked bits with the corresponding bits of a value. updateAndGet combines a set and a clear in one step - (old & ~clearMask) | setMask, where setMask wins on any overlap - and returns the new value, while getAndUpdate performs the same change but returns the previous value.
RBitVectorStore<String> store = redisson.getBitVectorStore("flags");
long afterSet = store.setBits("user-1", 0b000110L); // turn masked bits on
long afterClear = store.clearBits("user-1", 0b000010L); // turn masked bits off
long afterFlip = store.flipBits("user-1", 0b001000L); // toggle masked bits
// overwrite only the masked bits with those of the value
long afterReplace = store.replaceBits("user-1", 0b001100L, 0b000100L);
// set some bits and clear others in one atomic step (set wins on overlap)
long updated = store.updateAndGet("user-1", 0b000001L, 0b010000L);
Long previous = store.getAndUpdate("user-1", 0b000001L, 0b010000L);
Single-bit operations¶
For one attribute at a time, the single-bit operations address a bit position in [0, 63]; a position outside that range throws IndexOutOfBoundsException. getBit reads a bit (or null if the key is absent); setBit, clearBit, and flipBit change one bit and return the new vector; and getAndSetBit/getAndFlipBit return the full vector as it was before the change. As with the mask operations, an absent key is created on write.
Boolean bit = store.getBit("user-1", 2); // null if "user-1" is absent
long afterSet = store.setBit("user-1", 2); // set bit 2 to 1
long afterValue = store.setBit("user-1", 3, false); // set bit 3 to a given value
long afterClear = store.clearBit("user-1", 2);
long afterFlip = store.flipBit("user-1", 4);
long before = store.getAndSetBit("user-1", 5); // returns the vector before the change
Conditional storage and updates¶
These operations apply only when a precondition holds. putIfAbsent stores a vector only if the key is new, returning the existing value otherwise (the Map.putIfAbsent convention); putIfExists stores only if the key already exists. updateIfExists applies a set/clear mask update like updateAndGet but skips absent keys instead of creating them. compareAndSet swaps the whole vector only if it currently equals an expected value, and compareAndSetBits performs that compare-and-swap restricted to the bits selected by a mask.
Long existing = store.putIfAbsent("user-1", 0b000001L); // null if stored, else current value
Long prevIfExists = store.putIfExists("user-1", 0b000010L); // null if "user-1" was absent
Long updated = store.updateIfExists("user-1", 0b000100L, 0b001000L); // null if absent
boolean swapped = store.compareAndSet("user-1", 0b000001L, 0b000011L);
// compare and swap only within the masked bits
boolean swappedBits = store.compareAndSetBits("user-1", 0b001100L, 0b000100L, 0b001000L);
Counting matches¶
Each predicate has a counting form that returns how many stored vectors satisfy it, evaluated entirely on the server. countMatchAll(mask) counts vectors that have every bit of mask set, countMatchAny(mask) those with at least one, countMatchNone(mask) those with none, and countMatchExact(mask, target) those whose masked bits equal target. A mask of 0 makes the all, none, and exact predicates trivially true (returning size()) and the any predicate trivially false (returning 0).
long all = store.countMatchAll(0b000110L);
long any = store.countMatchAny(0b011000L);
long none = store.countMatchNone(0b000001L);
long exact = store.countMatchExact(0b101001L, 0b100001L);
Iterating matches and keys¶
The same predicates have iterating forms that return the matching keys: matchAll, matchAny, and matchNone take a MatchArgs built from a mask, while matchExact takes a MatchExactArgs built from a mask and a target. ids iterates every stored key. The returned Iterable is single-pass - its iterator may be taken only once - and walks the result server-side in batches, so it is safe for stores of any size and reflects a non-strict snapshot. Two parameters tune the walk: chunkSize controls how many keys are fetched per round-trip, and chunkFetchTTL bounds how long the server-side cursor state is kept, so an iterator abandoned before it is fully consumed is still cleaned up.
// iterate keys whose vectors contain every bit of the mask
for (String id : store.matchAll(MatchArgs.mask(0b000110L))) {
// ...
}
// exact match on the selected bits
Iterable<String> exact = store.matchExact(
MatchExactArgs.mask(0b101001L).target(0b100001L));
// tune batch size and server-side cursor TTL
Iterable<String> tuned = store.matchAny(
MatchArgs.mask(0b101001L)
.chunkSize(2048)
.chunkFetchTTL(Duration.ofMinutes(2)));
// iterate all stored keys
for (String id : store.ids()) {
// ...
}
Use Cases¶
Bit Vector Store is well-suited to applications that filter many records by combinations of discrete boolean attributes — feature flags, permissions, categorical tags, audience segments, and similar low-cardinality dimensions. Up to 64 such attributes can be packed into a single vector per key, and queries combine them with bitmask predicates evaluated server-side.
Feature Flags and Permissions
Each user is associated with a vector whose bits represent capabilities (admin, premium, beta access, export rights, ...). Queries find users matching a required permission set without scanning every user — typically expressed with matchAll, which returns keys whose vectors have every required bit set.
// Bit layout: 0=login, 1=premium, 2=admin, 3=beta, 4=export, 5=billing
long PREMIUM = 1L << 1;
long ADMIN = 1L << 2;
long EXPORT = 1L << 4;
RBitVectorStore<String> permissions = redisson.getBitVectorStore("user-permissions");
permissions.put("alice", PREMIUM | ADMIN | EXPORT);
permissions.put("bob", PREMIUM | EXPORT);
permissions.put("carol", PREMIUM);
// Find all users who can both export AND have admin rights
Iterable<String> exporters = permissions.matchAll(
MatchArgs.mask(ADMIN | EXPORT));
// Count premium users
long premiumCount = permissions.countMatchAll(PREMIUM);
Categorical Tag Filtering
Each item carries a vector of category bits (dietary tags, content categories, product attributes). Catalog and search queries filter on tag combinations: matchAny returns items matching at least one selected tag (broad discovery); matchExact returns items matching a specific combination on the selected bits.
// Bit layout: 0=vegan, 1=gluten-free, 2=organic, 3=kosher, 4=halal, 5=nut-free
long VEGAN = 1L << 0;
long GLUTEN_FREE = 1L << 1;
long NUT_FREE = 1L << 5;
RBitVectorStore<String> products = redisson.getBitVectorStore("product-tags");
products.put("sku-100", VEGAN | GLUTEN_FREE);
products.put("sku-101", GLUTEN_FREE | NUT_FREE);
products.put("sku-102", VEGAN | GLUTEN_FREE | NUT_FREE);
// Items with at least one of the selected dietary tags
Iterable<String> any = products.matchAny(
MatchArgs.mask(VEGAN | NUT_FREE));
// Items that are gluten-free AND nut-free, but NOT vegan
Iterable<String> exact = products.matchExact(
MatchExactArgs.mask(VEGAN | GLUTEN_FREE | NUT_FREE)
.target(GLUTEN_FREE | NUT_FREE));
Audience Segments and Experiment Cohorts
Each user is associated with a vector whose bits represent membership in marketing segments, A/B experiment buckets, or behavioral cohorts. Analytical queries answer "find users in cohort X but not cohort Y" — a question naturally expressed with matchExact by selecting both bits in the mask and pinning their required values in the target.
// Bit layout: 0=newsletter, 1=cart-abandoned, 2=high-value, 3=experiment-A, 4=experiment-B
long CART_ABANDONED = 1L << 1;
long HIGH_VALUE = 1L << 2;
long EXPERIMENT_A = 1L << 3;
long EXPERIMENT_B = 1L << 4;
RBitVectorStore<Long> segments = redisson.getBitVectorStore("user-segments");
segments.put(1001L, HIGH_VALUE | EXPERIMENT_A);
segments.put(1002L, CART_ABANDONED | EXPERIMENT_B);
segments.put(1003L, HIGH_VALUE | CART_ABANDONED | EXPERIMENT_A);
// High-value users in experiment A but NOT experiment B — targeting candidates
Iterable<Long> targets = segments.matchExact(
MatchExactArgs.mask(HIGH_VALUE | EXPERIMENT_A | EXPERIMENT_B)
.target(HIGH_VALUE | EXPERIMENT_A));
// How many users abandoned a cart and are NOT in any experiment
long retargeting = segments.countMatchExact(
CART_ABANDONED | EXPERIMENT_A | EXPERIMENT_B,
CART_ABANDONED);