云快充1.5.0 初始化

This commit is contained in:
3god
2024-10-08 09:38:54 +08:00
parent dea6774942
commit cb19b45919
297 changed files with 18020 additions and 28 deletions

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
抖音关注:程序员三丙
知识星球https://t.zsxq.com/j9b21
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>sanbing</groupId>
<artifactId>jcpp-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>jcpp-infrastructure-cache</artifactId>
<packaging>jar</packaging>
<name>JChargePointProtocol Infrastructure Cache Module</name>
<description>基础缓存管理模块</description>
<properties>
<main.dir>${basedir}/..</main.dir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>sanbing</groupId>
<artifactId>jcpp-infrastructure-util</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
public final class CacheConstants {
public static final String PILE_CACHE = "piles";
public static final String PILE_SESSION_CACHE = "pileSessions";
}

View File

@@ -0,0 +1,13 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.Data;
@Data
public class CacheSpecs {
private Integer timeToLiveInMinutes;
private Integer maxSize;
}

View File

@@ -0,0 +1,22 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.Data;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "cache")
@Data
public class CacheSpecsMap {
@Getter
private Map<String, CacheSpecs> specs;
}

View File

@@ -0,0 +1,15 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
public interface CacheTransaction<K, V> {
void put(K key, V value);
boolean commit();
void rollback();
}

View File

@@ -0,0 +1,11 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
public interface CacheValueWrapper<T> {
T get();
}

View File

@@ -0,0 +1,48 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Slf4j
@RequiredArgsConstructor
public class CaffeineCacheTransaction<K extends Serializable, V extends Serializable> implements CacheTransaction<K, V> {
@Getter
private final UUID id = UUID.randomUUID();
private final CaffeineTransactionalCache<K, V> cache;
@Getter
private final List<K> keys;
@Getter
@Setter
private boolean failed;
private final Map<K, V> pendingPuts = new LinkedHashMap<>();
@Override
public void put(K key, V value) {
pendingPuts.put(key, value);
}
@Override
public boolean commit() {
return cache.commit(id, pendingPuts);
}
@Override
public void rollback() {
cache.rollback(id);
}
}

View File

@@ -0,0 +1,180 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@RequiredArgsConstructor
public abstract class CaffeineTransactionalCache<K extends Serializable, V extends Serializable> implements TransactionalCache<K, V> {
@Getter
protected final String cacheName;
protected final Cache cache;
protected final Lock lock = new ReentrantLock();
private final Map<K, Set<UUID>> objectTransactions = new HashMap<>();
private final Map<UUID, CaffeineCacheTransaction<K, V>> transactions = new HashMap<>();
public CaffeineTransactionalCache(CacheManager cacheManager, String cacheName) {
this.cacheName = cacheName;
this.cache = Optional.ofNullable(cacheManager.getCache(cacheName))
.orElseThrow(() -> new IllegalArgumentException("Cache '" + cacheName + "' is not configured"));
}
@Override
public CacheValueWrapper<V> get(K key) {
return SimpleCacheValueWrapper.wrap(cache.get(key));
}
@Override
public void put(K key, V value) {
lock.lock();
try {
failAllTransactionsByKey(key);
cache.put(key, value);
} finally {
lock.unlock();
}
}
@Override
public void putIfAbsent(K key, V value) {
lock.lock();
try {
failAllTransactionsByKey(key);
doPutIfAbsent(key, value);
} finally {
lock.unlock();
}
}
@Override
public void evict(K key) {
lock.lock();
try {
failAllTransactionsByKey(key);
doEvict(key);
} finally {
lock.unlock();
}
}
@Override
public void evict(Collection<K> keys) {
lock.lock();
try {
keys.forEach(key -> {
failAllTransactionsByKey(key);
doEvict(key);
});
} finally {
lock.unlock();
}
}
@Override
public void evictOrPut(K key, V value) {
evict(key);
}
@Override
public CacheTransaction<K, V> newTransactionForKey(K key) {
return newTransaction(Collections.singletonList(key));
}
@Override
public CacheTransaction<K, V> newTransactionForKeys(List<K> keys) {
return newTransaction(keys);
}
void doPutIfAbsent(K key, V value) {
cache.putIfAbsent(key, value);
}
void doEvict(K key) {
cache.evict(key);
}
CacheTransaction<K, V> newTransaction(List<K> keys) {
lock.lock();
try {
var transaction = new CaffeineCacheTransaction<>(this, keys);
var transactionId = transaction.getId();
for (K key : keys) {
objectTransactions.computeIfAbsent(key, k -> new HashSet<>()).add(transactionId);
}
transactions.put(transactionId, transaction);
return transaction;
} finally {
lock.unlock();
}
}
public boolean commit(UUID trId, Map<K, V> pendingPuts) {
lock.lock();
try {
var tr = transactions.get(trId);
var success = !tr.isFailed();
if (success) {
for (K key : tr.getKeys()) {
Set<UUID> otherTransactions = objectTransactions.get(key);
if (otherTransactions != null) {
for (UUID otherTrId : otherTransactions) {
if (trId == null || !trId.equals(otherTrId)) {
transactions.get(otherTrId).setFailed(true);
}
}
}
}
pendingPuts.forEach(this::doPutIfAbsent);
}
removeTransaction(trId);
return success;
} finally {
lock.unlock();
}
}
void rollback(UUID id) {
lock.lock();
try {
removeTransaction(id);
} finally {
lock.unlock();
}
}
private void removeTransaction(UUID id) {
CaffeineCacheTransaction<K, V> transaction = transactions.remove(id);
if (transaction != null) {
for (var key : transaction.getKeys()) {
Set<UUID> transactions = objectTransactions.get(key);
if (transactions != null) {
transactions.remove(id);
if (transactions.isEmpty()) {
objectTransactions.remove(key);
}
}
}
}
}
protected void failAllTransactionsByKey(K key) {
Set<UUID> transactionsIds = objectTransactions.get(key);
if (transactionsIds != null) {
for (UUID otherTrId : transactionsIds) {
transactions.get(otherTrId).setFailed(true);
}
}
}
}

