当前位置: 首页 > news >正文

SpringBoot基于工厂模式的多类型缓存设计

Pear Spring Boot Starter 缓存模块功能详解

Pear 是一个基于 Spring Boot 构建的单体应用快速开发框架,其中的缓存模块设计灵活、易扩展,支持多种缓存策略,并提供了丰富的配置选项。

一、缓存策略设计

Pear 缓存模块采用了面向接口编程的设计理念,定义了 CacheTemplate<K,V> 接口作为所有缓存实现的基础:

public interface CacheTemplate<K,V> {V get(K key);void put(K key, V value);void remove(K key);long size();void clear();boolean containsKey(K key);Class<K> getKeyClass();Class<V> getValueClass();
}

目前支持三种具体的缓存策略实现:

public enum CacheStrategyEnum {LRU(0,"LRU"),LFU(1,"LFU"),REDIS(2,"REDIS");
}
  1. LRU (Least Recently Used) - 最近最少使用策略

    LRUCacheTemplate 基于 LinkedHashMap 实现,当缓存达到容量上限时,会自动移除最近最少使用的条目,并采用读写锁机制:

    • 读操作(如 get, containsKey, size)使用读锁,允许多个线程同时读取;

    • 写操作(如 put, remove, clear)使用写锁,保证同一时刻只有一个线程能修改数据;

      保证并发安全,遵循锁升级以及读锁进读锁出的设计原则:

    /*** LRU缓存, 基于LinkedHashMap实现, 使用读写锁保证线程安全,超过最大容量后,最近最久未使用的数据会被清除**/
    public class LRUCacheTemplate<K,V> implements CacheTemplate<K,V> {private final LinkedHashMap<K, ExpiredCacheValue<V>> cache;private final ReadWriteLock readWriteLock=new ReentrantReadWriteLock();private final Lock readLock=readWriteLock.readLock();private final Lock writeLock=readWriteLock.writeLock();private final long expire;private final TimeUnit timeUnit;private final Class<K> keyClass;private final Class<V> valueClass;public LRUCacheTemplate(int capacity, long expire, TimeUnit timeUnit,Class<K> keyClass, Class<V> valueClass) {this.expire = expire;this.timeUnit = timeUnit;this.keyClass = keyClass;this.valueClass = valueClass;if(capacity==0) cache=new LinkedHashMap<>();else cache = new LinkedHashMap<>(capacity,0.75f,true){@Overrideprotected boolean removeEldestEntry(java.util.Map.Entry<K,ExpiredCacheValue<V>> eldest) {return size()>capacity;}};}public LRUCacheTemplate(CacheConfig config, Class<K> keyClass, Class<V> valueClass) {this(config.getCapacity(),config.getExpire(),config.getTimeUnit(),keyClass,valueClass);}@Overridepublic void put(K key,V value){writeLock.lock();ExpiredCacheValue<V> expiredCacheValue=new ExpiredCacheValue<>(System.currentTimeMillis(),value);try{cache.put(key,expiredCacheValue);}finally {writeLock.unlock();}}@Overridepublic V get(K key){readLock.lock();try{if(!cache.containsKey(key)){return null;}ExpiredCacheValue<V> expiredCacheValue= cache.get(key);if(expiredCacheValue==null) return null;if(System.currentTimeMillis()-expiredCacheValue.getLastTime()<expire * timeUnit.toMillis(1)||expire==0L){expiredCacheValue.setLastTime(System.currentTimeMillis());return expiredCacheValue.getVal();}else {remove(key);return null;}}finally {readLock.unlock();}}@Overridepublic void clear(){...}@Overridepublic void remove(K key){...}@Overridepublic long size(){...}@Overridepublic String toString() {...}@Overridepublic Class<K> getKeyClass() {...}@Overridepublic Class<V> getValueClass() {...}@Overridepublic boolean containsKey(K key) {...}}
    
  2. LFU (Least Frequently Used) - 最少频繁使用策略

    LFUCacheTemplate 使用频率计数来决定淘汰策略,访问次数越少的条目越容易被淘汰:

    /*** LFU缓存,访问次数越少,越早被删除,使用三个数据结构以及读写锁实现**/
    public class LFUCacheTemplate<K, V> implements CacheTemplate<K, V> {private final int capacity;private final Map<K, ExpiredCacheValue<V>> cache; // 存储键值对private final Map<K, Integer> frequencyMap; // 存储键的访问频率private final TreeMap<Integer, LinkedHashSet<K>> frequencyKeysMap; // 频率到键集合的映射private final ReadWriteLock lock = new ReentrantReadWriteLock();private final Lock readLock = lock.readLock();private final Lock writeLock = lock.writeLock();private final long expire;private final TimeUnit timeUnit;private final Class<K> keyClass;private final Class<V> valueClass;public LFUCacheTemplate(int capacity, long expire, TimeUnit timeUnit, Class<K> keyClass, Class<V> valueClass) {this.capacity = capacity;this.expire = expire;this.timeUnit = timeUnit;this.cache = new HashMap<>();this.frequencyMap = new HashMap<>();this.frequencyKeysMap = new TreeMap<>();this.keyClass = keyClass;this.valueClass = valueClass;}public LFUCacheTemplate(CacheConfig config, Class<K> keyClass, Class<V> valueClass) {this(config.getCapacity(), config.getExpire(), config.getTimeUnit(), keyClass, valueClass);}@Overridepublic V get(K key) {ExpiredCacheValue<V> value;try {readLock.lock();if (!cache.containsKey(key)) {return null;}value = cache.get(key);} finally {readLock.unlock();}if (System.currentTimeMillis() - value.getLastTime() < expire * timeUnit.toMillis(1) || expire == 0L) {try {try {writeLock.lock();incrementFrequency(key);} finally {readLock.lock();writeLock.unlock();}return value.getVal();} finally {readLock.unlock();}} else {remove(key);return null;}}@Overridepublic void put(K key, V value) {ExpiredCacheValue<V> expiredCacheValue;readLock.lock();try {expiredCacheValue = cache.get(key);} finally {readLock.unlock();}// 键已存在则更新值并增加频率之后退出,若键已存在且已过期,则移除该键执行后续逻辑if (expiredCacheValue != null) {if (System.currentTimeMillis() - expiredCacheValue.getLastTime() < expire * timeUnit.toMillis(1) || expire == 0L) {expiredCacheValue.setVal(value);expiredCacheValue.setLastTime(System.currentTimeMillis());writeLock.lock();try {try {cache.put(key, expiredCacheValue);incrementFrequency(key);return;} finally {readLock.lock();writeLock.unlock();}} finally {readLock.unlock();}}remove(key);}// 键不存在,则创建新的键值对ExpiredCacheValue<V> newExpiredCacheValue = new ExpiredCacheValue<>(System.currentTimeMillis(), value);newExpiredCacheValue.setVal(value);newExpiredCacheValue.setLastTime(System.currentTimeMillis());writeLock.lock();try {try {while (cache.size() > capacity && capacity != 0) {removeLeastFrequent();}cache.put(key, newExpiredCacheValue);incrementFrequency(key);} finally {readLock.lock();writeLock.unlock();}} finally {readLock.unlock();}}/*** 更新键的访问次数与频率到元素映射的集合**/private void incrementFrequency(K key) {int frequency = frequencyMap.getOrDefault(key, 0) + 1;frequencyMap.put(key, frequency);// 从旧频率集合中移除if (frequencyKeysMap.containsKey(frequency)) {frequencyKeysMap.get(frequency).remove(key);if (frequencyKeysMap.get(frequency).isEmpty()) {frequencyKeysMap.remove(frequency);}}// 添加到新频率集合frequencyKeysMap.computeIfAbsent(frequency + 1, k -> new LinkedHashSet<>()).add(key);}private void removeLeastFrequent() {Integer lowestFrequency = frequencyKeysMap.firstKey();LinkedHashSet<K> keysWithLowestFrequency = frequencyKeysMap.get(lowestFrequency);// 获取并移除第一个键(LRU策略处理相同频率的情况)K keyToRemove = keysWithLowestFrequency.iterator().next();keysWithLowestFrequency.remove(keyToRemove);if (keysWithLowestFrequency.isEmpty()) {frequencyKeysMap.remove(lowestFrequency);}cache.remove(keyToRemove);frequencyMap.remove(keyToRemove);}/*** 移除缓存**/public void remove(K key) {readLock.lock();try {if (!cache.containsKey(key)) {return;}readLock.unlock();writeLock.lock();try {int frequency = frequencyMap.get(key);frequencyMap.remove(key);frequencyKeysMap.get(frequency).remove(key);if (frequencyKeysMap.get(frequency).isEmpty()) {frequencyKeysMap.remove(frequency);}cache.remove(key);} finally {readLock.lock();writeLock.unlock();}} finally {readLock.unlock();}}@Overridepublic long size() {...}@Overridepublic void clear() {...}@Overridepublic boolean containsKey(K key) {...}@Overridepublic String toString() {...}@Overridepublic Class<K> getKeyClass() {...}@Overridepublic Class<V> getValueClass() {...}
    }
  3. Redis - 基于 Redis 的分布式缓存策略

