Skip to content

Integrated Redis Cache and Redis Sessions into Redis OM Spring. #608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ djlStarterVersion = 0.26
djlVersion = 0.30.0
springAiVersion = 1.0.0
azureIdentityVersion = 1.15.4

lettuceCoreVersion = 6.7.1.RELEASE
lettucemodVersion = 4.2.1
globVersion = 0.9.0
11 changes: 11 additions & 0 deletions redis-om-spring/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,15 @@ dependencies {
compileOnly "javax.enterprise:cdi-api:${cdi}"
implementation "com.google.auto.service:auto-service:${autoServiceVersion}"
annotationProcessor "com.google.auto.service:auto-service:${autoServiceVersion}"


compileOnly 'org.projectlombok:lombok:1.18.38'

annotationProcessor 'org.projectlombok:lombok:1.18.38'

implementation 'org.springframework.session:spring-session-core'
implementation "io.lettuce:lettuce-core:$lettuceCoreVersion"
implementation "com.redis:lettucemod:$lettucemodVersion"
implementation "com.hrakaroo:glob:$globVersion"
implementation 'io.micrometer:micrometer-core:1.15.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package com.redis.om.cache;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import com.redis.lettucemod.api.StatefulRedisModulesConnection;

import io.lettuce.core.ScanArgs;
import io.lettuce.core.ScanIterator;

/**
* Abstract base class for Redis cache accessors that provides common functionality
* for interacting with Redis as a cache.
*/
abstract class AbstractRedisCacheAccessor implements CacheAccessor {

/**
* Default number of elements to scan in each iteration when cleaning cache entries.
*/
public static final long DEFAULT_SCAN_COUNT = 100;

/**
* The Redis connection used for cache operations.
*/
protected final StatefulRedisModulesConnection<byte[], byte[]> connection;

private long scanCount = DEFAULT_SCAN_COUNT;

/**
* Creates a new {@link AbstractRedisCacheAccessor} with the given connection.
*
* @param connection the Redis connection, must not be {@literal null}.
*/
AbstractRedisCacheAccessor(StatefulRedisModulesConnection<byte[], byte[]> connection) {
Assert.notNull(connection, "Connection must not be null");
this.connection = connection;

}

/**
* Converts a String key to a byte array using UTF-8 encoding.
*
* @param key the key to convert, never {@literal null}.
* @return the key as a byte array.
*/
private byte[] convertKey(String key) {
return key.getBytes(StandardCharsets.UTF_8);
}

@Override
public Object get(String key, Duration ttl) {
return get(convertKey(key), ttl);
}

/**
* Retrieves a cached object for the given key, optionally extending its TTL.
*
* @param key the cache key as byte array, never {@literal null}.
* @param ttl the time-to-live to set if the key exists, can be {@literal null}.
* @return the cached object or {@literal null} if not found.
*/
protected abstract Object get(byte[] key, Duration ttl);

@Override
public void put(String key, Object value, Duration ttl) {
put(convertKey(key), value, ttl);
}

/**
* Stores an object in the cache with the given key and TTL.
*
* @param key the cache key as byte array, never {@literal null}.
* @param value the object to cache, can be {@literal null}.
* @param ttl the time-to-live for the cached entry, can be {@literal null}.
*/
protected abstract void put(byte[] key, Object value, Duration ttl);

@Override
public Object putIfAbsent(String key, Object value, Duration ttl) {
return putIfAbsent(convertKey(key), value, ttl);
}

/**
* Stores an object in the cache only if the key does not already exist.
*
* @param key the cache key as byte array, never {@literal null}.
* @param value the object to cache, can be {@literal null}.
* @param ttl the time-to-live for the cached entry, can be {@literal null}.
* @return the previous value associated with the key, or {@literal null} if there was no value.
*/
protected abstract Object putIfAbsent(byte[] key, Object value, Duration ttl);

/**
* Sets the number of elements to scan in each iteration when cleaning cache entries.
*
* @param count the number of elements to scan in each iteration
*/
public void setScanCount(long count) {
this.scanCount = count;
}

@Override
public void remove(String key) {
Assert.notNull(key, "Key must not be null");
delete(convertKey(key));
}

@Override
public long clean(String pattern) {
Assert.notNull(pattern, "Pattern must not be null");
ScanArgs args = new ScanArgs();
args.match(pattern);
args.limit(scanCount);
ScanIterator<byte[]> scanIterator = ScanIterator.scan(connection.sync(), args);
List<byte[]> keys = new ArrayList<>();
long count = 0;
while (scanIterator.hasNext()) {
keys.add(scanIterator.next());
if (keys.size() >= scanCount) {
count += delete(keys);
keys.clear();
}
}
count += delete(keys);
return count;
}

/**
* Checks if a key exists in Redis.
*
* @param key the key to check, never {@literal null}.
* @return {@literal true} if the key exists, {@literal false} otherwise.
*/
protected boolean exists(byte[] key) {
return connection.sync().exists(key) > 0;
}

/**
* Deletes multiple keys from Redis.
*
* @param keys the list of keys to delete, can be empty.
* @return the number of keys that were deleted.
*/
private long delete(List<byte[]> keys) {
if (CollectionUtils.isEmpty(keys)) {
return 0;
}
return delete(keys.toArray(new byte[0][]));
}

/**
* Deletes multiple keys from Redis.
*
* @param keys the array of keys to delete.
* @return the number of keys that were deleted.
*/
private long delete(byte[]... keys) {
return connection.sync().del(keys);
}

/**
* Determines if the given TTL duration should be applied as an expiration.
*
* @param ttl the time-to-live duration, can be {@literal null}
* @return {@literal true} if the TTL is not null, not zero, and not negative
*/
protected boolean shouldExpireWithin(@Nullable Duration ttl) {
return ttl != null && !ttl.isZero() && !ttl.isNegative();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.redis.om.cache;

import java.time.Duration;

import org.springframework.lang.Nullable;

/**
* {@link CacheAccessor} provides low-level access to Redis commands
* ({@code HSET, HGETALL, EXPIRE,...}) used for caching.
* <p>
* The {@link CacheAccessor} may be shared by multiple cache implementations and
* is responsible for reading/writing binary data from/to Redis. The
* implementation honors potential cache lock flags that might be set.
*/
public interface CacheAccessor {

/**
* Get the binary value representation from Redis stored for the given key and
* set the given {@link Duration TTL expiration} for the cache entry.
*
* @param key must not be {@literal null}.
* @param ttl {@link Duration} specifying the {@literal expiration timeout} for
* the cache entry.
* @return {@literal null} if key does not exist or has {@literal expired}.
*/
@Nullable
Object get(String key, @Nullable Duration ttl);

/**
* Write the given key/value pair to Redis and set the expiration time if
* defined.
*
* @param key The key for the cache entry. Must not be {@literal null}.
* @param value The value stored for the key. Must not be {@literal null}.
* @param ttl Optional expiration time. Can be {@literal null}.
*/
void put(String key, Object value, @Nullable Duration ttl);

/**
* Write the given value to Redis if the key does not already exist.
*
* @param key The key for the cache entry. Must not be {@literal null}.
* @param value The value stored for the key. Must not be {@literal null}.
* @param ttl Optional expiration time. Can be {@literal null}.
* @return {@literal null} if the value has been written, the value stored for
* the key if it already exists.
*/
@Nullable
Object putIfAbsent(String key, Object value, @Nullable Duration ttl);

/**
* Remove the given key from Redis.
*
* @param key The key for the cache entry. Must not be {@literal null}.
*/
void remove(String key);

/**
* Remove all keys following the given pattern.
*
* @param pattern The pattern for the keys to remove. Must not be
* {@literal null}.
* @return number of keys deleted
*/
long clean(String pattern);

}
66 changes: 66 additions & 0 deletions redis-om-spring/src/main/java/com/redis/om/cache/KeyFunction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.redis.om.cache;

import org.springframework.util.Assert;

/**
* {@link KeyFunction} is a function for creating custom prefixes prepended to
* the actual {@literal key} stored in Redis.
*
*/
@FunctionalInterface
public interface KeyFunction {

/**
* Default separator.
*
*/
String SEPARATOR = ":";

/**
* A pass-through implementation that returns the key unchanged without any prefix.
* This can be used when no key transformation is needed.
*/
KeyFunction PASSTHROUGH = (cache, key) -> key;

/**
* Default {@link KeyFunction} scheme that prefixes cache keys with
* the {@link String name} of the cache followed by double colons.
*
* For example, a cache named {@literal myCache} will prefix all cache keys with
* {@literal myCache::}.
*
*/
KeyFunction SIMPLE = (cache, key) -> cache + SEPARATOR + key;

/**
* Compute the {@link String prefix} for the actual {@literal cache key} stored
* in Redis.
*
* @param cache {@link String name} of the cache in which the key is stored;
* will never be {@literal null}.
* @param key the cache key to be processed; will never be {@literal null}.
* @return the computed {@literal cache key} stored in Redis; never
* {@literal null}.
*/
String compute(String cache, String key);

/**
* Creates a {@link KeyFunction} scheme that prefixes cache keys with the given
* {@link String prefix}.
*
* The {@link String prefix} is prepended to the {@link String cacheName}
* followed by double colons.
*
* For example, a prefix {@literal redis-} with a cache named
* {@literal myCache} results in {@literal redis-myCache::}.
*
* @param prefix must not be {@literal null}.
* @return the default {@link KeyFunction} scheme.
* @since 2.3
*/
static KeyFunction prefixed(String prefix) {
Assert.notNull(prefix, "Prefix must not be null");
return (name, key) -> prefix + name + SEPARATOR + key;
}

}
Loading