View File

@@ -0,0 +1,14 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
public interface HasVersion {
Integer getVersion();
default void setVersion(Integer version) {
}
}

View File

@@ -0,0 +1,83 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.Ticker;
import com.github.benmanes.caffeine.cache.Weigher;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Configuration
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true)
@EnableCaching
@Slf4j
public class JCPPCaffeineCacheConfiguration {
private final CacheSpecsMap configuration;
public JCPPCaffeineCacheConfiguration(CacheSpecsMap configuration) {
this.configuration = configuration;
}
@Bean
public CacheManager cacheManager() {
log.info("Initializing cache: {} specs {}", Arrays.toString(RemovalCause.values()), configuration.getSpecs());
SimpleCacheManager manager = new SimpleCacheManager();
if (configuration.getSpecs() != null) {
List<CaffeineCache> caches =
configuration.getSpecs().entrySet().stream()
.map(entry -> buildCache(entry.getKey(),
entry.getValue()))
.collect(Collectors.toList());
manager.setCaches(caches);
}
manager.initializeCaches();
return manager;
}
private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) {
final Caffeine<Object, Object> caffeineBuilder
= Caffeine.newBuilder()
.weigher(collectionSafeWeigher())
.maximumWeight(cacheSpec.getMaxSize())
.ticker(ticker());
if (!cacheSpec.getTimeToLiveInMinutes().equals(0)) {
caffeineBuilder.expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES);
}
return new CaffeineCache(name, caffeineBuilder.build());
}
@Bean
public Ticker ticker() {
return Ticker.systemTicker();
}
private Weigher<? super Object, ? super Object> collectionSafeWeigher() {
return (Weigher<Object, Object>) (key, value) -> {
if (value instanceof Collection) {
return ((Collection<?>) value).size();
}
return 1;
};
}
}

View File

@@ -0,0 +1,54 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
@Configuration
@ConditionalOnExpression("'${cache.type:null}'=='redis' && '${redis.connection.type:null}'=='cluster'")
@Slf4j
public class JCPPJCPPRedisClusterConfiguration extends JCPPRedisCacheConfiguration {
@Value("${redis.cluster.nodes:}")
private String clusterNodes;
@Value("${redis.cluster.max-redirects:12}")
private Integer maxRedirects;
@Value("${redis.cluster.useDefaultPoolConfig:true}")
private boolean useDefaultPoolConfig;
@Value("${redis.password:}")
private String password;
@Override
public LettuceConnectionFactory loadFactory() {
log.info("Initializing Redis Cluster on {}", clusterNodes);
RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration();
clusterConfiguration.setClusterNodes(getNodes(clusterNodes));
clusterConfiguration.setMaxRedirects(maxRedirects);
clusterConfiguration.setPassword(password);
return new LettuceConnectionFactory(clusterConfiguration, buildClientConfig());
}
private LettucePoolingClientConfiguration buildClientConfig() {
var clientConfigurationBuilder = LettucePoolingClientConfiguration.builder();
if (!useDefaultPoolConfig) {
clientConfigurationBuilder
.poolConfig(buildPoolConfig());
}
return clientConfigurationBuilder
.build();
}
}

View File

