├── README.md └── redis-caffeine-cache-starter ├── .gitignore ├── pom.xml └── src └── main ├── java └── com │ └── axin │ └── idea │ └── rediscaffeinecachestarter │ ├── CacheRedisCaffeineAutoConfiguration.java │ ├── CacheRedisCaffeineProperties.java │ └── support │ ├── CacheMessage.java │ ├── CacheMessageListener.java │ ├── CacheNames.java │ ├── RedisCaffeineCache.java │ └── RedisCaffeineCacheManager.java └── resources ├── META-INF └── spring.factories └── demo.yml /README.md: -------------------------------------------------------------------------------- 1 | # 分布式二级缓存 2 | 3 | 在生产中已有实践,本组件仅做个人学习交流分享使用。 4 | 5 | ## 所谓二级缓存 6 | > 缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。 7 | 8 | 平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。 9 | 10 | 但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了进程内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存。 11 | 12 | ## 系统是否需要缓存 13 | 14 | - **CPU占用**:如果你有某些应用需要消耗大量的cpu去计算获得结果。 15 | - **数据库IO占用**:如果你发现你的数据库连接池比较空闲,那么不应该用缓存。但是如果数据库连接池比较繁忙,甚至经常报出连接不够的报警,那么是时候应该考虑缓存了。 16 | 17 | ## 分布式二级缓存的优势 18 | 19 | Redis用来存储热点数据,Redis中没有的数据则直接去数据库访问。 20 | 21 | 已经有Redis了,干嘛还需要了解Guava,Caffeine这些进程缓存呢: 22 | 23 | - Redis如果不可用,这个时候我们只能访问数据库,很容易造成雪崩,但一般不会出现这种情况。 24 | - 访问Redis会有一定的网络I/O以及序列化反序列化开销,虽然性能很高但是其终究没有本地方法快,可以将最热的数据存放在本地,以便进一步加快访问速度。这个思路并不是我们做互联网架构独有的,在计算机系统中使用L1,L2,L3多级缓存,用来减少对内存的直接访问,从而加快访问速度。 25 | 26 | 所以如果仅仅是使用Redis,能满足我们大部分需求,但是当需要追求更高的性能以及更高的可用性的时候,那就不得不了解多级缓存。 27 | 28 | ## 二级缓存操作过程 29 | 30 | 数据读流程 | 描述 31 | ---|--- 32 | ![image](http://axin-soochow.oss-cn-hangzhou.aliyuncs.com/21-10/lock.png)| redis 与本地缓存都查询不到值的时候,会触发更新过程,整个过程是加锁的 33 | 34 | 35 | 缓存失效流程 | 描述 36 | ---|--- 37 | ![image](http://axin-soochow.oss-cn-hangzhou.aliyuncs.com/21-10/shixiao.png)| redis更新与删除缓存key都会触发,清除redis缓存后 38 | 39 | 40 | ## 如何使用组件? 41 | 42 | 组件是基于Spring Cache框架上改造的,在项目中使用分布式缓存,仅仅需要在缓存注解上增加:cacheManager ="L2_CacheManager",或者 cacheManager = CacheRedisCaffeineAutoConfiguration.分布式二级缓存 43 | 44 | 45 | ```Java 46 | //这个方法会使用分布式二级缓存来提供查询 47 | @Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager") 48 | public Config getAllValidateConfig() { 49 | } 50 | ``` 51 | 52 | 如果你想既使用分布式缓存,又想用分布式二级缓存组件,那你需要向Spring注入一个 @Primary 的 CacheManager bean 然后: 53 | 54 | ```Java 55 | //这个方法会使用分布式二级缓存 56 | @Cacheable(cacheNames = CacheNames.CACHE_12HOUR, cacheManager = "L2_CacheManager") 57 | public Config getAllValidateConfig() { 58 | } 59 | 60 | //这个方法会使用分布式缓存 61 | @Cacheable(cacheNames = CacheNames.CACHE_12HOUR) 62 | public Config getAllValidateConfig2() { 63 | } 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.9.RELEASE 9 | 10 | 11 | 12 | com.axin.idea 13 | redis-caffeine-cache-starter 14 | 1.0.0-SNAPSHOT 15 | 16 | redis-caffeine-cache-starter 17 | 分布式二级缓存装载组件 18 | 19 | 20 | 21 | 2.8.0 22 | 1.2.70 23 | 1.16.20 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-redis 31 | provided 32 | 33 | 34 | 35 | org.projectlombok 36 | lombok 37 | ${lombok.version} 38 | 39 | 40 | 41 | com.github.ben-manes.caffeine 42 | caffeine 43 | ${caffeine.version} 44 | 45 | 46 | 47 | com.alibaba 48 | fastjson 49 | ${fastjson.version} 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/java/com/axin/idea/rediscaffeinecachestarter/CacheRedisCaffeineAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.axin.idea.rediscaffeinecachestarter; 2 | 3 | import com.axin.idea.rediscaffeinecachestarter.support.CacheMessageListener; 4 | import com.axin.idea.rediscaffeinecachestarter.support.RedisCaffeineCacheManager; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 9 | import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.data.redis.core.RedisTemplate; 14 | import org.springframework.data.redis.listener.ChannelTopic; 15 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; 16 | 17 | 18 | @Configuration 19 | @AutoConfigureAfter(RedisAutoConfiguration.class) 20 | @EnableConfigurationProperties(CacheRedisCaffeineProperties.class) 21 | @Slf4j 22 | public class CacheRedisCaffeineAutoConfiguration { 23 | 24 | @Autowired 25 | private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; 26 | 27 | @Bean("L2_CacheManager") 28 | @ConditionalOnBean(RedisTemplate.class) 29 | public RedisCaffeineCacheManager cacheManager(RedisTemplate redisTemplate) { 30 | log.info("==========================================="); 31 | log.info("= ="); 32 | log.info("= Two Level Cache Start ="); 33 | log.info("= ="); 34 | log.info("==========================================="); 35 | return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate); 36 | } 37 | 38 | @Bean 39 | @ConditionalOnBean(RedisTemplate.class) 40 | public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate stringRedisTemplate, 41 | RedisCaffeineCacheManager redisCaffeineCacheManager) { 42 | RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); 43 | redisMessageListenerContainer.setConnectionFactory(stringRedisTemplate.getConnectionFactory()); 44 | CacheMessageListener cacheMessageListener = new CacheMessageListener(stringRedisTemplate, redisCaffeineCacheManager); 45 | redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic())); 46 | return redisMessageListenerContainer; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/java/com/axin/idea/rediscaffeinecachestarter/CacheRedisCaffeineProperties.java: -------------------------------------------------------------------------------- 1 | package com.axin.idea.rediscaffeinecachestarter; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import java.util.HashMap; 7 | import java.util.HashSet; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | @ConfigurationProperties(prefix = "cache.multi") 12 | @Data 13 | public class CacheRedisCaffeineProperties { 14 | 15 | /** 是否存储空值,默认true,防止缓存穿透*/ 16 | private boolean cacheNullValues = true; 17 | 18 | /** 是否动态根据cacheName创建Cache的实现,默认true*/ 19 | private boolean dynamic = true; 20 | 21 | /** 缓存key的前缀*/ 22 | private String cachePrefix; 23 | 24 | private Redis redis = new Redis(); 25 | 26 | private CacheDefault cacheDefault = new CacheDefault(); 27 | private Cache15m cache15m = new Cache15m(); 28 | private Cache30m cache30m = new Cache30m(); 29 | private Cache60m cache60m = new Cache60m(); 30 | private Cache180m cache180m = new Cache180m(); 31 | private Cache12h cache12h = new Cache12h(); 32 | 33 | @Data 34 | public class Redis { 35 | 36 | /** 全局过期时间,单位秒,默认不过期*/ 37 | private long defaultExpiration = 0; 38 | 39 | /** 每个cacheName的过期时间,单位秒,优先级比defaultExpiration高*/ 40 | private Map expires = new HashMap<>(); 41 | 42 | /** 缓存更新时通知其他节点的topic名称*/ 43 | private String topic = "cache:redis:caffeine:topic"; 44 | 45 | } 46 | 47 | @Data 48 | public class CacheDefault { 49 | /** 访问后过期时间,单位秒*/ 50 | protected long expireAfterAccess; 51 | /** 写入后过期时间,单位秒*/ 52 | protected long expireAfterWrite = 120; 53 | /** 写入后刷新时间,单位秒*/ 54 | protected long refreshAfterWrite; 55 | /** 初始化大小,默认50*/ 56 | protected int initialCapacity = 50; 57 | /** 最大缓存对象个数*/ 58 | protected long maximumSize = 50; 59 | 60 | /** 由于权重需要缓存对象来提供,对于使用spring cache这种场景不是很适合,所以暂不支持配置*/ 61 | // private long maximumWeight; 62 | } 63 | public class Cache15m extends CacheDefault{} 64 | public class Cache30m extends CacheDefault{} 65 | public class Cache60m extends CacheDefault{} 66 | public class Cache180m extends CacheDefault{} 67 | public class Cache12h extends CacheDefault{} 68 | } 69 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/java/com/axin/idea/rediscaffeinecachestarter/support/CacheMessage.java: -------------------------------------------------------------------------------- 1 | package com.axin.idea.rediscaffeinecachestarter.support; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.io.Serializable; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | public class CacheMessage implements Serializable { 11 | 12 | private static final long serialVersionUID = -1L; 13 | 14 | private String cacheName; 15 | 16 | private Object key; 17 | 18 | public CacheMessage(String cacheName, Object key) { 19 | super(); 20 | this.cacheName = cacheName; 21 | this.key = key; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/java/com/axin/idea/rediscaffeinecachestarter/support/CacheMessageListener.java: -------------------------------------------------------------------------------- 1 | package com.axin.idea.rediscaffeinecachestarter.support; 2 | 3 | import com.alibaba.fastjson.parser.ParserConfig; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.data.redis.connection.Message; 7 | import org.springframework.data.redis.connection.MessageListener; 8 | import org.springframework.data.redis.core.RedisTemplate; 9 | 10 | /** 11 | * @author axin 12 | * @since 2019-10-31 13 | * @summary 缓存监听器 14 | */ 15 | public class CacheMessageListener implements MessageListener { 16 | 17 | private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class); 18 | 19 | private RedisTemplate redisTemplate; 20 | 21 | private RedisCaffeineCacheManager redisCaffeineCacheManager; 22 | 23 | { 24 | //打开json autotype功能 25 | ParserConfig.getGlobalInstance().addAccept("com.axin.idea.rediscaffeinecachestarter.support."); 26 | } 27 | 28 | public CacheMessageListener(RedisTemplate redisTemplate, 29 | RedisCaffeineCacheManager redisCaffeineCacheManager) { 30 | this.redisTemplate = redisTemplate; 31 | this.redisCaffeineCacheManager = redisCaffeineCacheManager; 32 | } 33 | 34 | /** 35 | * 利用 redis 发布订阅通知其他节点清除本地缓存 36 | * 37 | * @param message 38 | * @param pattern 39 | */ 40 | @Override 41 | public void onMessage(Message message, byte[] pattern) { 42 | CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody()); 43 | logger.debug("收到redis清除缓存消息, 开始清除本地缓存, the cacheName is {}, the key is {}", cacheMessage.getCacheName(), cacheMessage.getKey()); 44 | redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/java/com/axin/idea/rediscaffeinecachestarter/support/CacheNames.java: -------------------------------------------------------------------------------- 1 | package com.axin.idea.rediscaffeinecachestarter.support; 2 | 3 | public interface CacheNames { 4 | /** 15分钟缓存组 */ 5 | String CACHE_15MINS = "cache:15m"; 6 | /** 30分钟缓存组 */ 7 | String CACHE_30MINS = "cache:30m"; 8 | /** 60分钟缓存组 */ 9 | String CACHE_60MINS = "cache:60m"; 10 | /** 180分钟缓存组 */ 11 | String CACHE_180MINS = "cache:180m"; 12 | /** 12Hours分钟缓存组 */ 13 | String CACHE_12HOUR = "cache:12h"; 14 | } -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/java/com/axin/idea/rediscaffeinecachestarter/support/RedisCaffeineCache.java: -------------------------------------------------------------------------------- 1 | package com.axin.idea.rediscaffeinecachestarter.support; 2 | 3 | import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties; 4 | import com.github.benmanes.caffeine.cache.Cache; 5 | import lombok.Getter; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.cache.support.AbstractValueAdaptingCache; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.util.StringUtils; 11 | 12 | import java.time.Duration; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.Set; 16 | import java.util.concurrent.Callable; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.TimeUnit; 19 | import java.util.concurrent.locks.ReentrantLock; 20 | 21 | public class RedisCaffeineCache extends AbstractValueAdaptingCache { 22 | 23 | private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class); 24 | 25 | private String name; 26 | 27 | private RedisTemplate redisTemplate; 28 | 29 | @Getter 30 | private Cache caffeineCache; 31 | 32 | private String cachePrefix; 33 | 34 | /** 35 | * 默认key超时时间 3600s 36 | */ 37 | private long defaultExpiration = 3600; 38 | 39 | private Map defaultExpires = new HashMap<>(); 40 | { 41 | defaultExpires.put(CacheNames.CACHE_15MINS, TimeUnit.MINUTES.toSeconds(15)); 42 | defaultExpires.put(CacheNames.CACHE_30MINS, TimeUnit.MINUTES.toSeconds(30)); 43 | defaultExpires.put(CacheNames.CACHE_60MINS, TimeUnit.MINUTES.toSeconds(60)); 44 | defaultExpires.put(CacheNames.CACHE_180MINS, TimeUnit.MINUTES.toSeconds(180)); 45 | defaultExpires.put(CacheNames.CACHE_12HOUR, TimeUnit.HOURS.toSeconds(12)); 46 | } 47 | 48 | private String topic; 49 | private Map keyLockMap = new ConcurrentHashMap(); 50 | 51 | protected RedisCaffeineCache(boolean allowNullValues) { 52 | super(allowNullValues); 53 | } 54 | 55 | public RedisCaffeineCache(String name, RedisTemplate redisTemplate, 56 | Cache caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) { 57 | super(cacheRedisCaffeineProperties.isCacheNullValues()); 58 | this.name = name; 59 | this.redisTemplate = redisTemplate; 60 | this.caffeineCache = caffeineCache; 61 | this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix(); 62 | this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration(); 63 | this.topic = cacheRedisCaffeineProperties.getRedis().getTopic(); 64 | defaultExpires.putAll(cacheRedisCaffeineProperties.getRedis().getExpires()); 65 | } 66 | 67 | @Override 68 | public String getName() { 69 | return this.name; 70 | } 71 | 72 | @Override 73 | public Object getNativeCache() { 74 | return this; 75 | } 76 | 77 | @Override 78 | public T get(Object key, Callable valueLoader) { 79 | Object value = lookup(key); 80 | if (value != null) { 81 | return (T) value; 82 | } 83 | //key在redis和缓存中均不存在 84 | ReentrantLock lock = keyLockMap.get(key.toString()); 85 | 86 | if (lock == null) { 87 | logger.debug("create lock for key : {}", key); 88 | keyLockMap.putIfAbsent(key.toString(), new ReentrantLock()); 89 | lock = keyLockMap.get(key.toString()); 90 | } 91 | try { 92 | lock.lock(); 93 | value = lookup(key); 94 | if (value != null) { 95 | return (T) value; 96 | } 97 | //执行原方法获得value 98 | value = valueLoader.call(); 99 | Object storeValue = toStoreValue(value); 100 | put(key, storeValue); 101 | return (T) value; 102 | } catch (Exception e) { 103 | throw new ValueRetrievalException(key, valueLoader, e.getCause()); 104 | } finally { 105 | lock.unlock(); 106 | } 107 | } 108 | 109 | @Override 110 | public void put(Object key, Object value) { 111 | if (!super.isAllowNullValues() && value == null) { 112 | this.evict(key); 113 | return; 114 | } 115 | long expire = getExpire(); 116 | logger.debug("put:{},expire:{}", getKey(key), expire); 117 | redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.SECONDS); 118 | 119 | //缓存变更时通知其他节点清理本地缓存 120 | push(new CacheMessage(this.name, key)); 121 | //此处put没有意义,会收到自己发送的缓存key失效消息 122 | // caffeineCache.put(key, value); 123 | } 124 | 125 | @Override 126 | public ValueWrapper putIfAbsent(Object key, Object value) { 127 | Object cacheKey = getKey(key); 128 | // 使用setIfAbsent原子性操作 129 | long expire = getExpire(); 130 | boolean setSuccess; 131 | setSuccess = redisTemplate.opsForValue().setIfAbsent(getKey(key), toStoreValue(value), Duration.ofSeconds(expire)); 132 | 133 | Object hasValue; 134 | //setNx结果 135 | if (setSuccess) { 136 | push(new CacheMessage(this.name, key)); 137 | hasValue = value; 138 | }else { 139 | hasValue = redisTemplate.opsForValue().get(cacheKey); 140 | } 141 | 142 | caffeineCache.put(key, toStoreValue(value)); 143 | return toValueWrapper(hasValue); 144 | } 145 | 146 | @Override 147 | public void evict(Object key) { 148 | // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中 149 | redisTemplate.delete(getKey(key)); 150 | 151 | push(new CacheMessage(this.name, key)); 152 | 153 | caffeineCache.invalidate(key); 154 | } 155 | 156 | @Override 157 | public void clear() { 158 | // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中 159 | Set keys = redisTemplate.keys(this.name.concat(":*")); 160 | for (Object key : keys) { 161 | redisTemplate.delete(key); 162 | } 163 | 164 | push(new CacheMessage(this.name, null)); 165 | caffeineCache.invalidateAll(); 166 | } 167 | 168 | /** 169 | * 取值逻辑 170 | * @param key 171 | * @return 172 | */ 173 | @Override 174 | protected Object lookup(Object key) { 175 | Object cacheKey = getKey(key); 176 | Object value = caffeineCache.getIfPresent(key); 177 | if (value != null) { 178 | logger.debug("从本地缓存中获得key, the key is : {}", cacheKey); 179 | return value; 180 | } 181 | 182 | value = redisTemplate.opsForValue().get(cacheKey); 183 | 184 | if (value != null) { 185 | logger.debug("从redis中获得值,将值放到本地缓存中, the key is : {}", cacheKey); 186 | caffeineCache.put(key, value); 187 | } 188 | return value; 189 | } 190 | 191 | /** 192 | * @description 清理本地缓存 193 | */ 194 | public void clearLocal(Object key) { 195 | logger.debug("clear local cache, the key is : {}", key); 196 | if (key == null) { 197 | caffeineCache.invalidateAll(); 198 | } else { 199 | caffeineCache.invalidate(key); 200 | } 201 | } 202 | 203 | //————————————————————————————私有方法—————————————————————————— 204 | 205 | private Object getKey(Object key) { 206 | String keyStr = this.name.concat(":").concat(key.toString()); 207 | return StringUtils.isEmpty(this.cachePrefix) ? keyStr : this.cachePrefix.concat(":").concat(keyStr); 208 | } 209 | 210 | private long getExpire() { 211 | long expire = defaultExpiration; 212 | Long cacheNameExpire = defaultExpires.get(this.name); 213 | return cacheNameExpire == null ? expire : cacheNameExpire.longValue(); 214 | } 215 | 216 | /** 217 | * @description 缓存变更时通知其他节点清理本地缓存 218 | */ 219 | private void push(CacheMessage message) { 220 | redisTemplate.convertAndSend(topic, message); 221 | } 222 | 223 | } 224 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/java/com/axin/idea/rediscaffeinecachestarter/support/RedisCaffeineCacheManager.java: -------------------------------------------------------------------------------- 1 | package com.axin.idea.rediscaffeinecachestarter.support; 2 | 3 | import com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineProperties; 4 | import com.github.benmanes.caffeine.cache.Caffeine; 5 | import com.github.benmanes.caffeine.cache.stats.CacheStats; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.cache.Cache; 10 | import org.springframework.cache.CacheManager; 11 | import org.springframework.data.redis.core.RedisTemplate; 12 | import org.springframework.util.CollectionUtils; 13 | 14 | import java.util.*; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.concurrent.ConcurrentMap; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | @Slf4j 20 | public class RedisCaffeineCacheManager implements CacheManager { 21 | 22 | private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class); 23 | 24 | private static ConcurrentMap cacheMap = new ConcurrentHashMap(); 25 | 26 | private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; 27 | 28 | private RedisTemplate stringKeyRedisTemplate; 29 | 30 | private boolean dynamic = true; 31 | 32 | private Set cacheNames; 33 | { 34 | cacheNames = new HashSet<>(); 35 | cacheNames.add(CacheNames.CACHE_15MINS); 36 | cacheNames.add(CacheNames.CACHE_30MINS); 37 | cacheNames.add(CacheNames.CACHE_60MINS); 38 | cacheNames.add(CacheNames.CACHE_180MINS); 39 | cacheNames.add(CacheNames.CACHE_12HOUR); 40 | } 41 | public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties, 42 | RedisTemplate stringKeyRedisTemplate) { 43 | super(); 44 | this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties; 45 | this.stringKeyRedisTemplate = stringKeyRedisTemplate; 46 | this.dynamic = cacheRedisCaffeineProperties.isDynamic(); 47 | } 48 | 49 | //——————————————————————— 进行缓存工具 —————————————————————— 50 | /** 51 | * 清除所有进程缓存 52 | */ 53 | public void clearAllCache() { 54 | stringKeyRedisTemplate.convertAndSend(cacheRedisCaffeineProperties.getRedis().getTopic(), new CacheMessage(null, null)); 55 | } 56 | 57 | /** 58 | * 返回所有进程缓存(二级缓存)的统计信息 59 | * result:{"缓存名称":统计信息} 60 | * @return 61 | */ 62 | public static Map getCacheStats() { 63 | if (CollectionUtils.isEmpty(cacheMap)) { 64 | return null; 65 | } 66 | 67 | Map result = new LinkedHashMap<>(); 68 | for (Cache cache : cacheMap.values()) { 69 | RedisCaffeineCache caffeineCache = (RedisCaffeineCache) cache; 70 | result.put(caffeineCache.getName(), caffeineCache.getCaffeineCache().stats()); 71 | } 72 | return result; 73 | } 74 | 75 | //—————————————————————————— core ————————————————————————— 76 | @Override 77 | public Cache getCache(String name) { 78 | Cache cache = cacheMap.get(name); 79 | if(cache != null) { 80 | return cache; 81 | } 82 | if(!dynamic && !cacheNames.contains(name)) { 83 | return null; 84 | } 85 | 86 | cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(name), cacheRedisCaffeineProperties); 87 | Cache oldCache = cacheMap.putIfAbsent(name, cache); 88 | logger.debug("create cache instance, the cache name is : {}", name); 89 | return oldCache == null ? cache : oldCache; 90 | } 91 | 92 | @Override 93 | public Collection getCacheNames() { 94 | return this.cacheNames; 95 | } 96 | 97 | public void clearLocal(String cacheName, Object key) { 98 | //cacheName为null 清除所有进程缓存 99 | if (cacheName == null) { 100 | log.info("清除所有本地缓存"); 101 | cacheMap = new ConcurrentHashMap<>(); 102 | return; 103 | } 104 | 105 | Cache cache = cacheMap.get(cacheName); 106 | if(cache == null) { 107 | return; 108 | } 109 | 110 | RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache; 111 | redisCaffeineCache.clearLocal(key); 112 | } 113 | 114 | /** 115 | * 实例化本地一级缓存 116 | * @param name 117 | * @return 118 | */ 119 | private com.github.benmanes.caffeine.cache.Cache caffeineCache(String name) { 120 | Caffeine cacheBuilder = Caffeine.newBuilder(); 121 | CacheRedisCaffeineProperties.CacheDefault cacheConfig; 122 | switch (name) { 123 | case CacheNames.CACHE_15MINS: 124 | cacheConfig = cacheRedisCaffeineProperties.getCache15m(); 125 | break; 126 | case CacheNames.CACHE_30MINS: 127 | cacheConfig = cacheRedisCaffeineProperties.getCache30m(); 128 | break; 129 | case CacheNames.CACHE_60MINS: 130 | cacheConfig = cacheRedisCaffeineProperties.getCache60m(); 131 | break; 132 | case CacheNames.CACHE_180MINS: 133 | cacheConfig = cacheRedisCaffeineProperties.getCache180m(); 134 | break; 135 | case CacheNames.CACHE_12HOUR: 136 | cacheConfig = cacheRedisCaffeineProperties.getCache12h(); 137 | break; 138 | default: 139 | cacheConfig = cacheRedisCaffeineProperties.getCacheDefault(); 140 | } 141 | long expireAfterAccess = cacheConfig.getExpireAfterAccess(); 142 | long expireAfterWrite = cacheConfig.getExpireAfterWrite(); 143 | int initialCapacity = cacheConfig.getInitialCapacity(); 144 | long maximumSize = cacheConfig.getMaximumSize(); 145 | long refreshAfterWrite = cacheConfig.getRefreshAfterWrite(); 146 | 147 | log.debug("本地缓存初始化:"); 148 | if (expireAfterAccess > 0) { 149 | log.debug("设置本地缓存访问后过期时间,{}秒", expireAfterAccess); 150 | cacheBuilder.expireAfterAccess(expireAfterAccess, TimeUnit.SECONDS); 151 | } 152 | if (expireAfterWrite > 0) { 153 | log.debug("设置本地缓存写入后过期时间,{}秒", expireAfterWrite); 154 | cacheBuilder.expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS); 155 | } 156 | if (initialCapacity > 0) { 157 | log.debug("设置缓存初始化大小{}", initialCapacity); 158 | cacheBuilder.initialCapacity(initialCapacity); 159 | } 160 | if (maximumSize > 0) { 161 | log.debug("设置本地缓存最大值{}", maximumSize); 162 | cacheBuilder.maximumSize(maximumSize); 163 | } 164 | if (refreshAfterWrite > 0) { 165 | cacheBuilder.refreshAfterWrite(refreshAfterWrite, TimeUnit.SECONDS); 166 | } 167 | cacheBuilder.recordStats(); 168 | return cacheBuilder.build(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.axin.idea.rediscaffeinecachestarter.CacheRedisCaffeineAutoConfiguration -------------------------------------------------------------------------------- /redis-caffeine-cache-starter/src/main/resources/demo.yml: -------------------------------------------------------------------------------- 1 | #配置模板: 2 | #更多可自定义配置参考 CacheRedisCaffeineProperties.Java 3 | cache.multi: 4 | cachePrefix: axin #缓存key前缀 5 | dynamic: true #是否动态根据cacheName创建Cache的实现,默认true 6 | redis: 7 | topic: axin:cache:redis:caffeine:topic 8 | defaultExpiration: 600 #二级缓存默认redis过期时间,单位秒,默认3600s 9 | # 针对自定义cacheName的本地一级缓存配置 10 | cacheDefault: 11 | expireAfterAccess: 5 #访问后过期时间,单位秒 12 | expireAfterWrite: 60 #写入后过期时间,单位秒 13 | initialCapacity: 50 #初始化大小 14 | maximumSize: 50 #最大缓存对象个数,超过此数量时会使用Window TinyLfu策略来淘汰缓存 15 | 16 | # 默认写入后过期时间,单位秒 expireAfterWrite = 120; 17 | # 默认初始化大小 initialCapacity = 50; 18 | # 默认最大缓存对象个数 maximumSize = 50; 19 | cache15m: 20 | expireAfterAccess: 10 21 | expireAfterWrite: 300 22 | initialCapacity: 100 23 | maximumSize: 100 24 | cache30m: 25 | expireAfterAccess: 5 26 | expireAfterWrite: 300 27 | initialCapacity: 100 28 | maximumSize: 100 29 | cache60m: 30 | expireAfterAccess: 5 31 | initialCapacity: 100 32 | expireAfterWrite: 300 33 | maximumSize: 100 34 | cache180m: 35 | expireAfterAccess: 5 36 | initialCapacity: 100 37 | expireAfterWrite: 300 38 | maximumSize: 100 39 | cache12h: 40 | expireAfterAccess: 5 41 | expireAfterWrite: 300 42 | initialCapacity: 100 43 | maximumSize: 100 --------------------------------------------------------------------------------