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

  1. 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 whichsetCacheManagersThe 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