@@ -0,0 +1,59 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
@Configuration
@ConditionalOnMissingBean(JCPPCaffeineCacheConfiguration.class)
@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "sentinel")
@Slf4j
public class JCPPJCPPRedisSentinelConfiguration extends JCPPRedisCacheConfiguration {
@Value("${redis.sentinel.master:}")
private String master;
@Value("${redis.sentinel.sentinels:}")
private String sentinels;
@Value("${redis.sentinel.password:}")
private String sentinelPassword;
@Value("${redis.sentinel.useDefaultPoolConfig:true}")
private boolean useDefaultPoolConfig;
@Value("${redis.db:}")
private Integer database;
@Value("${redis.password:}")
private String password;
@Override
public LettuceConnectionFactory loadFactory() {
log.info("Initializing Redis Sentinel on {}, sentinels: {}", master, sentinels);
RedisSentinelConfiguration redisSentinelConfiguration = new RedisSentinelConfiguration();
redisSentinelConfiguration.setMaster(master);
redisSentinelConfiguration.setSentinels(getNodes(sentinels));
redisSentinelConfiguration.setSentinelPassword(sentinelPassword);
redisSentinelConfiguration.setPassword(password);
redisSentinelConfiguration.setDatabase(database);
return new LettuceConnectionFactory(redisSentinelConfiguration, buildClientConfig());
}
private LettucePoolingClientConfiguration buildClientConfig() {
var clientConfigurationBuilder = LettucePoolingClientConfiguration.builder();
if (!useDefaultPoolConfig) {
clientConfigurationBuilder.poolConfig(buildPoolConfig());
}
return clientConfigurationBuilder.build();
}
}

View File

@@ -0,0 +1,78 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import java.time.Duration;
@Configuration
@ConditionalOnMissingBean(JCPPCaffeineCacheConfiguration.class)
@ConditionalOnProperty(prefix = "redis.connection", value = "type", havingValue = "standalone")
@Slf4j
public class JCPPJCPPRedisStandaloneConfiguration extends JCPPRedisCacheConfiguration {
@Value("${redis.standalone.host:localhost}")
private String host;
@Value("${redis.standalone.port:6379}")
private Integer port;
@Value("${redis.standalone.clientName:standalone}")
private String clientName;
@Value("${redis.standalone.commandTimeout:30000}")
private Long commandTimeout;
@Value("${redis.standalone.shutdownTimeout:5000}")
private Long shutdownTimeout;
@Value("${redis.standalone.useDefaultClientConfig:true}")
private boolean useDefaultClientConfig;
@Value("${redis.standalone.usePoolConfig:false}")
private boolean usePoolConfig;
@Value("${redis.db:0}")
private Integer db;
@Value("${redis.password:}")
private String password;
@Override
public LettuceConnectionFactory loadFactory() {
log.info("Initializing Redis Standalone on {}:{}", host, port);
RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
standaloneConfiguration.setHostName(host);
standaloneConfiguration.setPort(port);
standaloneConfiguration.setDatabase(db);
standaloneConfiguration.setPassword(password);
return new LettuceConnectionFactory(standaloneConfiguration, buildClientConfig());
}
private LettucePoolingClientConfiguration buildClientConfig() {
var clientConfigurationBuilder = LettucePoolingClientConfiguration.builder();
if (!useDefaultClientConfig) {
clientConfigurationBuilder
.clientName(clientName)
.commandTimeout(Duration.ofMillis(commandTimeout))
.shutdownTimeout(Duration.ofMillis(shutdownTimeout));
}
if (usePoolConfig) {
clientConfigurationBuilder.poolConfig(buildPoolConfig());
}
return clientConfigurationBuilder.build();
}
}

View File

