Skip to content

Commit

Permalink
feat: redis cache service.
Browse files Browse the repository at this point in the history
  • Loading branch information
QwQ-dev committed Dec 21, 2024
1 parent ca2b402 commit 961ad5e
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 0 deletions.
3 changes: 3 additions & 0 deletions cache/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,7 @@ fairy {
dependencies {
// https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine
api("com.github.ben-manes.caffeine:caffeine:3.1.8")

// https://mvnrepository.com/artifact/org.redisson/redisson
api("org.redisson:redisson:3.40.2")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.Duration;
import java.util.concurrent.TimeUnit;

/**
Expand All @@ -24,8 +25,19 @@ public class ExpirationSettings {
* @param timeToLive the time to live for the cache item
* @param timeUnit the time unit for the expiration
* @return an ExpirationSettings instance
* @see TimeUnit
*/
public static ExpirationSettings of(long timeToLive, TimeUnit timeUnit) {
return new ExpirationSettings(timeToLive, timeUnit);
}

/**
* Converts the ExpirationSettings to a {@link Duration}.
*
* @return a {@link Duration} object representing the expiration time
* @see Duration
*/
public Duration toDuration() {
return Duration.ofMillis(timeUnit.toMillis(timeToLive));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package me.qwqdev.library.cache.service.redis;

import io.fairyproject.log.Log;
import me.qwqdev.library.cache.model.ExpirationSettings;
import org.redisson.Redisson;
import org.redisson.api.RBucket;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;

/**
* Redis Cache Service that supports flexible functional programming to operate the cache.
*
* @param <K> the type of the key
* @param <V> the type of the value
* @author qwq-dev
* @since 2024-12-21 12:00
*/
public class RedisCacheService<K, V> {
private final RedissonClient redissonClient;

/**
* Initializes the Redis Cache Service with the provided Redisson configuration.
*
* @param config the Redisson configuration
*/
public RedisCacheService(Config config) {
this.redissonClient = Redisson.create(config);
}

/**
* {@inheritDoc}
*
* @param key {@inheritDoc}
* @param function {@inheritDoc}
* @param query {@inheritDoc}
* @param cacheAfterQuery {@inheritDoc}
* @param expirationSettings {@inheritDoc}
* @return
*/
public V get(K key, Function<RedissonClient, RBucket<V>> function, Supplier<V> query, boolean cacheAfterQuery, ExpirationSettings expirationSettings) {
return executeCacheOperation(function, key, query, cacheAfterQuery, expirationSettings);
}

/**
* {@inheritDoc}
*
* @param key {@inheritDoc}
* @param function {@inheritDoc}
* @param query {@inheritDoc}
* @param cacheAfterQuery {@inheritDoc}
* @param expirationSettings {@inheritDoc}
* @return {@inheritDoc}
*/
public V getWithLock(K key, Function<RedissonClient, RBucket<V>> function, Supplier<V> query, boolean cacheAfterQuery, ExpirationSettings expirationSettings) {
return executeCacheOperationWithLock(function, key, query, cacheAfterQuery, expirationSettings);
}

private V executeCacheOperation(Function<RedissonClient, RBucket<V>> function, K key, Supplier<V> query, boolean cacheAfterQuery, ExpirationSettings expirationSettings) {
RBucket<V> rBucket = function.apply(redissonClient);
return retrieveOrStoreInCache(rBucket, key, query, cacheAfterQuery, expirationSettings);
}

private V executeCacheOperationWithLock(Function<RedissonClient, RBucket<V>> function, K key, Supplier<V> query, boolean cacheAfterQuery, ExpirationSettings expirationSettings) {
RLock lock = redissonClient.getLock("lock:" + key);

try {
if (lock.tryLock(30, 10, TimeUnit.SECONDS)) {
return executeCacheOperation(function, key, query, cacheAfterQuery, expirationSettings);
}
String msg = "Could not acquire lock for key: " + key;
Log.error(msg);
throw new RuntimeException(msg);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
String msg = "Thread interrupted while trying to acquire lock for key: " + key;
Log.error(msg, exception);
throw new RuntimeException(msg, exception);
} finally {
try {
lock.unlock();
} catch (Exception exception) {
Log.error("Error unlocking lock for key: " + key, exception);
}
}
}

private V retrieveOrStoreInCache(RBucket<V> rBucket, K key, Supplier<V> query, boolean cacheAfterQuery, ExpirationSettings expirationSettings) {
V value = rBucket.get();

if (value != null) {
return value;
}

value = query.get();

if (cacheAfterQuery) {
storeInCache(rBucket, value, expirationSettings);
}

return value;
}

private void storeInCache(RBucket<V> rBucket, V value, ExpirationSettings expirationSettings) {
if (expirationSettings != null) {
rBucket.set(value, expirationSettings.toDuration());
} else {
rBucket.set(value);
}
}

/**
* {@inheritDoc}
*/
public void shutdown() {
if (redissonClient != null) {
try {
redissonClient.shutdown();
} catch (Exception exception) {
Log.error("Error shutting down Redisson client", exception);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package me.qwqdev.library.cache.service.redis;

import me.qwqdev.library.cache.model.ExpirationSettings;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;

import java.util.function.Function;
import java.util.function.Supplier;

/**
* Interface for redis cache service.
*
* @param <K> the type of the key
* @param <V> the type of the value
* @author qwq-dev
* @since 2024-12-21 12:16
*/
public interface RedisCacheServiceInterface<K, V> {
/**
* Get the {@link RedissonClient} that implements the cache
*
* @return the {@link RedissonClient}
*/
RedissonClient getRedissonClient();

/**
* Retrieves a value from the Redis cache. If the value is not found, it fetches the value
* using the provided query function and optionally stores it in the cache.
*
* @param key the cache key
* @param function function to access the Redis cache (e.g., using {@link RedissonClient#getBucket(String)})
* @param query the query function to fetch the value when not present in the cache
* @param cacheAfterQuery whether to cache the value after querying
* @param expirationSettings the expiration settings for the cache item
* @return the value from the cache, or the queried value if not found
*/
V get(K key, Function<RedissonClient, RBucket<V>> function, Supplier<V> query, boolean cacheAfterQuery, ExpirationSettings expirationSettings);

/**
* Retrieves a value from the Redis cache with a distributed lock to ensure exclusive access
* in high-concurrency environments. If the value is not found, it fetches the value using
* the provided query function and optionally stores it in the cache.
*
* @param key the cache key
* @param function function to access the Redis cache (e.g., using {@link RedissonClient#getBucket(String)})
* @param query the query function to fetch the value when not present in the cache
* @param cacheAfterQuery whether to cache the value after querying
* @param expirationSettings the expiration settings for the cache item
* @return the value from the cache, or the queried value if not found
* @throws RuntimeException if the lock cannot be acquired
*/
V getWithLock(K key, Function<RedissonClient, RBucket<V>> function, Supplier<V> query, boolean cacheAfterQuery, ExpirationSettings expirationSettings);

/**
* Shuts down the Redisson client and releases resources.
*/
void shutdown();
}

0 comments on commit 961ad5e

Please sign in to comment.