# springboot-cache **Repository Path**: feng_zhang/springboot-cache ## Basic Information - **Project Name**: springboot-cache - **Description**: 多级缓存使用。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2022-03-25 - **Last Updated**: 2022-03-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # springboot-cache #### 项目介绍 多级缓存使用。 #### 软件架构 redis ehcache #### 安装教程 1. ehcache 缓存 ``` 1) Maven依赖 org.springframework.boot spring-boot-starter-cache org.ehcache ehcache 3.3.0 2)类配置 3)xml配置 ehcache.xml java.lang.String java.lang.String 1 1 1 20 60 500 配置类中加载: System.out.println("[Ehcache配置初始化<开始>]"); // 配置默认缓存属性 cacheManager = CacheManagerBuilder.newCacheManager(new XmlConfiguration(getClass().getResource("/ehcache.xml"))); cacheManager.init(); System.out.println("[Ehcache配置初始化<完成>]"); 配置到application.yml中: spring: cache: ehcache: config: ehcache.xml 4)代码测试 5)注解测试 @Service public class EhCacheService { @Cacheable(value="test") public String readCache(){ System.out.println("是否缓存,如果没有执行,说明缓存了"); return "1234"; } @CachePut(value="test") public String update() { //更新缓存,将会改变内容 System.out.println("缓存更新了"); return "456"; } @CacheEvict(value="test",allEntries = true) public void deleteAll() { System.out.println("移除所有缓存"); } } 6) 控制器代码 @RestController @RequestMapping("/ehcache") public class EhCacheController { @Autowired public EhCacheService ehCacheService; @GetMapping("/read") public String readEhCache(){ return ehCacheService.readCache(); } @GetMapping("/deleteAll") public String deleteAll(){ ehCacheService.deleteAll(); return "移除成功"; } @GetMapping("/update") public String update(){ ehCacheService.update(); return "更新成功"; } } ``` 2. 集成Redis ``` 1)Maven依赖 org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 2.4.2 2)配置类 @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) @Slf4j public class RedisCacheConfiguration { /** * 缓存模板 * @param lettuceConnectionFactory * @return */ @Bean public RedisTemplate redisCacheTemplate(LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(lettuceConnectionFactory); return template; } /** * 缓存管理器 * @param lettuceConnectionFactory * @return */ @Bean public CacheManager cacheManager(LettuceConnectionFactory lettuceConnectionFactory) { RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(lettuceConnectionFactory); @SuppressWarnings("serial") Set cacheNames = new HashSet() { { add("codeNameCache"); } }; builder.initialCacheNames(cacheNames); return builder.build(); } } 3)配置文件 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password= # 连接超时时间(毫秒) spring.redis.timeout=10000 # Redis默认情况下有16个分片,这里配置具体使用的分片 spring.redis.database=0 # 连接池最大连接数(使用负值表示没有限制) 默认 8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 spring.redis.lettuce.pool.max-wait=-1 # 连接池中的最大空闲连接 默认 8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认 0 spring.redis.lettuce.pool.min-idle=0 4)测试类 // TODO 测试线程安全 ExecutorService executorService = Executors.newFixedThreadPool(1000); IntStream.range(0, 1000).forEach(i -> executorService.execute(() -> stringRedisTemplate.opsForValue().increment("kk", 1)) ); stringRedisTemplate.opsForValue().set("k1", "v1"); final String k1 = stringRedisTemplate.opsForValue().get("k1"); log.info("[字符缓存结果] - [{}]", k1); // TODO Redis支持的命令它都支持 String key = "battcn:user:1"; redisCacheTemplate.opsForValue().set(key, new User(1L, "u1", "pa")); // TODO 对应 String(字符串) final User user = (User) redisCacheTemplate.opsForValue().get(key); log.info("[对象缓存结果] - [{}]", user); } ``` 3. 多级缓存整合 ``` 1)消息类 @Data public class CacheMessage implements Serializable { private static final long serialVersionUID = 5987219310442078193L; private String cacheName; private Object key; private Integer sender; public CacheMessage(String cacheName, Object key) { super(); this.cacheName = cacheName; this.key = key; } public CacheMessage(String cacheName, Object key, Integer sender) { super(); this.cacheName = cacheName; this.key = key; this.sender = sender; } } 2)自定义缓存类 @Slf4j public class RedisEhcacheCache extends AbstractValueAdaptingCache { private String name; private RedisTemplate redisTemplate; private Cache ehcacheCache; private String cachePrefix; private long defaultExpiration = 0; private Map expires; private String topic = "cache:redis:ehcache:topic"; protected RedisEhcacheCache(boolean allowNullValues) { super(allowNullValues); } public RedisEhcacheCache(String name, RedisTemplate redisTemplate, Cache ehcacheCache, RedisEhcacheProperties redisEhcacheProperties) { super(redisEhcacheProperties.isCacheNullValues()); this.name = name; this.redisTemplate = redisTemplate; this.ehcacheCache = ehcacheCache; this.cachePrefix = redisEhcacheProperties.getCachePrefix(); this.defaultExpiration = redisEhcacheProperties.getRedis().getDefaultExpiration(); this.expires = redisEhcacheProperties.getRedis().getExpires(); this.topic = redisEhcacheProperties.getRedis().getTopic(); log.info("自定义缓存器"+name); } @Override public String getName() { return this.name; } @Override public Object getNativeCache() { return this; } @SuppressWarnings("unchecked") @Override public T get(Object key, Callable valueLoader) { log.info("调用自定义缓存GET方法"); Object value = lookup(key); if(value != null) { return (T) value; } ReentrantLock lock = new ReentrantLock(); try { lock.lock(); value = lookup(key); if(value != null) { return (T) value; } value = valueLoader.call(); Object storeValue = toStoreValue(valueLoader.call()); put(key, storeValue); return (T) value; } catch (Exception e) { try { Class c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException"); Constructor constructor = c.getConstructor(Object.class, Callable.class, Throwable.class); RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause()); throw exception; } catch (Exception e1) { throw new IllegalStateException(e1); } } finally { lock.unlock(); } } //从持久层读取value,然后存入缓存。允许value = null @Override public void put(Object key, Object value) { log.info("调用自定义缓存PUT方法"+getKey(key)); if (!super.isAllowNullValues() && value == null) { this.evict(key); return; } long expire = getExpire(); if(expire > 0) { redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS); } else { redisTemplate.opsForValue().set(getKey(key), toStoreValue(value)); } //通过redis推送消息,使其他服务的ehcache失效。 //原来的有个缺点:服务1给缓存put完KV后推送给redis的消息,服务1本身也会接收到该消息, // 然后会将刚刚put的KV删除。这里把ehcacheCache的hashcode传过去,避免这个问题。 push(new CacheMessage(this.name, key, this.ehcacheCache.hashCode())); ehcacheCache.put(key, value); } //key的生成 name:cachePrefix:key private Object getKey(Object key) { log.info("调用自定义缓存getKey方法"); return this.name.concat(":").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString())); } private long getExpire() { log.info("调用自定义缓存getExpire方法"); long expire = defaultExpiration; Long cacheNameExpire = expires.get(this.name); return cacheNameExpire == null ? expire : cacheNameExpire.longValue(); } @Override public ValueWrapper putIfAbsent(Object key, Object value) { Object cacheKey = getKey(key); Object prevValue = null; // 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作 synchronized (key) { prevValue = redisTemplate.opsForValue().get(cacheKey); if(prevValue == null) { long expire = getExpire(); if(expire > 0) { redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS); } else { redisTemplate.opsForValue().set(getKey(key), toStoreValue(value)); } // push(new CacheMessage(this.name, key, this.ehcacheCache.hashCode())); // ehcacheCache.put(key, toStoreValue(value)); } } return toValueWrapper(prevValue); } @Override public void evict(Object key) { log.info("调用自定义缓存evict方法"); // 先清除redis中缓存数据,然后清除ehcache中的缓存,避免短时间内如果先清除ehcache缓存后其他请求会再从redis里加载到ehcache中 redisTemplate.delete(getKey(key)); push(new CacheMessage(this.name, key, this.ehcacheCache.hashCode())); // ehcacheCache.remove(key); } @Override public void clear() { // 先清除redis中缓存数据,然后清除ehcache中的缓存,避免短时间内如果先清除ehcache缓存后其他请求会再从redis里加载到ehcache中 Set keys = redisTemplate.keys(this.name.concat(":")); for(Object key : keys) { redisTemplate.delete(key); } push(new CacheMessage(this.name, null)); ehcacheCache.clear(); } //获根据key取缓存,如果返回null,则要读取持久层 @Override protected Object lookup(Object key) { Object cacheKey = getKey(key); Object value = ehcacheCache.get(key); if(value != null) { log.info("获取ehcache缓存, 这个键为 : {}", cacheKey); return value; } value = redisTemplate.opsForValue().get(cacheKey); log.info("值:"+value); log.info("键:"+cacheKey); if(value != null) { log.info("获取redis缓存并添加到ehcache, 这个键为: {}", cacheKey); //将二级缓存重新复制到一级缓存。原理是最近访问的key很可能再次被访问 ehcacheCache.put(cacheKey, value); } return value; } /** * 缓存变更时,利用redis的消息订阅功能,通知其他节点清理本地缓存。 * @description * @param message */ private void push(CacheMessage message) { redisTemplate.convertAndSend(topic, message); } /** * @description 清理本地缓存 * @param key */ public void clearLocal(Object key) { log.info("清理本地缓存, 这个键为 : {}", key); if(key == null) { ehcacheCache.clear(); } else { ehcacheCache.remove(key); } } public Cache getLocalCache(){ return ehcacheCache; } } 3)监听redis消息类 @Slf4j public class CacheMessageListener implements MessageListener { private RedisTemplate redisTemplate; private RedisEhcacheCacheManager redisEhcacheCacheManager; public CacheMessageListener(RedisTemplate redisTemplate, RedisEhcacheCacheManager redisEhcacheCacheManager) { super(); this.redisTemplate = redisTemplate; this.redisEhcacheCacheManager = redisEhcacheCacheManager; log.info("2、缓存监听"); } @Override public void onMessage(Message message, byte[] pattern) { CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody()); log.info("接受redis消息, 缓存名为: {}, 这个键为: {}", cacheMessage.getCacheName(), cacheMessage.getKey()); //清除ehcache缓存 redisEhcacheCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey(), cacheMessage.getSender()); } } 4)自定义缓存管理类 @Slf4j public class RedisEhcacheCacheManager implements CacheManager { private ConcurrentMap cacheMap = new ConcurrentHashMap(); private RedisEhcacheProperties redisEhcacheProperties; private RedisTemplate redisTemplate; private boolean dynamic = true; private Set cacheNames; private org.ehcache.CacheManager ehCacheManager; private CacheConfiguration configuration; public RedisEhcacheCacheManager(RedisEhcacheProperties redisEhcacheProperties, RedisTemplate redisTemplate) { super(); this.redisEhcacheProperties = redisEhcacheProperties; this.redisTemplate = redisTemplate; this.dynamic = redisEhcacheProperties.isDynamic(); this.cacheNames = redisEhcacheProperties.getCacheNames(); setAboutEhCache(); } /** * 设置EhCache缓存 */ private void setAboutEhCache(){ long ehcacheExpire = redisEhcacheProperties.getEhcache().getExpireAfterWrite(); this.configuration = CacheConfigurationBuilder .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(redisEhcacheProperties.getEhcache().getMaxEntry())) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(ehcacheExpire))) .build(); this.ehCacheManager = CacheManagerBuilder .newCacheManagerBuilder() .build(); this.ehCacheManager.init(); log.info("1、设置缓存"); } /** * 获取缓存 * @param name * @return */ @Override public Cache getCache(String name) { Cache cache = cacheMap.get(name); if(cache != null) { return cache; } if(!dynamic && !cacheNames.contains(name)) { return cache; } cache = new RedisEhcacheCache(name, redisTemplate, getEhcache(name), redisEhcacheProperties); Cache oldCache = cacheMap.putIfAbsent(name, cache); log.debug("创建缓存实例, 缓存名字为 : {}", name); return oldCache == null ? cache : oldCache; } /** * 获取Ehcache 缓存 * @param name * @return */ public org.ehcache.Cache getEhcache(String name){ org.ehcache.Cache res = ehCacheManager.getCache(name, Object.class, Object.class); if(res != null){ return res; } return ehCacheManager.createCache(name, configuration); } @Override public Collection getCacheNames() { return this.cacheNames; } public void clearLocal(String cacheName, Object key, Integer sender) { Cache cache = cacheMap.get(cacheName); if(cache == null) { return ; } RedisEhcacheCache redisEhcacheCache = (RedisEhcacheCache) cache; //如果是发送者本身发送的消息,就不进行key的清除 if(redisEhcacheCache.getLocalCache().hashCode() != sender) { redisEhcacheCache.clearLocal(key); } } } 5)缓存配置类CacheRedisEhcacheAutoConfiguration @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) //二级缓存和一级缓存切换:true 启用二级缓存,false禁用二级缓存 @ConditionalOnProperty(name = "cache.use2L", havingValue = "true", matchIfMissing = false) @EnableConfigurationProperties(RedisEhcacheProperties.class) @Slf4j public class CacheRedisEhcacheAutoConfiguration { @Autowired private RedisEhcacheProperties redisEhcacheProperties; /** * Redis+Ehcache缓存管理 * @param redisTemplate * @return */ @Bean public RedisEhcacheCacheManager cacheManager(RedisTemplate redisTemplate) { return new RedisEhcacheCacheManager(redisEhcacheProperties, redisTemplate); } /** * RedisMessage 监听 * @param redisTemplate * @param redisEhcacheCacheManager * @return */ @Bean @ConditionalOnBean(RedisEhcacheCacheManager.class) public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate redisTemplate, RedisEhcacheCacheManager redisEhcacheCacheManager) { RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory()); CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisEhcacheCacheManager); //添加缓存监听器 log.info("3、添加缓存监听到Redis"); redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(redisEhcacheProperties.getRedis().getTopic())); return redisMessageListenerContainer; } } 6)Redis配置类RedisCacheConfig @Configuration @EnableCaching//开启注解 @ConditionalOnProperty(name = "cache.use2L", havingValue = "false", matchIfMissing = true) @EnableConfigurationProperties(RedisEhcacheProperties.class) @Slf4j public class RedisCacheConfig { /** * 缓存模板 * * @param lettuceConnectionFactory * @return */ @Bean public RedisTemplate redisCacheTemplate(LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate template = new RedisTemplate<>(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(lettuceConnectionFactory); return template; } /** * 缓存管理器 * * @param lettuceConnectionFactory * @return */ @Bean public CacheManager cacheManager(LettuceConnectionFactory lettuceConnectionFactory) { RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder .fromConnectionFactory(lettuceConnectionFactory); return builder.build(); } } 7)EhCache配置文件 8)属性配置文件 spring.redis.hostName=127.0.0.1 spring.redis.port=6379 spring.redis.timeOut=1000 spring.redis.maxIdle=10 spring.redis.maxWaitMillis=15000 spring.redis.testOnBorrow=true spring.redis.testWhileIdle=false ## redis-starter的配置 spring.cache.cache-names=cache1,cache2,cache3 spring.redis.timeout=10000 #自定义配置。expire统一单位为毫秒 cache.multi.cacheNames=cache1,cache2,cache3 cache.multi.ehcache.expireAfterWrite=5000 cache.multi.ehcache.maxEntry=1000 cache.multi.redis.defaultExpiration=60000 cache.multi.redis.expires.cache1=50000 cache.multi.redis.expires.cache2=70000 cache.multi.redis.expires.cache3=70000 #开启二级缓存 cache.use2L=true 9)测试 @Component @Slf4j public class CacheTestService { //cacheManager = "cacheManager"可以不指定 @Cacheable(value = "gerritCache", key = "#root.targetClass + '_' + #root.methodName + '_' + #root.args[0]", cacheManager = "cacheManager") public String get(long id) { log.info("获取数据库数据"); return "test"; } } public class CacheTestServiceTest extends SpringbootCacheApplicationTests { @Autowired private CacheTestService cacheTestService; @Test public void get0() throws Exception { cacheTestService.get(123L); } } 说明: 如果cache.use2L=true设置为true 将开启Ehcache缓存,如果设置为false则为只使用Redsi缓存。 ``` #### 使用说明 1. xxxx 2. xxxx 3. xxxx