@@ -0,0 +1,168 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import io.lettuce.core.api.StatefulRedisConnection;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@Configuration
@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis")
@Data
@Slf4j
public abstract class JCPPRedisCacheConfiguration {
private static final String COMMA = ",";
private static final String COLON = ":";
@Value("${redis.evictTtlInMs:60000}")
private int evictTtlInMs;
@Value("${redis.pool_config.maxTotal:128}")
private int maxTotal;
@Value("${redis.pool_config.maxIdle:128}")
private int maxIdle;
@Value("${redis.pool_config.minIdle:16}")
private int minIdle;
@Value("${redis.pool_config.testOnBorrow:true}")
private boolean testOnBorrow;
@Value("${redis.pool_config.testOnReturn:true}")
private boolean testOnReturn;
@Value("${redis.pool_config.testWhileIdle:true}")
private boolean testWhileIdle;
@Value("${redis.pool_config.minEvictableMs:60000}")
private long minEvictableMs;
@Value("${redis.pool_config.evictionRunsMs:30000}")
private long evictionRunsMs;
@Value("${redis.pool_config.maxWaitMills:60000}")
private long maxWaitMills;
@Value("${redis.pool_config.numberTestsPerEvictionRun:3}")
private int numberTestsPerEvictionRun;
@Value("${redis.pool_config.blockWhenExhausted:true}")
private boolean blockWhenExhausted;
@Bean
public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory(LettuceConnectionFactory loadFactory) {
return loadFactory;
}
@Bean
public RedisConnectionFactory redisConnectionFactory(LettuceConnectionFactory loadFactory) {
return loadFactory;
}
@Bean
protected abstract LettuceConnectionFactory loadFactory();
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
DefaultFormattingConversionService redisConversionService = new DefaultFormattingConversionService();
RedisCacheConfiguration.registerDefaultConverters(redisConversionService);
registerDefaultConverters(redisConversionService);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().withConversionService(redisConversionService);
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(configuration)
.transactionAware()
.build();
}
@Bean
public ReactiveRedisTemplate<String, Object> reactiveRedisTemplate(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) {
RedisSerializationContext<String, Object> serializationContext = RedisSerializationContext
.<String, Object>newSerializationContext()
.key(new StringRedisSerializer())
.value(new GenericJackson2JsonRedisSerializer())
.hashKey(new StringRedisSerializer())
.hashValue(new GenericJackson2JsonRedisSerializer())
.build();
return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, serializationContext);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
private static void registerDefaultConverters(ConverterRegistry registry) {
Assert.notNull(registry, "ConverterRegistry must not be null!");
registry.addConverter(UUID.class, String.class, UUID::toString);
}
protected GenericObjectPoolConfig<StatefulRedisConnection<String, String>> buildPoolConfig() {
GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(maxTotal);
poolConfig.setMaxIdle(maxIdle);
poolConfig.setMinIdle(minIdle);
poolConfig.setTestOnBorrow(testOnBorrow);
poolConfig.setTestOnReturn(testOnReturn);
poolConfig.setTestWhileIdle(testWhileIdle);
poolConfig.setSoftMinEvictableIdleDuration(Duration.ofMillis(minEvictableMs));
poolConfig.setTimeBetweenEvictionRuns(Duration.ofMillis(evictionRunsMs));
poolConfig.setMaxWait(Duration.ofMillis(maxWaitMills));
poolConfig.setNumTestsPerEvictionRun(numberTestsPerEvictionRun);
poolConfig.setBlockWhenExhausted(blockWhenExhausted);
return poolConfig;
}
protected List<RedisNode> getNodes(String nodes) {
List<RedisNode> result;
if (!StringUtils.hasText(nodes)) {
result = Collections.emptyList();
} else {
result = new ArrayList<>();
for (String hostPort : nodes.split(COMMA)) {
String host = hostPort.split(COLON)[0];
int port = Integer.parseInt(hostPort.split(COLON)[1]);
result.add(new RedisNode(host, port));
}
}
return result;
}
}

View File

@@ -0,0 +1,18 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.lang.Nullable;
public interface JCPPRedisSerializer<K, T> {
@Nullable
byte[] serialize(@Nullable T t) throws SerializationException;
@Nullable
T deserialize(K key, @Nullable byte[] bytes) throws SerializationException;
}

View File

@@ -0,0 +1,45 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnection;
import java.io.Serializable;
import java.util.Objects;
@Slf4j
@RequiredArgsConstructor
public class RedisCacheTransaction<K extends Serializable, V extends Serializable> implements CacheTransaction<K, V> {
private final RedisTransactionalCache<K, V> cache;
private final RedisConnection connection;
@Override
public void put(K key, V value) {
cache.put(key, value, connection);
}
@Override
public boolean commit() {
try {
var execResult = connection.exec();
return execResult.stream().anyMatch(Objects::nonNull);
} finally {
connection.close();
}
}
@Override
public void rollback() {
try {
connection.discard();
} finally {
connection.close();
}
}
}

View File

