缓存(Cache)
缓存模块是以 EhCache 作为默认 JVM 进程内缓存服务,通过整合外部 Redis 服务实现多级缓存(MultiLevel)的轻量级缓存框架,并与 YMP 框架深度集成(支持针对类方法的缓存,可以根据方法参数值进行缓存),灵活的配置、易于使用和扩展。
Maven包依赖
<dependency>
<groupId>net.ymate.platform</groupId>
<artifactId>ymate-platform-cache</artifactId>
<version>2.1.4</version>
</dependency>
若需要启用redis作为缓存服务,需额外添加以下依赖配置:
<dependency>
<groupId>net.ymate.platform</groupId>
<artifactId>ymate-platform-persistence-redis</artifactId>
<version>2.1.4</version>
</dependency>
基础接口概念
开发者可以根据以下接口完成对缓存模块的自定义扩展实现。
-
缓存服务提供者接口:
ICacheProvider- DefaultCacheProvider - 基于EhCache缓存服务的默认缓存服务提供者接口实现类;
- RedisCacheProvider - 基于Redis数据库的缓存服务提供者接口实现类;
- MultievelCacheProvider - 融合EhCache和Redis两者的缓存服务提供者接口实现类,通过MultilevelKey决定缓存对象的获取方式;
-
缓存Key生成器接口:
ICacheKeyGenerator- DefaultCacheKeyGenerator - 根据提供的类方法、方法参数类型与参数对象生成缓存Key,默认是将参数序列化后先做 SHA-256 散列,再整体计算其 SHA-1 值;
-
序列化服务接口:
ISerializer- DefaultSerializer - 默认序列化服务采用JDK自带的对象序列化技术实现;
-
缓存事件监听接口:
ICacheEventListener- 用于监听被缓存对象发生变化时的事件处理,需开发者实现接口;
-
缓存作用域处理器接口:
ICacheScopeProcessor- 用于处理
@Cacheable注解的scope参数设置为非DEFAULT作用域的缓存对象,需开发者实现接口;
- 用于处理
模块配置
配置文件参数说明
#-------------------------------------
# 缓存模块初始化参数
#-------------------------------------
# 缓存提供者, 可选参数, 默认值: default, 目前支持[default|redis|multilevel]或自定义类名称
ymp.configs.cache.provider_class=
# 缓存对象事件监听器, 可选参数, 此类需实现net.ymate.platform.cache.ICacheEventListener接口, 默认值: 空
ymp.configs.cache.event_listener_class=
# 缓存作用域处理器, 可选参数, 此类需实现net.ymate.platform.cache.ICacheScopeProcessor接口, 默认值: 空
ymp.configs.cache.scope_processor_class=
# 缓存Key生成器, 可选参数, 此类需实现net.ymate.platform.cache.ICacheKeyGenerator接口, 默认值: net.ymate.platform.cache.impl.DefaultCacheKeyGenerator
ymp.configs.cache.key_generator_class=
# 对象序列化接口实现, 可选参数, 默认值: SerializerManager.getDefaultSerializer()
ymp.configs.cache.serializer_class=
# 默认缓存名称, 可选参数, 默认值: default, 对应于Ehcache配置文件中设置name="__DEFAULT__"
ymp.configs.cache.default_cache_name=
# 默认缓存数据超时时间(秒), 可选参数, 数值必须大于等于0, 默认值: 0
ymp.configs.cache.default_cache_timeout=
# 可选参数, 若未设置或设置的文件路径无效将使用默认值: ${root}/cfgs/ehcache.xml
ymp.configs.cache.config_file=
# Redis是否采用Set存储缓存键名, 默认值: false
ymp.configs.cache.storage_with_set=
# 是否开启Redis订阅缓存元素过期事件, 可选参数, 默认值: false
ymp.configs.cache.enabled_subscribe_expired=
# Multilevel模式下是否自动同步Master和Slave级缓存, 可选参数, 默认值: false
ymp.configs.cache.multilevel_slave_auto_sync=
配置注解参数说明
@CacheConf
| 配置项 | 描述 |
|---|---|
| defaultCacheName | 默认缓存名称 |
| defaultCacheTimeout | 默认缓存数据超时时间(秒) |
| configFile | Ehcache配置文件路径 |
| storageWithSet | 是否采用Set存储缓存键名 |
| subscribeExpired | 是否开启Redis订阅缓存元素过期事件 (注:Redis服务需开启 notify-keyspace-events Ex 配置) |
| multilevelSlavesAutoSync | Multilevel模式下是否自动同步Master和Slave级缓存 |
| providerClass | 缓存提供者 |
| eventListenerClass | 缓存对象事件监听器 |
| scopeProcessorClass | 缓存作用域处理器 |
| keyGeneratorClass | 缓存Key生成器 |
| serializerClass | 对象序列化接口实现 |
EhCache配置示例
以下是默认 ehcache.xml 文件内容,模块初始化时会检查该文件是否存在,若不存在则生成并放置在${root}/cfgs/路径下。
<ehcache updateCheck="false" dynamicConfig="false">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="300"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="300"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>
<cache name="__DEFAULT__"
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="300"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="300"
memoryStoreEvictionPolicy="LRU"
overflowToDisk="true"/>
</ehcache>
模块事件
当缓存对象事件监听器(event_listener_class)采用默认配置时,可以通过CacheEvent捕获缓存事件,事件枚举对象包括以下事件类型:
| 事务类型 | 说明 |
|---|---|
| ELEMENT_PUT | 缓存元素添加 |
| ELEMENT_UPDATED | 缓存元素更新 |
| ELEMENT_EXPIRED | 缓存元素过期 |
| ELEMENT_EVICTED | 缓存元素被驱逐 |
| ELEMENT_REMOVED | 缓存元素删 除 |
| ELEMENT_REMOVED_ALL | 缓存元素被清空 |
模块使用
示例一:通过缓存模块操作缓存数据
@EnableAutoScan
public class Starter {
static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}
public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
ICaches caches = Caches.get();
// 1. 将键名为key1的值写入默认缓存
caches.put("key1", "value1");
// 2. 从默认缓存中读取键名为key1的值
System.out.println(caches.get("key1"));
// 3. 将键名为key2的值写入指定名称的缓存(默认缓存的名称为:default)
caches.put("default", "key2", "value2");
// 4. 从指定名称的缓存中读取键名为key2的值
System.out.println(caches.get("default", "key2"));
// 5. 写入缓存时指定其超时时间(秒)
caches.put("key3", "value3", 30);
// 6. 更新指定缓存对象
caches.update("key3", "Value updated.", 50);
//
// 7. 当采用多级缓存(multilevel)时,可以通过MultilevelKey对象设置操作缓存目标:
if (caches.isMultilevel()) {
MultilevelKey key = new MultilevelKey("key4", false);
caches.put(key, "Value4", 50);
// 8. 读取多级缓存对象
caches.get(key);
}
}
}
}
当指定缓存名称时,请确认 ehcache.xml 文件中与名称对应的配置是否已存在。
示例二:基于注解完成类方法的缓存
这里用到了 @Cacheable 注解,作用是标识类中方法的执行结果是否进行缓存,需要注意的是:
首先
@Cacheable注解必须在已注册到YMP类对象管理器的类上声明,表示该类支持缓存;其次,在需要缓存执行结果的方法上添加
@Cacheable注解;
@Cacheable注解参数说明:
cacheName:缓存名称, 默认值为default;
key:缓存Key,若以
#开头则尝试从方法参数中获取该参数值,若未设置则使用Key生成器自动生成;generator:Key生成器接口实现类,默认为DefaultKeyGenerator.class;
scope:缓存作用域,可选值为
APPLICATION、SESSION和DEFAULT,默认为DEFAULT,非DEFAULT设置需要缓存作用域处理器(ICacheScopeProcessor)接口配合;timeout:缓存数据超时时间(秒), 默认值为0表示使用缓存配置的缓存数据超时时间;
示例代码:
@Bean
@Cacheable
public class CacheDemo {
@Cacheable
public String sayHi(String name) {
System.out.println("Not Cached");
return "Hi, " + name;
}
}
@EnableAutoScan
@EnableBeanProxy
public class Starter {
static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}
public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
CacheDemo demo = application.getBeanFactory().getBean(CacheDemo.class);
System.out.println(demo.sayHi("YMP"));
System.out.println(demo.sayHi("YMP"));
//
System.out.println("--------");
//
System.out.println(demo.sayHi("YMPer"));
System.out.println(demo.sayHi("YMP"));
System.out.println(demo.sayHi("YMPer"));
}
}
}
执行结果:
Not Cached
Hi, YMP
Hi, YMP
--------
Not Cached
Hi, YMPer
Hi, YMP
Hi, YMPer
以上结果输出可以看出,sayHi方法相同参数首次被调用时将输出“Not Cached”字符串,说明它没有使用缓存,再次调用时直接从缓存中返回值。
缓存 KEY 的自动生成规则
在 @Cacheable 注解的参数说明中已经说明,若 key 参数值以 # 开头则尝试从对应的方法参数中获取该参数值;若未设置则使用指定的缓存 Key 生成器接口实现类自动生成。ICacheKeyGenerator 接口的默认实现类 DefaultCacheKeyGenerator 采用的自动生成规则是:将方法参数类型与参数值集合序列化后,先对参数字节做 SHA-256 散列得到定长参数部分,再与当前类名、方法名组合成字符串并做 SHA-1 散列得到最终缓存 KEY,示意代码如下:
@Override
public Serializable generateKey(Method method, Object[] params) throws Exception {
// 参数空值归一,避免无参方法或调用方传入 null 时序列化抛异常
Object[] safeParams = params == null ? new Object[0] : params;
// 将参数类型纳入KEY组成,规避同名重载方法因序列化结果一致而产生的KEY碰撞
String paramTypes = Arrays.stream(method.getParameterTypes())
.map(Class::getName)
.collect(Collectors.joining(","));
// 直接对序列化后的参数字节做 SHA-256 散列得到定长参数部分,避免 Base64 文本膨胀与大对象物化
// [className:methodName(paramTypes):{sha256Hex(serializeParams)}]
String keyGenBuilder = String.format("[%s:%s(%s){%s}]",
method.getDeclaringClass().getName(),
method.getName(),
paramTypes,
DigestUtils.sha256Hex(serializer.serialize(safeParams)));
return DigestUtils.sha1Hex(keyGenBuilder);
}
- 自动生成的 KEY 同时包含参数类型,因此同名重载方法(如
compute(int)与compute(Integer))不会因序列化结果一致而发生缓存 KEY 碰撞。 - 参数部分采用
SHA-256散列(定长 64 位十六进制),外层整串再做SHA-1散列得到规范 KEY:既避免完整参数文本(Base64)膨胀,也保证最终 KEY 长度恒定,对大参数对象更省内存。 params为null时会被归一为空数组,避免序列化null抛出SerializationException。- 显式指定
key参数(或以#引用某参数值)时,将由开发者完全控制的键名生成,不经过上述自动规则。
缓存方法参数的使用约定
与缓存命中率及正确性相关,请重点关注
缓存 KEY 由方法参数的序列化字节决定,因此参数值的"序列化确定性"直接影响缓存能否正确命中,需注意以下事项:
- 避免使用无序集合作为缓存方法的参数:若参数中包含
HashMap、HashSet等无序集合,或其它序列化顺序不固定的对象,两次"逻辑相等"的调用可能序列化为不同字节,导致同一逻辑请求生成不同缓存 KEY,造成缓存命中率下降甚至结果错乱。建议改用List、LinkedHashMap、LinkedHashSet等有序结构,或在传入前转换为确定形态。 - 参数应尽量为确定性、可稳定比较的类型:基本类型及其包装类、字符串、普通 DTO/POJO(字段顺序固定)是安全的;应避免将运行期顺序不确定的集合、流对象、或含有瞬态/随机字段的对象作为缓存方法参数。
- 避免超大参数对象:对体积庞大的参数对象进行序列化与哈希计算会带来额外 CPU 与内存开销,并拉长缓存 KEY 的计算时间(尽管参数部分最终收敛为定长摘要,但超大对象的序列化与散列本身仍有成本)。此类场景建议改用显式
key参数,仅以能唯一标识业务含义的少量字段(如主键 ID)参与 KEY 生成。
缓存同步锁
示例:
@EnableAutoScan
@EnableBeanProxy
public class Starter {
static {
System.setProperty(IApplication.SYSTEM_MAIN_CLASS, Starter.class.getName());
}
public static void main(String[] args) throws Exception {
try (IApplication application = YMP.run(args)) {
ICache cache = Caches.get().getConfig().getCacheProvider().getCache("default");
ICacheLocker cacheLocker = cache.acquireCacheLocker();
// 设置锁的键名
String cacheKey = "cacheLockKey";
// 当采用多级缓存(multilevel)时,可以通过MultilevelKey对象设置操作缓存目标:
// MultilevelKey cacheKey = new MultilevelKey("cacheLockKey", true);
cacheLocker.writeLock(cacheKey);
try {
// ...
} finally {
cacheLocker.releaseWriteLock(cacheKey);
}
}
}
}
框架未提供基于 Redis 的缓存同步锁的默认实现,开发者可以通过 IRedisCacheLocker 接口自定义并以 SPI 方式加载。