RedisCacheTemplate没什么好看的,主要还是直接调用了RedisTemplate的方法,同样读写锁保证并发安全,只有对size大小的获取和clear清空用法高级一点,采用键匹配与Scan扫描,这种方法在键值数量小的时候比较消耗CPU性能的:

    @Overridepublic long size() {AtomicLong keys = new AtomicLong();redisTemplate.execute((RedisCallback<Object>) connection -> {var keyCommands = connection.keyCommands();var scanOpts = ScanOptions.scanOptions().match(CACHE_PREFIX + "*").count(500).build();try (Cursor<byte[]> cursor = keyCommands.scan(scanOpts)) {while (cursor.hasNext()) {keys.getAndIncrement();cursor.next();}} catch (Exception e) {LOG.warn("Error during Redis SCAN for prefix: " + CACHE_PREFIX, e);}return null;});return keys.get();}@Overridepublic void clear() {// 收集所有要删除的 keySet<String> keys = new HashSet<>();redisTemplate.execute((RedisCallback<Object>) (connection) -> {// 使用新的 keyCommands().scan() API 而不是旧的 connection.scan()var keyCommands = connection.keyCommands();var scanOpts = ScanOptions.scanOptions().match(CACHE_PREFIX + "*").count(500).build();try (Cursor<byte[]> cursor = keyCommands.scan(scanOpts)) {while (cursor.hasNext()) {keys.add(new String(cursor.next()));}} catch (Exception e) {LOG.warn("Error during Redis SCAN for prefix: " + CACHE_PREFIX, e);}return null;});if (keys.isEmpty()) {return;}// 2. 分批调用 del 删除(因为 del(byte[]...) 的参数可能有限制)List<String> keyList = new ArrayList<>(keys);final int BATCH_SIZE = 100;  // 每次删除 100 个 key 为例int total = keyList.size();for (int i = 0; i < total; i += BATCH_SIZE) {int end = Math.min(i + BATCH_SIZE, total);List<String> subList = keyList.subList(i, end);redisTemplate.execute((RedisCallback<Long>) connection -> {var keyCommands = connection.keyCommands();byte[][] bkeys = new byte[subList.size()][];for (int j = 0; j < subList.size(); j++) {bkeys[j] = subList.get(j).getBytes();}return keyCommands.del(bkeys);});}}

这些策略都通过工厂模式统一管理,由CacheFactory类负责创建,该类无修饰符,避免开发人员绕过CacheContainer直接对CacheFacory进行操作:

/*** 缓存工厂类,根据不同缓存配置创建缓存并存储在缓存容器中**/
@Component
class CacheFactory {private final CacheConfig cacheConfig;private final RedisSupport redisSupport;CacheFactory(CacheConfig cacheConfig, @Nullable RedisSupport redisSupport){this.cacheConfig = cacheConfig;this.redisSupport = redisSupport;}/*** 创建缓存* @param expire 缓存过期时间,0表示不过期,单位秒* @param capacity 缓存容量,0表示无容量限制* @param strategy 缓存策略,默认使用LRU缓存策略* @param keyClass 缓存键Class对象* @param valueClass 缓存值Class对象* @param <K> 缓存键类型* @param <V> 缓存值类型* @return CacheTemplate对象* @throws CacheException 选择Redis缓存策略但未配置RedisTemplate对象**/<K,V> CacheTemplate<K,V> createCacheTemplate(long expire, TimeUnit timeUnit, int capacity, CacheStrategyEnum strategy, Class<K> keyClass, Class<V> valueClass){if (capacity < 0) {capacity=cacheConfig.getCapacity();}if (expire < 0) {expire=cacheConfig.getExpire();}if (Constant.CACHE_TYPE_LRU.equals(strategy.getStrategy())) {return new LRUCacheTemplate<>(capacity, expire,timeUnit, keyClass, valueClass);} else if (Constant.CACHE_TYPE_REDIS.equals(strategy.getStrategy())) {if(redisSupport==null||redisSupport.getRedisTemplate()==null){throw new CacheException("redisTemplate is null.");}return new RedisCacheTemplate<>(redisSupport.getRedisTemplate(), keyClass, valueClass);}else {return new LFUCacheTemplate<>(capacity, expire,timeUnit ,keyClass, valueClass);}}
}
二、缓存容器管理

CacheContainer作为核心管理类,负责缓存实例的创建、存储和生命周期管理:

@Component
public class CacheContainer {private final CacheFactory cacheFactory;private final CacheConfig cacheConfig;private final ConcurrentHashMap<String, CacheTemplate<?, ?>> cacheTemplateMap = new ConcurrentHashMap<>();public CacheContainer(CacheFactory cacheFactory, CacheConfig cacheConfig) {this.cacheFactory = cacheFactory;this.cacheConfig = cacheConfig;}/*** 创建缓存放入缓存容器内,缓存名称作为主键存放。K为缓存的键类型,V为缓存的值类型。若对应缓存名称已存在则返回该缓存,但若想要创建的缓存键值对类型与已存在的缓存键值对类型不匹配则抛出异常。** @param cacheName  缓存名称,主键值, 若传入字符串为空则随机生成一个十位随机字符串* @param expire     缓存过期时间, 0表示不过期* @param capacity   缓存容量* @param strategy   缓存策略* @param keyClass   缓存键类型* @param valueClass 缓存值类型* @param <K>         缓存键类型* @param <V>         缓存值类型* @throws CacheException 如果原本存在该缓存名**/public <K, V> boolean createCache(String cacheName, long expire, TimeUnit timeUnit, int capacity, CacheStrategyEnum strategy, Class<K> keyClass, Class<V> valueClass) {if (strategy == null || expire < 0 || capacity < 0) return false;if (StringUtil.isEmpty(cacheName)) cacheName = RandomStrUtil.getRandomStr(10);CacheTemplate<K, V> cacheTemplate = cacheFactory.createCacheTemplate(expire, timeUnit, capacity, strategy, keyClass, valueClass);CacheTemplate<?, ?> existing = cacheTemplateMap.putIfAbsent(cacheName, cacheTemplate);if (existing != null) {if (!existing.getKeyClass().equals(keyClass)||!existing.getValueClass().equals(valueClass))throw new CacheException("The cache already exists, but there is an error in the type matching of the key-value pair.");return true;}cacheTemplateMap.put(cacheName, cacheTemplate);return true;}/*** 获取对应缓存中key映射的值,若缓存不存在则返回null。若你使用放入缓存内的不同值类型来接收返回值,则会抛出无法解决的ClassCastException异常,推荐使用get(String cacheName, K key, Class valueClazz)方法。** @param cacheName 缓存名称* @param key       缓存key* @param <K>         缓存键类型* @param <V>         缓存值类型* @return 缓存值* @throws CacheException 如果缓存键值对类型不匹配**/@SuppressWarnings("unchecked")public <K, V> V get(String cacheName, K key) {...}/*** 获取对应缓存中key映射的值,若缓存不存在则返回null,valueClass会将从缓存容器内取出的值做强制类型转换,若无法转换则抛出CacheException异常。** @param cacheName 缓存名称* @param key       缓存key* @param valueClazz 缓存值类型* @param <K>         缓存键类型* @param <V>         缓存值类型* @return 缓存值* @throws CacheException 如果缓存键值对类型不匹配**/@SuppressWarnings("unchecked")public <K, V> V get(String cacheName, K key, Class<V> valueClazz) {...}/*** 向缓存中添加缓存键值对** @param cacheName 缓存名称* @param key       缓存key* @param value     缓存值* @param <K>         缓存键类型* @param <V>         缓存值类型* @throws CacheException 如果缓存键值对类型不匹配**/@SuppressWarnings("unchecked")public <K, V> void put(String cacheName, K key, V value) {...}/*** 查询缓存中是否存在指定缓存键** @param cacheName 缓存名称* @param key       缓存key* @param <K>         缓存键类型* @throws CacheException 如果缓存键值对类型不匹配**/@SuppressWarnings("unchecked")public <K> boolean containsKey(String cacheName, K key) {...}/*** 查询缓存中是否存在指定缓存键** @param cacheName 缓存名称* @throws CacheException 如果缓存键值对类型不匹配**/public boolean contains(String cacheName) {...}/*** 查询缓存中键值对的数量** @param cacheName 缓存名称* @throws CacheException 如果缓存键值对类型不匹配**/public long size(String cacheName) {...}/*** 删除缓存中指定缓存键的缓存键值对** @param cacheName 缓存名称* @param key       缓存key* @param <K>         缓存键类型* @throws CacheException 如果缓存键值对类型不匹配**/@SuppressWarnings("unchecked")public <K> void remove(String cacheName, K key) {...}/*** 清空缓存** @param cacheName 缓存名称**/public void clear(String cacheName) {...}}

它通过 ConcurrentHashMap 来存储多个命名缓存实例,支持并发访问。

三、灵活的配置机制

Pear 缓存模块支持两种配置方式:

1. Properties 配置方式

通过 application.properties 或 application.yml 文件进行配置:

pear.starter.cache.expire=3600
pear.starter.cache.capacity=1000
pear.starter.cache.strategy=LRU
pear.starter.cache.time-unit=s

对应的配置属性类:

@Data
@ConfigurationProperties("pear.starter.cache")
public class CacheProperties {private long expire;private int capacity;private String strategy;private String timeUnit;public void applyTo(CacheConfig config){// 属性应用逻辑...}
}
2. JavaConfig 配置方式

用户可通过自定义 [CacheConfig](file:///Applications/LocalGit/pear-spring-boot-starter/pear-spring-boot-basic/src/main/java/cn/muzisheng/pear/config/CacheConfig.java#L12-L52) Bean 来覆盖默认配置:

@Bean
public CacheConfig customCacheConfig() {return new CacheConfig.Builder().expire(7200).capacity(2000).strategy(CacheStrategyEnum.LFU).build();
}

自动配置类会检测用户是否已经提供了自定义配置:

@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class CacheAutoConfiguration {@Bean@ConditionalOnMissingBean(CacheConfig.class)public CacheConfig defaultCacheConfig(CacheProperties properties) {CacheConfig config = new CacheConfig.Builder().build();properties.applyTo(config);return config;}
}
四、泛型支持与类型安全

Pear 缓存模块通过泛型机制确保类型安全,在创建缓存时指定键值类型:

cacheContainer.createCache("test",10000, TimeUnit.MILLISECONDS, 1000,CacheStrategyEnum.REDIS, String.class, int[].class);
cacheContainer.put("test", "testKey", new int[]{1,2});
int[] value1 = cacheContainer.get("test", "testKey");

为了进一步增强类型安全性,提供了带类型转换的 get 方法:

public <K, V> V get(String cacheName, K key, Class<V> valueClazz) {// ...try{return valueClazz.cast(value);}catch (ClassCastException e){throw new CacheException("Value type mismatch: expected "+valueClazz.getName()+", but got "+value.getClass().getName());}
}

这样即使在复杂的泛型场景下也能保证类型转换的安全性。

五、Redis 支持与依赖解耦

为了避免强制依赖 Redis,Pear 采用了 RedisSupport 抽象层:

@Component
public class DefaultRedisSupport implements RedisSupport{private final RedisTemplate<String, Object> redisTemplate;public DefaultRedisSupport(RedisTemplate<String, Object> redisTemplate) {this.redisTemplate = redisTemplate;}@Overridepublic RedisTemplate<String, Object> getRedisTemplate() {return redisTemplate;}
}

当用户选择 Redis 策略但未引入 Redis 依赖时,会抛出友好的异常提示而非启动失败。

总结

Pear 缓存模块通过工厂模式、泛型机制和策略模式的巧妙结合,实现了高度可扩展和类型安全的缓存解决方案。无论是对内服务Pear框架还是对外为开发人员服务,都能通过统一的 API 进行操作,大大提升了开发效率和系统的可维护性。

后言

欢迎各位在github上star作者开源的个人项目单体应用快速开发框架Pear(若点击无法跳转请手动输入网址https://github.com/MuziSuper/pear-spring-boot-starter),对于此模块设计中出现的bug以及性能漏洞也会在之后不断修复。

http://www.dtcms.com/a/483066.html

相关文章:

  • Redis中渐进式命令scan详解与使用
  • 江苏省建设厅网站 投诉wordpress页面写文章
  • Redis-主从复制和哨兵模式
  • 推荐一下做年会视频的网站做网站的上市公司
  • 淘宝网站建设论文河北城乡建设学校官方网站
  • 结构一次函数的图像
  • 1032 挖掘机技术哪家强
  • 程序员培训出来到底有没有用徐州seo计费管理
  • git status时发现有未提交的事件提交发现Git 锁文件冲突的问题解决办法
  • 使用 NNCF 量化模型(Python篇)
  • php网站怎么做自适应智慧团建登录入口官方网站
  • 建网站需要什么资质河北智能网站建设
  • 高职示范校建设网站个人网站工商备案
  • 面试-上海电力大学研一的学习经验
  • 理查德西尔斯做的网站做网站发房源综合语录
  • java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver
  • 广州中小企业网站建设应用宝下载
  • 影刀 —— 钉钉表格写入
  • 为网站网站做推广彬县网新闻最新消息
  • 汽车芯片:驱动汽车智能进化的“数字发动机”
  • 创建wordpress网站企业域名是什么意思
  • vue实现批量导出二维码到PDF(支持分页生成 PDF)
  • Collections.synchronizedList()详解
  • 做一家仓储用地的网站陕西十二建设有限公司网站
  • 网站有备案号吗天元建设集团有限公司发展历程
  • 网站建设的税收分类编码淘宝店需要多少资金
  • 做网站创业怎么样wordpress 透明背景
  • win10秘钥登录linux问题
  • 丹东建设网官方网站移动云服务器
  • OkHttp源码解析(二)