@@ -0,0 +1,246 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import io.lettuce.core.RedisAsyncCommandsImpl;
import io.lettuce.core.RedisClient;
import io.lettuce.core.cluster.RedisAdvancedClusterAsyncCommandsImpl;
import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.support.NullValue;
import org.springframework.data.redis.connection.RedisClusterNode;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.lettuce.LettuceConnection;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
@Slf4j
public abstract class RedisTransactionalCache<K extends Serializable, V extends Serializable> implements TransactionalCache<K, V> {
static final byte[] BINARY_NULL_VALUE = RedisSerializer.java().serialize(NullValue.INSTANCE);
@Getter
private final String cacheName;
@Getter
private final LettuceConnectionFactory connectionFactory;
private final RedisSerializer<String> keySerializer = StringRedisSerializer.UTF_8;
private final JCPPRedisSerializer<K, V> valueSerializer;
protected final Expiration evictExpiration;
protected final Expiration cacheTtl;
protected final boolean cacheEnabled;
public RedisTransactionalCache(String cacheName,
CacheSpecsMap cacheSpecsMap,
LettuceConnectionFactory connectionFactory,
JCPPRedisCacheConfiguration configuration,
JCPPRedisSerializer<K, V> valueSerializer) {
this.cacheName = cacheName;
this.connectionFactory = connectionFactory;
this.valueSerializer = valueSerializer;
this.evictExpiration = Expiration.from(configuration.getEvictTtlInMs(), TimeUnit.MILLISECONDS);
this.cacheTtl = Optional.ofNullable(cacheSpecsMap)
.map(CacheSpecsMap::getSpecs)
.map(specs -> specs.get(cacheName))
.map(CacheSpecs::getTimeToLiveInMinutes)
.filter(ttl -> !ttl.equals(0))
.map(ttl -> Expiration.from(ttl, TimeUnit.MINUTES))
.orElseGet(Expiration::persistent);
this.cacheEnabled = Optional.ofNullable(cacheSpecsMap)
.map(CacheSpecsMap::getSpecs)
.map(x -> x.get(cacheName))
.map(CacheSpecs::getMaxSize)
.map(size -> size > 0)
.orElse(false);
}
@Override
public CacheValueWrapper<V> get(K key) {
if (!cacheEnabled) {
return null;
}
try (var connection = connectionFactory.getConnection()) {
byte[] rawValue = doGet(key, connection);
if (rawValue == null || rawValue.length == 0) {
return null;
} else if (Arrays.equals(rawValue, BINARY_NULL_VALUE)) {
return SimpleCacheValueWrapper.empty();
} else {
V value = valueSerializer.deserialize(key, rawValue);
return SimpleCacheValueWrapper.wrap(value);
}
}
}
protected byte[] doGet(K key, RedisConnection connection) {
return connection.stringCommands().get(getRawKey(key));
}
@Override
public void put(K key, V value) {
if (!cacheEnabled) {
return;
}
try (var connection = connectionFactory.getConnection()) {
put(key, value, connection);
}
}
public void put(K key, V value, RedisConnection connection) {
put(connection, key, value, RedisStringCommands.SetOption.UPSERT);
}
@Override
public void putIfAbsent(K key, V value) {
if (!cacheEnabled) {
return;
}
try (var connection = connectionFactory.getConnection()) {
put(connection, key, value, RedisStringCommands.SetOption.SET_IF_ABSENT);
}
}
@Override
public void evict(K key) {
if (!cacheEnabled) {
return;
}
try (var connection = connectionFactory.getConnection()) {
connection.keyCommands().del(getRawKey(key));
}
}
@Override
public void evict(Collection<K> keys) {
if (!cacheEnabled) {
return;
}
if (keys.isEmpty()) {
return;
}
try (var connection = connectionFactory.getConnection()) {
connection.keyCommands().del(keys.stream().map(this::getRawKey).toArray(byte[][]::new));
}
}
@Override
public void evictOrPut(K key, V value) {
if (!cacheEnabled) {
return;
}
try (var connection = connectionFactory.getConnection()) {
var rawKey = getRawKey(key);
var records = connection.keyCommands().del(rawKey);
if (records == null || records == 0) {
//We need to put the value in case of Redis, because evict will NOT cancel concurrent transaction used to "get" the missing value from cache.
connection.stringCommands().set(rawKey, getRawValue(value), evictExpiration, RedisStringCommands.SetOption.UPSERT);
}
}
}
@Override
public CacheTransaction<K, V> newTransactionForKey(K key) {
byte[][] rawKey = new byte[][]{getRawKey(key)};
RedisConnection connection = watch(rawKey);
return new RedisCacheTransaction<>(this, connection);
}
@Override
public CacheTransaction<K, V> newTransactionForKeys(List<K> keys) {
RedisConnection connection = watch(keys.stream().map(this::getRawKey).toArray(byte[][]::new));
return new RedisCacheTransaction<>(this, connection);
}
@Override
public <R> R getAndPutInTransaction(K key, Supplier<R> dbCall, Function<V, R> cacheValueToResult, Function<R, V> dbValueToCacheValue, boolean cacheNullValue) {
if (!cacheEnabled) {
return dbCall.get();
}
return TransactionalCache.super.getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue);
}
@SuppressWarnings("unchecked")
protected RedisConnection getConnection(byte[] rawKey) {
if (!connectionFactory.isClusterAware()) {
return connectionFactory.getConnection();
}
RedisClusterNode redisClusterNode = connectionFactory.getClusterConnection().clusterGetNodeForKey(rawKey);
Object nativeConnection = connectionFactory.getConnection().getNativeConnection();
RedisClusterAsyncCommands<?,?> connection = ((RedisAdvancedClusterAsyncCommandsImpl<?,?>) nativeConnection).getConnection(redisClusterNode.getId());
LettuceConnection lettuceConnection = new LettuceConnection(((RedisAsyncCommandsImpl) connection).getStatefulConnection(),
connectionFactory.getTimeout(),
RedisClient.create());
lettuceConnection.setConvertPipelineAndTxResults(connectionFactory.getConvertPipelineAndTxResults());
return lettuceConnection;
}
protected RedisConnection watch(byte[][] rawKeysList) {
RedisConnection connection = getConnection(rawKeysList[0]);
try {
connection.watch(rawKeysList);
connection.multi();
} catch (Exception e) {
connection.close();
throw e;
}
return connection;
}
protected byte[] getRawKey(K key) {
String keyString = cacheName + key.toString();
byte[] rawKey;
try {
rawKey = keySerializer.serialize(keyString);
} catch (Exception e) {
log.warn("Failed to serialize the cache key: {}", key, e);
throw new RuntimeException(e);
}
if (rawKey == null) {
log.warn("Failed to serialize the cache key: {}", key);
throw new IllegalArgumentException("Failed to serialize the cache key!");
}
return rawKey;
}
protected byte[] getRawValue(V value) {
if (value == null) {
return BINARY_NULL_VALUE;
} else {
try {
return valueSerializer.serialize(value);
} catch (Exception e) {
log.warn("Failed to serialize the cache value: {}", value, e);
throw new RuntimeException(e);
}
}
}
public void put(RedisConnection connection, K key, V value, RedisStringCommands.SetOption setOption) {
if (!cacheEnabled) {
return;
}
byte[] rawKey = getRawKey(key);
put(connection, rawKey, value, setOption);
}
public void put(RedisConnection connection, byte[] rawKey, V value, RedisStringCommands.SetOption setOption) {
byte[] rawValue = getRawValue(value);
connection.stringCommands().set(rawKey, rawValue, this.cacheTtl, setOption);
}
}

