Pear Spring Boot Starter 缓存模块功能详解
Pear 是一个基于 Spring Boot 构建的单体应用快速开发框架,其中的缓存模块设计灵活、易扩展,支持多种缓存策略,并提供了丰富的配置选项。
一、缓存策略设计
Pear 缓存模块采用了面向接口编程的设计理念,定义了 CacheTemplate<K,V> 接口作为所有缓存实现的基础:
java
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();
}
目前支持三种具体的缓存策略实现:
java
public enum CacheStrategyEnum {
LRU(0,"LRU"),
LFU(1,"LFU"),
REDIS(2,"REDIS");
}
-
LRU (Least Recently Used) - 最近最少使用策略
LRUCacheTemplate 基于 LinkedHashMap 实现,当缓存达到容量上限时,会自动移除最近最少使用的条目,并采用读写锁机制:
-
读操作(如 get, containsKey, size)使用读锁,允许多个线程同时读取;
-
写操作(如 put, remove, clear)使用写锁,保证同一时刻只有一个线程能修改数据;
保证并发安全,遵循锁升级以及读锁进读锁出的设计原则:
java/** * 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){ @Override protected 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); } @Override public void put(K key,V value){ writeLock.lock(); ExpiredCacheValue<V> expiredCacheValue=new ExpiredCacheValue<>(System.currentTimeMillis(),value); try{ cache.put(key,expiredCacheValue); }finally { writeLock.unlock(); } } @Override public 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(); } } @Override public void clear(){...} @Override public void remove(K key){...} @Override public long size(){...} @Override public String toString() {...} @Override public Class<K> getKeyClass() {...} @Override public Class<V> getValueClass() {...} @Override public boolean containsKey(K key) {...} } -
-
LFU (Least Frequently Used) - 最少频繁使用策略
LFUCacheTemplate 使用频率计数来决定淘汰策略,访问次数越少的条目越容易被淘汰:
java/** * 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); } @Override public 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; } } @Override public 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(); } } @Override public long size() {...} @Override public void clear() {...} @Override public boolean containsKey(K key) {...} @Override public String toString() {...} @Override public Class<K> getKeyClass() {...} @Override public Class<V> getValueClass() {...} } -
Redis - 基于 Redis 的分布式缓存策略
RedisCacheTemplate没什么好看的,主要还是直接调用了RedisTemplate的方法,同样读写锁保证并发安全,只有对size大小的获取和clear清空用法高级一点,采用键匹配与Scan扫描,这种方法在键值数量小的时候比较消耗CPU性能的:
java
@Override
public 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();
}
@Override
public void clear() {
// 收集所有要删除的 key
Set<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进行操作:
java
/**
* 缓存工厂类,根据不同缓存配置创建缓存并存储在缓存容器中
**/
@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作为核心管理类,负责缓存实例的创建、存储和生命周期管理:
java
@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 文件进行配置:
properties
pear.starter.cache.expire=3600
pear.starter.cache.capacity=1000
pear.starter.cache.strategy=LRU
pear.starter.cache.time-unit=s
对应的配置属性类:
java
@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 来覆盖默认配置:
java
@Bean
public CacheConfig customCacheConfig() {
return new CacheConfig.Builder()
.expire(7200)
.capacity(2000)
.strategy(CacheStrategyEnum.LFU)
.build();
}
自动配置类会检测用户是否已经提供了自定义配置:
java
@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 缓存模块通过泛型机制确保类型安全,在创建缓存时指定键值类型:
java
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 方法:
java
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 抽象层:
java
@Component
public class DefaultRedisSupport implements RedisSupport{
private final RedisTemplate<String, Object> redisTemplate;
public DefaultRedisSupport(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
}
当用户选择 Redis 策略但未引入 Redis 依赖时,会抛出友好的异常提示而非启动失败。
总结
Pear 缓存模块通过工厂模式、泛型机制和策略模式的巧妙结合,实现了高度可扩展和类型安全的缓存解决方案。无论是对内服务Pear框架还是对外为开发人员服务,都能通过统一的 API 进行操作,大大提升了开发效率和系统的可维护性。
后言
欢迎各位在github上star作者开源的个人项目单体应用快速开发框架Pear(若点击无法跳转请手动输入网址https://github.com/MuziSuper/pear-spring-boot-starter),对于此模块设计中出现的bug以及性能漏洞也会在之后不断修复。