使用ehcache3+redisson,实现本地缓存配置管理及分布本地缓存更新方案。项目使用springboot3.1.7 gradle8.5。
核心逻辑:采用redisson 发布订阅模式同步变更消息。
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
//redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.26.0'
//缓存
implementation('org.springframework.boot:spring-boot-starter-cache')
//本地缓存
implementation('org.ehcache:ehcache:3.10.0')
implementation('javax.cache:cache-api:1.1.0')
// To read XML config
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
// To read XML config
implementation group: 'com.sun.xml.bind', name: 'jaxb-impl', version: '2.3.6'
1)开启注解 -----spingboot启动类或配置类
@EnableCaching
2)cache配置 jcache -----application.properties
# jcache
spring.cache.type=jcache
# jcache支持: ehcache3 redis等
spring.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
# 配置文件地址
spring.cache.jcache.config=classpath:ehcache3.xml
3)ehcache3.xml
<config xmlns='http://www.ehcache.org/v3'>
<!-- 持久化 路径 -->
<persistence directory="./tmp/Ehcache3"/>
<!-- 缓存模版,此处为了显示其用法,也可以不用模版直接在cache中配置与模版参数相同 -->
<cache-template name="oneHourTemplate">
<!-- <key-type>java.lang.String</key-type>-->
<!-- <value-type>java.lang.String</value-type>-->
<expiry>
<!-- 单位默认为秒当用秒作单位时,可以不填-->
<ttl unit="hours">1</ttl>
</expiry>
<resources>
<!-- 单位默认为entries当用entries作单位时,可以不填-->
<heap>100</heap>
<offheap unit="MB">5</offheap>
<!-- persistent 默认为false可以不填-->
<disk unit="MB">1024</disk>
</resources>
</cache-template>
<cache-template name="tenMinutesTemplate">
<!-- <key-type>java.lang.String</key-type>-->
<!-- <value-type>java.lang.String</value-type>-->
<expiry>
<!-- 单位默认为秒当用秒作单位时,可以不填-->
<ttl unit="minutes">10</ttl>
</expiry>
<resources>
<!-- 单位默认为entries当用entries作单位时,可以不填-->
<heap>1000</heap>
<offheap unit="MB">50</offheap>
<!-- persistent 默认为false可以不填-->
<disk unit="MB" persistent="false">1024</disk>
</resources>
</cache-template>
<cache-template name="tenSecondsTemplate">
<!-- <key-type>java.lang.String</key-type>-->
<!-- <value-type>java.lang.String</value-type>-->
<expiry>
<!-- 单位默认为秒当用秒作单位时,可以不填-->
<ttl unit="seconds">10</ttl>
</expiry>
<resources>
<!-- 单位默认为entries当用entries作单位时,可以不填-->
<heap>1000</heap>
<offheap unit="MB">50</offheap>
<!-- persistent 默认为false可以不填-->
<disk unit="MB" persistent="true">1024</disk>
</resources>
</cache-template>
<!-- 缓存对象,如果使用了模版会覆盖模版中的内容,使用uses-template=""来引用模版 -->
<cache alias="indexArticleList" uses-template="oneHourTemplate">
<!-- <key-type>java.lang.Integer</key-type>-->
<!-- <value-type>com.github.pagehelper.PageInfo</value-type>-->
<!-- <expiry>-->
<!-- <!–此处会覆盖模版中的(TTL)配置 –>-->
<!-- <tti>60</tti>-->
<!-- </expiry>-->
<!-- <resources>-->
<!-- <disk unit="MB" persistent="true"> 500</disk>-->
<!-- </resources>-->
<!-- 没有研究这块,暂时先不管
<eviction-advisor></eviction-advisor>
-->
</cache>
<cache alias="modules" uses-template="tenMinutesTemplate">
<!-- <key-type>java.lang.String</key-type>-->
<!-- <value-type>java.lang.Boolean</value-type>-->
</cache>
<cache alias="tests" uses-template="tenSecondsTemplate" >
<key-type>java.lang.Long</key-type>
<value-type>com.sun.practice.domain.Test</value-type>
</cache>
</config>
@Value("${spring.data.redis.host}")
public String host;
@Value("${spring.data.redis.port}")
public String port;
@Value("${spring.data.redis.password}")
public String password;
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
// 单节点/代理
SingleServerConfig singleServerConfig = config.useSingleServer();
singleServerConfig.setAddress("redis://" + host + ":" + port).setPassword(password);
// 使用json序列化方式
ObjectMapper objectMapper = new ObjectMapper();
//忽略value为null 时 key的输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.disable(SerializationFeature.INDENT_OUTPUT);
objectMapper.registerModule(new JavaTimeModule());
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(module);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
config.setCodec(new JsonJacksonCodec(objectMapper));
return Redisson.create(config);
}
使用spring cache 注解:@Cacheable @CachePut 注解介绍请参考 参考文档3(很全)
/**
* 3、修改test
*
* @param test test
* @return test
*/
@Override
@Transactional(rollbackFor = Exception.class)
public SysResult<?> updateTest(Test test) {
if (null == test.getId()) {
return SysResult.fail("参数异常:id不能为空");
}
int count = testDao.updateByPrimaryKeySelective(test);
if (count > 0) {
//更新本地缓存-----核心
ehcache3Listener.publishTestsUpdateMessage(test.getId());
return SysResult.success();
}
return SysResult.fail("修改test失败");
}
/**
* 4、获取test
*
* @param id test id
* @return test
*/
@Override
@Cacheable(cacheNames = "tests", key = "#id")
public Test getTestById(Long id) {
return testDao.selectByPrimaryKey(id);
}
/**
* 1、test 缓存重置
*/
@CachePut(cacheNames = "tests", key = "#id")
public Test resetTest(Long id) {
return testDao.selectByPrimaryKey(id);
}
package com.sun.practice.service;
import jakarta.annotation.Resource;
import org.redisson.api.RTopic;
import org.redisson.api.RedissonClient;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/**
* 微服务本地缓存同步 Runner
* 设计逻辑
* 1、服务启动后启动本Runner
* 2、使用 redisson 订阅、发布:订阅 local_cache topic消息
* 3、根据消息内不同缓存类型-更新对应缓存
*
* @author slj
* @date 2024/02/07 17:51
**/
@Component
public class LocalCacheSynchronizationRunner implements ApplicationRunner {
@Resource
private RedissonClient redissonClient;
@Resource
private Ehcache3Listener ehcache3Listener;
/**
* Callback used to run the bean.
*
* @param args incoming application arguments
* @throws Exception on error
*/
@Override
public void run(ApplicationArguments args) throws Exception {
//1、创建topic local_cache
RTopic topic = redissonClient.getTopic("local_cache");
//2、创建订阅
topic.addListener(LocalCacheMessage.class, ehcache3Listener);
}
}
package com.sun.practice.service;
import com.sun.practice.domain.Test;
import com.sun.practice.enums.LocalCache;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RTopic;
import org.redisson.api.RedissonClient;
import org.redisson.api.listener.MessageListener;
import org.springframework.stereotype.Component;
/**
* ehcache3 本地缓存监听器
* 主要功能设计
* 1、消息订阅处理
* 2、变更消息发送
*
* @author slj
* @date 2024/02/16 10:15
**/
@Slf4j
@Component
public class Ehcache3Listener implements MessageListener<LocalCacheMessage<?>> {
@Resource
private RedissonClient redissonClient;
@Resource
private EhcacheService ehcacheService;
/**
* 1、消息订阅 消息处理
*
* @param channel topic channel
* @param msg topic message
*/
@Override
public void onMessage(CharSequence channel, LocalCacheMessage<?> msg) {
switch (msg.getCacheName()) {
case "tests":
Test test = ehcacheService.resetTest((Long) msg.getKey());
log.info("tests 缓存更新处理:key={},test={}", msg.getKey(), test);
break;
case "modules":
log.info("modules 缓存处理");
break;
}
}
/**
* 2、消息发布
*
* @param localCacheMessage 消息体
*/
public void publishMessage(LocalCacheMessage<?> localCacheMessage) {
//1、创建topic local_cache
RTopic topic = redissonClient.getTopic(LocalCache.LOCAL_CACHE.getCacheName());
//2、发布消息
topic.publish(localCacheMessage);
}
/**
* 3、tests 缓存变更消息发布
*
* @param key 缓存key
*/
public void publishTestsUpdateMessage(Long key) {
//0、组合消息体
LocalCacheMessage<Long> testsCache = new LocalCacheMessage<>();
testsCache.setCacheName(LocalCache.TESTS.getCacheName());
testsCache.setKey(key);
//1、发送消息
publishMessage(testsCache);
}
}
package com.sun.practice.service;
import lombok.Data;
/**
* 本地缓存同步消息
*
* @author slj
* @date 2024/02/16 10:19
**/
@Data
public class LocalCacheMessage<T> {
/**
* 本地缓存名
*/
private String cacheName;
/**
* 本地缓存key
*/
private T key;
}
package com.sun.practice.enums;
/**
* 本地缓存枚举
*/
public enum LocalCache {
LOCAL_CACHE("local_cache", "本地缓存 topic"),
TESTS("tests", "test本地缓存"),
MODULES("modules", "module本地缓存");
private String cacheName;
private String remark;
LocalCache(String cacheName, String remark) {
this.cacheName = cacheName;
this.remark = remark;
}
public String getCacheName() {
return cacheName;
}
public void setCacheName(String cacheName) {
this.cacheName = cacheName;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
两个服务80与8080端口,80端口服务更新数据,两个服务订阅变更消息更新缓存;
8080服务更新
因篇幅问题不能全部显示,请点此查看更多更全内容