View File

@@ -0,0 +1,35 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.cache.Cache;
@ToString
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class SimpleCacheValueWrapper<T> implements CacheValueWrapper<T> {
private final T value;
@Override
public T get() {
return value;
}
public static <T> SimpleCacheValueWrapper<T> empty() {
return new SimpleCacheValueWrapper<>(null);
}
public static <T> SimpleCacheValueWrapper<T> wrap(T value) {
return new SimpleCacheValueWrapper<>(value);
}
@SuppressWarnings("unchecked")
public static <T> SimpleCacheValueWrapper<T> wrap(Cache.ValueWrapper source) {
return source == null ? null : new SimpleCacheValueWrapper<>((T) source.get());
}
}

View File

@@ -0,0 +1,86 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
public interface TransactionalCache<K extends Serializable, V extends Serializable> {
String getCacheName();
CacheValueWrapper<V> get(K key);
void put(K key, V value);
void putIfAbsent(K key, V value);
void evict(K key);
void evict(Collection<K> keys);
void evictOrPut(K key, V value);
CacheTransaction<K, V> newTransactionForKey(K key);
CacheTransaction<K, V> newTransactionForKeys(List<K> keys);
default V getOrFetchFromDB(K key, Supplier<V> dbCall, boolean cacheNullValue, boolean putToCache) {
if (putToCache) {
return getAndPutInTransaction(key, dbCall, cacheNullValue);
} else {
CacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
return cacheValueWrapper.get();
}
return dbCall.get();
}
}
default V getAndPutInTransaction(K key, Supplier<V> dbCall, boolean cacheNullValue) {
return getAndPutInTransaction(key, dbCall, Function.identity(), Function.identity(), cacheNullValue);
}
default <R> R getAndPutInTransaction(K key, Supplier<R> dbCall, Function<V, R> cacheValueToResult, Function<R, V> dbValueToCacheValue, boolean cacheNullValue) {
CacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
V cacheValue = cacheValueWrapper.get();
return cacheValue != null ? cacheValueToResult.apply(cacheValue) : null;
}
var cacheTransaction = newTransactionForKey(key);
try {
R dbValue = dbCall.get();
if (dbValue != null || cacheNullValue) {
cacheTransaction.put(key, dbValueToCacheValue.apply(dbValue));
cacheTransaction.commit();
return dbValue;
} else {
cacheTransaction.rollback();
return null;
}
} catch (Throwable e) {
cacheTransaction.rollback();
throw e;
}
}
default <R> R getOrFetchFromDB(K key, Supplier<R> dbCall, Function<V, R> cacheValueToResult, Function<R, V> dbValueToCacheValue, boolean cacheNullValue, boolean putToCache) {
if (putToCache) {
return getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue);
} else {
CacheValueWrapper<V> cacheValueWrapper = get(key);
if (cacheValueWrapper != null) {
var cacheValue = cacheValueWrapper.get();
return cacheValue == null ? null : cacheValueToResult.apply(cacheValue);
}
return dbCall.get();
}
}
}

