搜索
您的当前位置:首页正文

Springboot3 + Springboot cache+Ehcache3 + Redisson 实现本地缓存管理及分布式本地缓存更新方案

来源:意榕旅游网

一、背景

使用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、ehcache3 配置

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>

2、redisson 配置

    @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);
    }

四、代码实现

1、本地缓存使用

使用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);
    }

2、Redisson 发布订阅

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;
    }
}

五、测试效果

1、启动效果

2、本地缓存测试:第一次取数据库,第二次取本地缓存

3、消息订阅本地缓存更新测试

两个服务80与8080端口,80端口服务更新数据,两个服务订阅变更消息更新缓存;
8080服务更新

六、参考文档



因篇幅问题不能全部显示,请点此查看更多更全内容

Top