The introduction
In this article, we will talk about how to combine local Cache and redis in the use of Spring Cache. That is, custom two-level caching.
1: Principle of custom cache
As mentioned in the previous article, the Cache interface defines the behavior of the Cache operation, and CacheManager defines how to generate the Cache. We need to define our own two-level Cache, so we need to define our own Cache and CacheManager. Spring has helped us provide a CompositeCacheManager implementation class for CacheManager.
2: How to achieve
Design and implementation ideas
- Here we use Caffeine(level 1 cache) + Redis(level 2 cache). The flow chart of realization effect is as follows:
2. Required key classes and interfaces
- CompositeCacheManager: This is an implementation of the CompositeCacheManager, of which
setCacheManagers
The CacheManager method allows you to set one or more CacheManager. - Cache: Define the behavior of the Cache operation, such as the one we can save to Redis or memory medium, which can be implemented by ourselves
- CacheSyncManager: a custom cache synchronization management interface that defines how caches are synchronized, such as Redis subscriptions or RabbitMq messages
- CaffeineRedisCacheManager: CacheManager that combined storage custom two levels of cache
- MultipleCache: Specifies the implementation of Cache
- MultipleCacheNode: cache node, local +Redis
3. Some codes and their implementation
CacheManager is related as follows:
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CompositeCacheManager cacheManager(CacheSyncManager cacheSyncManager, RedisCacheWriter redisCacheWriter) {
List<CacheManager> cacheManagerList = new LinkedList<>();
//caffeine
if(! CollectionUtils.isEmpty(cacheProperties.getCaffeine())) {/ / build CaffeineCacheManager
cacheManagerList.add(buildCaffeineCacheManager(cacheProperties.getCaffeine(), cacheSyncManager));
}
//redis
if(! CollectionUtils.isEmpty(cacheProperties.getRedis())) {/ / build RedisCacheManager
cacheManagerList.add(buildRedisCacheManager(redisCacheWriter, cacheSyncManager, cacheProperties.getRedis()));
}
//caffeine + redis
if(! CollectionUtils.isEmpty(cacheProperties.getMultiple())) { cacheManagerList.add(buildCaffeineRedis(cacheSyncManager, redisCacheWriter)); } CompositeCacheManager cacheManager =new CompositeCacheManager();
cacheManager.setCacheManagers(cacheManagerList);
return cacheManager;
}
/* * Build CaffeineCacheManger cache */
private CaffeineCacheManagerAdapter buildCaffeineCacheManager(Collection
configs, CacheSyncManager cacheSyncManager)
{
Set<CaffeineCacheManagerAdapter.CacheConfig> caffeineCacheConfigs = new LinkedHashSet<>();
Set<String> caffeineCacheNames = new LinkedHashSet<>();
Map<String, AbstractCaffeineCacheStrategy> cacheStrategyMap = new HashMap<>(8);
Map<String, Set<CacheDecorationHandler>> decorationHandlers = new HashMap<>(8);
configs.forEach(item -> {
caffeineCacheNames.add(item.getName());
CaffeineCacheManagerAdapter.CacheConfig cacheConfig = new CaffeineCacheManagerAdapter.CacheConfig();
cacheConfig.setExpireAfterAccess(item.getExpireAfterAccess());
cacheConfig.setExpireAfterWrite(item.getExpireAfterWrite());
cacheConfig.setInitialCapacity(item.getInitialCapacity());
cacheConfig.setMaximumSize(item.getMaximumSize());
cacheConfig.setName(item.getName());
cacheConfig.setDisableSync(item.isDisableSync());
cacheConfig.setEnableSoftRef(item.isEnableSoftRef());
/ / CacheLoader Caffeine.
if(! ObjectUtils.isEmpty(item.getCacheLoader()) && cacheLoaderMap.containsKey(item.getCacheLoader())) { cacheConfig.setCacheLoader(cacheLoaderMap.get(item.getCacheLoader())); }//Caffeine caches configuration information
caffeineCacheConfigs.add(cacheConfig);
//Caffeine customizes the cache storage policy and applies it if Caffeine has one.
if(! ObjectUtils.isEmpty(item.getStrategy()) && strategyMap.containsKey(item.getStrategy())) { CacheStrategy strategy = strategyMap.get(item.getStrategy());if (strategy instanceofAbstractCaffeineCacheStrategy) { cacheStrategyMap.put(item.getName(), (AbstractCaffeineCacheStrategy) strategy); }}// Cache wrapping policies, if any, are applied
if(! ObjectUtils.isEmpty(item.getDecorators())) { List<String> decoratorList = Arrays.asList(item.getDecorators().split(",")); Set<CacheDecorationHandler> collect = decoratorList.stream() .map(decorationHandlerMap::get).collect(Collectors.toSet()); decorationHandlers.put(item.getName(), collect); }});//CacheManager inherits from CaffeineCacheManager
return new CaffeineCacheManagerAdapter(caffeineCacheNames, caffeineCacheConfigs, cacheStrategyMap,
decorationHandlers, cacheSyncManager);
}
/* * Build RedisCacheManager */
private RedisCacheManagerAdapter buildRedisCacheManager(RedisCacheWriter cacheWriter, CacheSyncManager cacheSyncManager, Collection
configs)
{
Set<String> redisCacheNames = new LinkedHashSet<>();
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
Map<String, AbstractRedisCacheStrategy> cacheStrategyMap = new HashMap<>(8);
Map<String, Set<CacheDecorationHandler>> decorationHandlers = new HashMap<>(8);
configs.forEach(item -> {
// Cache name and cache key-value relationship, etc
redisCacheNames.add(item.getName());
redisCacheConfigurationMap.put(item.getName(), redisCacheConfiguration(item));
// Custom cache policy. If there is one, apply it. If not, use the default policy
if(item.getStrategy() ! =null && strategyMap.containsKey(item.getStrategy())) {
CacheStrategy strategy = strategyMap.get(item.getStrategy());
if (strategy instanceofAbstractRedisCacheStrategy) { cacheStrategyMap.put(item.getName(), (AbstractRedisCacheStrategy) strategy); }}else {
DefaultRedisCacheStrategy cacheStrategy = new DefaultRedisCacheStrategy(item.getName());
cacheStrategyMap.put(item.getName(), cacheStrategy);
}
// Cache wrapping policy, if any apply
if(! ObjectUtils.isEmpty(item.getDecorators())) { List<String> decoratorList = Arrays.asList(item.getDecorators().split(",")); Set<CacheDecorationHandler> collect = decoratorList.stream() .map(decorationHandlerMap::get).collect(Collectors.toSet()); decorationHandlers.put(item.getName(), collect); }});//CacheManager inherits from RedisCacheManager
RedisCacheManagerAdapter redisCacheManager = new RedisCacheManagerAdapter(cacheWriter, redisCacheNames,
false, redisCacheConfigurationMap, cacheStrategyMap, decorationHandlers);
redisCacheManager.initCaches();
return redisCacheManager;
}
/ * * build CaffeineRedisCacheManager * /
private CaffeineRedisCacheManager buildCaffeineRedis(CacheSyncManager cacheSyncManager, RedisCacheWriter redisCacheWriter) {
Set<CacheConfigProperties.CaffeineCacheConfig> caffeineCacheConfigs = new LinkedHashSet<>();
Set<CacheConfigProperties.RedisCacheConfig> redisCacheConfigs = new LinkedHashSet<>();
Map<String, Set<CacheDecorationHandler>> decorationHandlers = new HashMap<>(8);
cacheProperties.getMultiple().forEach(item -> {
// Cache configuration parameters
CacheConfigProperties.CaffeineCacheConfig caffeineCacheConfig = item.getCaffeine();
caffeineCacheConfig.setName(item.getName());
CacheConfigProperties.RedisCacheConfig redisCacheConfig = item.getRedis();
redisCacheConfig.setName(item.getName());
caffeineCacheConfigs.add(caffeineCacheConfig);
redisCacheConfigs.add(redisCacheConfig);
// Cache decorator policy
if(! ObjectUtils.isEmpty(item.getDecorators())) { List<String> decoratorList = Arrays.asList(item.getDecorators().split(",")); Set<CacheDecorationHandler> collect = decoratorList.stream() .map(decorationHandlerMap::get).collect(Collectors.toSet()); decorationHandlers.put(item.getName(), collect); }});/ / CacheManager that jointly by RedisCacheManagerAdapter + CaffeineCacheManagerAdapter implementation
CaffeineRedisCacheManager multipleCacheManager
= new CaffeineRedisCacheManager(
buildCaffeineCacheManager(caffeineCacheConfigs, cacheSyncManager),
buildRedisCacheManager(redisCacheWriter, cacheSyncManager, redisCacheConfigs), decorationHandlers);
multipleCacheManager.initializeCaches();
return multipleCacheManager;
}
Copy the code
Cache is related as follows:
public class MultipleCache implements Cache {
private MultipleCacheNode cacheNode;
public static MultipleCacheBuilder builder(a) {
return new MultipleCacheBuilder();
}
public MultipleCache(MultipleCacheNode cacheNode) {
this.cacheNode = cacheNode;
}
@Override
public String getName(a) {
return cacheNode.getName();
}
@Override
public Object getNativeCache(a) {
return cacheNode.getNativeCache();
}
@Override
public ValueWrapper get(Object key) {
return cacheNode.get(key);
}
@Override
public <T> T get(Object key, Class<T> type) {
return (T) cacheNode.get(key, type);
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
return (T) cacheNode.get(key, valueLoader);
}
@Override
public void put(Object key, Object value) {
cacheNode.put(key, value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
return cacheNode.putIfAbsent(key, value);
}
@Override
public void evict(Object key) {
cacheNode.evict(key);
}
@Override
public void clear(a) {
cacheNode.clear();
}
public static class MultipleCacheBuilder {
private MultipleCacheNode cache;
public MultipleCacheBuilder nextNode(Cache cache) {
MultipleCacheNode node = new MultipleCacheNode(cache);
if (this.cache == null) {
this.cache = node;
} else {
this.cache.setNext(node);
}
return this;
}
public MultipleCache build(a) {
return newMultipleCache(cache); }}}public class MultipleCacheNode<T extends Cache> implements Cache {
// Next cache Node
private MultipleCacheNode next;
// Current cache
private T cache;
public MultipleCacheNode(T cache) {
this.cache = cache;
}
public void setNext(MultipleCacheNode next) {
this.next = next;
}
public boolean hasNext(a) {
return null! = next; }@Override
public String getName(a) {
return cache.getName();
}
@Override
public Object getNativeCache(a) {
return cache.getNativeCache();
}
@Override
public ValueWrapper get(Object key) {
ValueWrapper value = cache.get(key);
if (null == value && hasNext()) {
value = next.get(key);
if (null != value) {
cache.putIfAbsent(key, value.get());
}
}
return value;
}
@Override
public <T> T get(Object key, Class<T> type) {
return cache.get(key, type);
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
return cache.get(key, valueLoader);
}
@Override
public void put(Object key, Object value) {
if (hasNext()) {
next.put(key, value);
}
cache.put(key, value);
}
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
return cache.putIfAbsent(key, value);
}
@Override
public void evict(Object key) {
if (hasNext()) {
next.evict(key);
}
cache.evict(key);
}
@Override
public void clear(a) {
if(hasNext()) { next.clear(); } cache.clear(); }}Copy the code
CacheSync related:
@Bean(name = "redisCacheMessageSyncListenerContainer")
@ConditionalOnMissingBean(name = "redisCacheMessageSyncListenerContainer")
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, CacheSyncMessageListener receiver,
@Qualifier("syncCacheTaskExecutor") TaskExecutor executor) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setTaskExecutor(executor);
container.addMessageListener(receiver, new ChannelTopic(receiver.getChannelName()));
return container;
}
@Bean
@ConditionalOnMissingBean(CacheSyncManager.class)
public CacheSyncManager redisBasedCacheSyncServce(RedisTemplate redisTemplate) {
return new RedisCacheSyncManager(applicationName, redisTemplate);
}
@Bean
@ConditionalOnMissingBean(CacheSyncMessageListener.class)
public CacheSyncMessageListener cacheSyncMessageListener(CacheSyncManager cacheSyncManager, RedisTemplate redisTemplate) {
return new CacheSyncMessageListener(redisTemplate, cacheSyncManager);
}
/* * Cache synchronization processing interface definition */
public interface CacheSyncManager{
String SYNCCHANNEL = "cache-sync";
void publish(CacheSyncEvent event);
void handle(CacheSyncEvent event);
String getChannelName(a);
}
/* * Abstract cache synchronization class */
@Slf4j
public abstract class AbstractCacheSyncManager implements CacheSyncManager {
private static Map<String, CacheSyncEventHandler> handlerMap = new ConcurrentHashMap<>();
public static void registHandler(String name, CacheSyncEventHandler handler) {
handlerMap.put(name, handler);
}
protected String applicationName;
public AbstractCacheSyncManager(String appName) {
this.applicationName = appName;
}
public static void doHandle(CacheSyncEvent event) {
CacheSyncEventHandler handler = handlerMap.get(event.getCacheName());
if (null == handler) {
log.warn("Non-existent cached message synchronizer: {}", event);
return;
}
// Related cache events
if (event instanceof PutEvent) {
handler.handlePut((PutEvent) event);
} else if (event instanceof EvictEvent) {
handler.handleEvict((EvictEvent) event);
} else if (event instanceof ClearEvent) {
handler.handleClear((ClearEvent) event);
} else {
log.warn("Unsupported events: {}", event); }}@Override
public void handle(CacheSyncEvent event) {
doHandle(event);
}
@Override
public String getChannelName(a) {
return applicationName + ":"+ SYNCCHANNEL; }}/* * Use Redis publish subscription for message synchronization */
@Slf4j
public class RedisCacheSyncManager extends AbstractCacheSyncManager {
private RedisTemplate redisTemplate;
public RedisCacheSyncManager(String appName, RedisTemplate redisTemplate) {
super(appName);
this.redisTemplate = redisTemplate;
}
@Override
public void publish(CacheSyncEvent event) {
redisTemplate.convertAndSend(getChannelName(), event);
log.info("Send cache synchronization message: channel: {}, event: {}", getChannelName(), event); }}/* * Cache event definition */
public interface CacheSyncEventHandler {
/** * put the cache event *@param event
*/
void handlePut(PutEvent event);
/** * Clear the cache event *@param event
*/
void handleEvict(EvictEvent event);
/** * clear the cache event *@param event
*/
void handleClear(ClearEvent event);
}
/* * Cache message listener */
@Slf4j
public class CacheSyncMessageListener implements MessageListener {
private RedisTemplate redisTemplate;
private CacheSyncManager cacheSyncManager;
public CacheSyncMessageListener(RedisTemplate redisTemplate, CacheSyncManager cacheSyncManager) {
this.redisTemplate = redisTemplate;
this.cacheSyncManager = cacheSyncManager;
}
@Override
public void onMessage(Message message, byte[] pattern) {
log.debug("Cache synchronization message received: {}", message);
try {
CacheSyncEvent event = (CacheSyncEvent) redisTemplate
.getValueSerializer().deserialize(message.getBody());
if (ObjectUtils.nullSafeEquals(HostUtil.getHostName(), event.getHost())) {
log.debug("This message is sent from this machine without processing: {}", event);
return;
}
cacheSyncManager.handle(event);
} catch (Exception e) {
log.error("Abnormal synchronization message!", e); }}public String getChannelName(a) {
returncacheSyncManager.getChannelName(); }}Copy the code
Use the spring.factories mechanism to ensure that it can be scanned by the SpringBoot project
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zy.github.multiple.cache.config.CacheManagerAutoConfiguration
Copy the code
This is the key part of the code. In summary, the key part is:
- 1: Implements the CacheManager
- 2: Implements its own Cache
- 3: implements synchronization between local caches
How to use
1: add @enablecaching to the startup class
@EnableCaching
@SpringBootApplication
public class MultipleCacheApplication {
public static void main(String[] args) { SpringApplication.run(MultipleCacheApplication.class, args); }}Copy the code
2: Configure Cache attributes
spring:
redis:
port: # redis server port
host: # redis server host
lettuce:
pool:
max-active: 50
max-wait: 2000
max-idle: 20
min-idle: 5
# cluster:
# nodes:
# lettuce:
# pool:
# max-active: 50
# max-wait: 2000
# max-idle: 20
# min-idle: 5
application:
name: aaaaaaaaaa
multiple-cache:
# redis:
# -name: testCache # Cache name
Expire: 100 # Cache expire time
# caffeine:
# -name: testCache # Cache name
# expireAfterAccess: 30 # Cache expiration time
# initialCapacity: 100 # Cache initialization storage size
# cache maximum storage size
multiple:
- name: testCache # cache name
caffeine:
expireAfterAccess: 30 # Cache expiration time
initialCapacity: 100 The cache initializes the storage size
maximumSize: 1000 # Cache maximum storage size
redis:
expire: 100 # Cache expiration time
Copy the code
3: There is no change in the way of use, or based on the form of annotations.
@RestController
public class DemoController {
@Autowired
private DemoService demoService;
@RequestMapping("cache-test")
public List<User> demo(a){
return demoService.cacheTest("testId"); }}@Service
public class DemoService {
@Cacheable(cacheNames = "testCache", key = "#id")
public List<User> cacheTest(String id){
User user = new User();
user.setAge(22);
user.setName("xxx");
List<User> users = new ArrayList<>();
users.add(user);
returnusers; }}Copy the code
4:
5: dependency
<? The XML version = "1.0" encoding = "utf-8"? > < 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 https://maven.apache.org/xsd/maven-4.0.0.xsd" > < modelVersion > 4.0.0 < / modelVersion > < groupId > com. Making. Zy < / groupId > < artifactId > multiple cache - < / artifactId > <version>0.0.3</version> <name>multiple-cache</name> <description> Multiple-cache </description> <properties> < Java version > 1.8 < / Java version > < Jackson. Version > 2.11.3 < / Jackson version > < Commons - pool2 version > 2.9.0 < / Commons - pool2. Version > < caffeine. Version > 2.8.5 < / caffeine. Version > < lettuce version > 6.0.1. RELEASE < / lettuce. Version > < maven - plugins. Version > 3.2.0 < / maven - plugins. Version > < / properties > <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> < artifactId > spring - the boot - dependencies < / artifactId > < version > 2.4.0 < / version > < type > pom < type > the < scope > import < / scope > </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>${lettuce.version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>${caffeine.version}</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>${commons-pool2.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>${maven-plugins.version}</version> <executions> <execution> <phase>package</phase> <goals> <goal>jar-no-fork</goal> </goals> <configuration> <excludes> <exclude>**/MultipleCacheApplication.java</exclude> <exclude>**/application.yml</exclude> </excludes> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>${maven-plugins.version}</version> <configuration> <excludes> <exclude>**/application.yml</exclude> <exclude>**/MultipleCacheApplication**</exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>Copy the code
portal
The github link is attached