View File

@@ -0,0 +1,51 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import java.io.Serializable;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Supplier;
public interface VersionedCache<K extends VersionedCacheKey, V extends Serializable & HasVersion> extends TransactionalCache<K, V> {
CacheValueWrapper<V> get(K key);
default V get(K key, Supplier<V> supplier) {
return get(key, supplier, true);
}
default V get(K key, Supplier<V> supplier, boolean putToCache) {
return Optional.ofNullable(get(key))
.map(CacheValueWrapper::get)
.orElseGet(() -> {
V value = supplier.get();
if (putToCache) {
put(key, value);
}
return value;
});
}
void put(K key, V value);
void evict(K key);
void evict(Collection<K> keys);
void evict(K key, Integer version);
default Integer getVersion(V value) {
if (value == null) {
return 0;
} else if (value.getVersion() != null) {
return value.getVersion();
} else {
return null;
}
}
}

View File

@@ -0,0 +1,15 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import java.io.Serializable;
public interface VersionedCacheKey extends Serializable {
default boolean isVersioned() {
return false;
}
}

View File

@@ -0,0 +1,86 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import sanbing.jcpp.infrastructure.util.JCPPPair;
import java.io.Serializable;
public abstract class VersionedCaffeineCache<K extends VersionedCacheKey, V extends Serializable & HasVersion> extends CaffeineTransactionalCache<K, V> implements VersionedCache<K, V> {
public VersionedCaffeineCache(CacheManager cacheManager, String cacheName) {
super(cacheManager, cacheName);
}
@Override
public CacheValueWrapper<V> get(K key) {
JCPPPair<Long, V> versionValuePair = doGet(key);
if (versionValuePair != null) {
return SimpleCacheValueWrapper.wrap(versionValuePair.getSecond());
}
return null;
}
@Override
public void put(K key, V value) {
Integer version = getVersion(value);
if (version == null) {
return;
}
doPut(key, value, version);
}
private void doPut(K key, V value, Integer version) {
lock.lock();
try {
JCPPPair<Long, V> versionValuePair = doGet(key);
if (versionValuePair == null || version > versionValuePair.getFirst()) {
failAllTransactionsByKey(key);
cache.put(key, wrapValue(value, version));
}
} finally {
lock.unlock();
}
}
private JCPPPair<Long, V> doGet(K key) {
Cache.ValueWrapper source = cache.get(key);
if (source != null && source.get() instanceof JCPPPair pair) {
return pair;
}
return null;
}
@Override
public void evict(K key) {
lock.lock();
try {
failAllTransactionsByKey(key);
cache.evict(key);
} finally {
lock.unlock();
}
}
@Override
public void evict(K key, Integer version) {
if (version == null) {
return;
}
doPut(key, null, version);
}
@Override
void doPutIfAbsent(K key, V value) {
cache.putIfAbsent(key, wrapValue(value, getVersion(value)));
}
private JCPPPair<Integer, V> wrapValue(V value, Integer version) {
return JCPPPair.of(version, value);
}
}

View File

