mirror of
https://gitee.com/san-bing/JChargePointProtocol
synced 2026-05-04 09:59:55 +08:00
云快充1.5.0 初始化
This commit is contained in:
51
jcpp-infrastructure-cache/pom.xml
Normal file
51
jcpp-infrastructure-cache/pom.xml
Normal 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>
|
||||
13
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheConstants.java
vendored
Normal file
13
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheConstants.java
vendored
Normal 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";
|
||||
|
||||
}
|
||||
13
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecs.java
vendored
Normal file
13
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecs.java
vendored
Normal 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;
|
||||
}
|
||||
22
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecsMap.java
vendored
Normal file
22
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecsMap.java
vendored
Normal 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;
|
||||
|
||||
}
|
||||
15
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheTransaction.java
vendored
Normal file
15
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheTransaction.java
vendored
Normal 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();
|
||||
|
||||
}
|
||||
11
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheValueWrapper.java
vendored
Normal file
11
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheValueWrapper.java
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 抖音关注:程序员三丙
|
||||
* 知识星球:https://t.zsxq.com/j9b21
|
||||
*/
|
||||
package sanbing.jcpp.infrastructure.cache;
|
||||
|
||||
public interface CacheValueWrapper<T> {
|
||||
|
||||
T get();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
14
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/HasVersion.java
vendored
Normal file
14
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/HasVersion.java
vendored
Normal 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) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
51
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java
vendored
Normal file
51
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
15
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCacheKey.java
vendored
Normal file
15
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCacheKey.java
vendored
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
155
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java
vendored
Normal file
155
jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java
vendored
Normal 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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user