@@ -0,0 +1,155 @@
/**
* 抖音关注:程序员三丙
* 知识星球https://t.zsxq.com/j9b21
*/
package sanbing.jcpp.infrastructure.cache;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
import java.util.Arrays;
@Slf4j
public abstract class VersionedRedisCache<K extends VersionedCacheKey, V extends Serializable & HasVersion> extends RedisTransactionalCache<K, V> implements VersionedCache<K, V> {
private static final int VERSION_SIZE = 8;
private static final int VALUE_END_OFFSET = -1;
static final byte[] SET_VERSIONED_VALUE_LUA_SCRIPT = StringRedisSerializer.UTF_8.serialize("""
local key = KEYS[1]
local newValue = ARGV[1]
local newVersion = tonumber(ARGV[2])
local expiration = tonumber(ARGV[3])
local function setNewValue()
local newValueWithVersion = struct.pack(">I8", newVersion) .. newValue
redis.call('SET', key, newValueWithVersion, 'EX', expiration)
end
-- Get the current version (first 8 bytes) of the current value
local currentVersionBytes = redis.call('GETRANGE', key, 0, 7)
if currentVersionBytes and #currentVersionBytes == 8 then
local currentVersion = struct.unpack(">I8", currentVersionBytes)
if newVersion > currentVersion then
setNewValue()
end
else
-- If the current value is absent or the current version is not found, set the new value
setNewValue()
end
""");
static final byte[] SET_VERSIONED_VALUE_SHA = StringRedisSerializer.UTF_8.serialize("0453cb1814135b706b4198b09a09f43c9f67bbfe");
public VersionedRedisCache(String cacheName, CacheSpecsMap cacheSpecsMap, LettuceConnectionFactory connectionFactory, JCPPRedisCacheConfiguration configuration, JCPPRedisSerializer<K, V> valueSerializer) {
super(cacheName, cacheSpecsMap, connectionFactory, configuration, valueSerializer);
}
@PostConstruct
public void init() {
try (var connection = getConnection(SET_VERSIONED_VALUE_SHA)) {
log.debug("Loading LUA with expected SHA[{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), connection.getNativeConnection());
String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT);
if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) {
log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}], connection [{}]", new String(SET_VERSIONED_VALUE_SHA), sha, connection.getNativeConnection());
}
} catch (Throwable t) {
log.error("Error on Redis versioned cache init", t);
}
}
@Override
protected byte[] doGet(K key, RedisConnection connection) {
if (!key.isVersioned()) {
return super.doGet(key, connection);
}
byte[] rawKey = getRawKey(key);
return connection.stringCommands().getRange(rawKey, VERSION_SIZE, VALUE_END_OFFSET);
}
@Override
public void put(K key, V value) {
if (!key.isVersioned()) {
super.put(key, value);
return;
}
Integer version = getVersion(value);
if (version == null) {
return;
}
doPut(key, value, version, cacheTtl);
}
@Override
public void put(K key, V value, RedisConnection connection) {
if (!key.isVersioned()) {
super.put(key, value, connection); // because scripting commands are not supported in transaction mode
return;
}
Integer version = getVersion(value);
if (version == null) {
return;
}
byte[] rawKey = getRawKey(key);
doPut(rawKey, value, version, cacheTtl, connection);
}
private void doPut(K key, V value, Integer version, Expiration expiration) {
if (!cacheEnabled) {
return;
}
log.trace("put [{}][{}][{}]", key, value, version);
final byte[] rawKey = getRawKey(key);
try (var connection = getConnection(rawKey)) {
doPut(rawKey, value, version, expiration, connection);
}
}
private void doPut(byte[] rawKey, V value, Integer version, Expiration expiration, RedisConnection connection) {
byte[] rawValue = getRawValue(value);
byte[] rawVersion = StringRedisSerializer.UTF_8.serialize(String.valueOf(version));
byte[] rawExpiration = StringRedisSerializer.UTF_8.serialize(String.valueOf(expiration.getExpirationTimeInSeconds()));
try {
connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration);
} catch (InvalidDataAccessApiUsageException e) {
log.debug("loading LUA [{}]", connection.getNativeConnection());
String sha = connection.scriptingCommands().scriptLoad(SET_VERSIONED_VALUE_LUA_SCRIPT);
if (!Arrays.equals(SET_VERSIONED_VALUE_SHA, StringRedisSerializer.UTF_8.serialize(sha))) {
log.error("SHA for SET_VERSIONED_VALUE_LUA_SCRIPT wrong! Expected [{}], but actual [{}]", new String(SET_VERSIONED_VALUE_SHA), sha);
}
try {
connection.scriptingCommands().evalSha(SET_VERSIONED_VALUE_SHA, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration);
} catch (InvalidDataAccessApiUsageException ignored) {
log.debug("Slowly executing eval instead of fast evalsha");
connection.scriptingCommands().eval(SET_VERSIONED_VALUE_LUA_SCRIPT, ReturnType.VALUE, 1, rawKey, rawValue, rawVersion, rawExpiration);
}
}
}
@Override
public void evict(K key, Integer version) {
log.trace("evict [{}][{}]", key, version);
if (version != null) {
doPut(key, null, version, evictExpiration);
}
}
@Override
public void putIfAbsent(K key, V value) {
throw new NotImplementedException("putIfAbsent is not supported by versioned cache");
}
@Override
public void evictOrPut(K key, V value) {
throw new NotImplementedException("evictOrPut is not supported by versioned cache");
}
}