SpringBoot之Ehcache缓存集群

438 阅读3分钟

Ehcache

SpringBoot集成Ehcache并配置集群服务

maven依赖

<!-- ehcache2 -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

yml配置

spring:
  cache:
    type: ehcache # 缓存类型
    ehcache:
      config: classpath:ehcache.xml # EhCache配置文件
      manager-peer-listener-port: 40001 # 监听其他节点发送到当前CacheManager的信息的监听器端口

PS:如果多个集群应用部署在同一服务器则在启动时需要设置不同的spring.cache.ehcache.manager-peer-listener-port,具体可参考源码下的cluster-run-1.shcluster-run-2.sh

ehcache.xml

根目录下ehcache.xml是Ehcache配置文件,其中包含集群配置,集群节点采用“自动发现”的方式。

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">

    <!-- 当内存缓存中的对象数量超过了maxElementInMemory时,将缓存对象写到磁盘中,缓存对象需要实现序列化接口
         如果path的值是Java系统参数则将会取改参数在JVM运行时的实际值,如下
         * user.home - 用户的主目录
         * user.dir - 用户的当前工作目录
         * java.io.tmpdir - 默认临时文件目录
         * ehcache.disk.store.dir - 应用运行时自定义的参数,如 java -Dehcache.disk.store.dir=/u01/myapp/diskdir ...
         还可以指定子目录,如 java.io.tmpdir/one
    -->
    <diskStore path="java.io.tmpdir"/>

    <!--
        分布式节点发现配置
        peerDiscovery:automatic(通过TCP组播的方式自动发现节点)
        multicastGroupAddress:组播地址
        multicastGroupPort:组播端口
        timeToLive:组播的传播距离,0-同一主机,1-同一子网,32-同一站点,64-同一地区,128-同一大陆,255-无限制
    -->
    <cacheManagerPeerProviderFactory
            class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
            properties="peerDiscovery=automatic,
                        multicastGroupAddress=230.0.0.1,
                        multicastGroupPort=4446,
                        timeToLive=32"/>

    <!--
        监听其他节点发送到当前CacheManager的信息
        hostName:本地主机ip,可以手动设置,这里改为代码配置自动获取,见com.sword.ehcache.config.CacheConfig#calculateHostAddress
        port:监听端口,如果两个成员在同一服务器,则需要不同,可配置文件配置,见com.sword.ehcache.config.CacheConfig#calculateHostAddress
        socketTimeoutMillis:连接超时时间
     -->
    <cacheManagerPeerListenerFactory
            class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
            properties="port=40001,
                        socketTimeoutMillis=120000"/>

    <!--
        强制性默认缓存配置. 这些配置只有在以编程的方式“CacheManager.add(String cacheName)”来创建缓存时有效
        maxElementsInMemory:缓存在内存中的对象个数最大值
        maxElementsOnDisk:缓存在硬盘中的对象个数最大值
        eternal:缓存是否不会被销毁,false则会被销毁
        timeToLiveSeconds:缓存对象的生存时间(秒)
        diskExpiryThreadIntervalSeconds:磁盘到期线程的运行时间(秒)
        memoryStoreEvictionPolicy:内存存储移出策略,LRU-最近最少使用的优先移出,FIFO-先进先出,LFU-较少使用
    -->
    <defaultCache
            maxElementsInMemory="10000"
            maxElementsOnDisk="10000000"
            eternal="false"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <!-- 缓存持久化策略,localTempSwap-本地磁盘临时存储,重启应用将删除所有缓存 -->
        <persistence strategy="localTempSwap"/>
    </defaultCache>

    <!-- 用户缓存配置,name-缓存名称,其他配置与默认配置相同 -->
    <cache
            name="user"
            maxElementsInMemory="10000"
            maxElementsOnDisk="10000000"
            eternal="false"
            timeToLiveSeconds="120"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">

        <!-- 缓存复制到其他节点的监听器 -->
        <cacheEventListenerFactory
                class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
                properties="replicateAsynchronously=true,
                            replicatePuts=true,
                            replicateUpdates=true,
                            replicateUpdatesViaCopy=true,
                            replicateRemovals=true "/>

        <!--
            新增节点的缓存引导(初始化)
            bootstrapAsynchronously:是否异步引导,即是否缓存可用后在进行引导
            maximumChunkSizeBytes:引导时每次获取的缓存的最大字节
        -->
        <bootstrapCacheLoaderFactory
                class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"
                properties="bootstrapAsynchronously=true,
                            maximumChunkSizeBytes=5000000"/>

        <persistence strategy="localTempSwap"/>
    </cache>
</ehcache>

开启缓存并设置一些可配置参数

/**
 * 缓存配置
 * @author sword
 * @date 2020/9/30 17:26
 */
@Configuration
@EnableCaching
public class CacheConfig {

    /**
     * rmi方式的节点监听器类型
     */
    public static final String RMI_CACHE_MANAGER_PEER_LISTENER_FACTORY_CLASS_NAME =
            "net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory";

    /**
     * 主机名称参数键名
     */
    public static final String HOST_NAME = "hostName";

    /**
     * 监听其他节点发送到当前CacheManager的信息的监听器端口
     */
    @Value("${spring.cache.ehcache.manager-peer-listener-port}")
    private String cacheManagerPeerListenerPort;

    /**
     * 监听其他节点发送到当前CacheManager的信息的监听器端口参数键名
     */
    public static final String CACHE_MANAGER_PEER_LISTENER_PORT_NAME = "port";

    /**
     * 如果缓存用的是EhCache则需要自定义CacheManager
     * 因为在设置cacheManagerPeerListener时,如果hostName为空则默认使用InetAddress.getLocalHost().getHostAddress()
     * 来获取服务器的IP,但Linux服务器用这个方法只能获取到127.0.0.1,所以在hostName为空时需要换种方法获取IP
     * @param cacheProperties 缓存配置
     * @return net.sf.ehcache.CacheManager
     * @author sword
     * @date 2020/10/9 14:49
     */
    @Bean
    @Primary
    @ConditionalOnProperty(name = "spring.cache.type", havingValue = "ehcache")
    public CacheManager ehCacheCacheManager(CacheProperties cacheProperties) {
        // 配置文件,文件路径对应spring.cache.ehcache.config,默认为classpath:ehcache.xml
        Resource location = cacheProperties.resolveConfigLocation(cacheProperties.getEhcache().getConfig());

        // 如果配置文件不为空则根据配置文件生成CacheManager,否则返回默认CacheManager
        if (location != null) {
            // 解析配置文件生存配置参数对象
            net.sf.ehcache.config.Configuration configuration = EhCacheManagerUtils.parseConfiguration(location);

            // 计算本机ip地址
            calculateHostAddress(configuration);

            return new CacheManager(configuration);
        }
        else {
            return EhCacheManagerUtils.buildCacheManager();
        }
    }

    /**
     * 计算本机ip地址
     * @param configuration EhCache配置参数对象
     * @author sword
     * @date 2020/10/10 9:15
     */
    public void calculateHostAddress(net.sf.ehcache.config.Configuration configuration) {
        configuration.getCacheManagerPeerListenerFactoryConfigurations().forEach(factoryConfiguration -> {
            // 如果不是RMI方式的节点监听器则直接跳过
            if (!RMI_CACHE_MANAGER_PEER_LISTENER_FACTORY_CLASS_NAME.equals(factoryConfiguration.getFullyQualifiedClassPath())) {
                return;
            }

            // cacheManagerPeerListener的配置参数
            Properties properties = PropertyUtil.parseProperties(
                    factoryConfiguration.getProperties(), factoryConfiguration.getPropertySeparator()
            );

            // 如果主机名称不为空则直接返回
            if (StringUtils.isNotEmpty(properties.getProperty(HOST_NAME))) {
                return;
            }

            // 设置主机名称,即本机的Ipv4地址
            properties.setProperty(HOST_NAME, CommonUtil.getHostAddressIpv4());

            // 顺便设置一下监听其他节点发送到当前CacheManager的信息的监听器端口
            properties.setProperty(CACHE_MANAGER_PEER_LISTENER_PORT_NAME, cacheManagerPeerListenerPort);

            // 将修改后的配置参数重新设置回去
            try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                properties.store(outputStream, null);
                factoryConfiguration.setProperties(outputStream.toString());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
}

使用ehcache.xml中配置的缓存user

/**
 * 用户接口
 * @author sword
 * @date 2021/11/11 17:25
 */
@RestController
@RequestMapping("/user")
@Api(tags = "用户接口")
public class UserApi {

    /**
     * 新增用户,同时以用户id为key将用户信息设置到缓存名称为“user”的缓存中
     * @param user 用户信息
     * @return com.sword.ehcache.model.User
     * @author sword
     * @date 2021/11/28 21:18
     */
    @PostMapping
    @CachePut(cacheNames = "user", key = "#user.id")
    @ApiOperation("新增用户")
    public User insert(@RequestBody User user) {
        return user;
    }

    /**
     * 删除用户,同时以用户id为key将缓存名称为“user”的缓存中对应的数据删除
     * @param id 用户id
     * @return boolean
     * @author sword
     * @date 2021/11/28 21:19
     */
    @DeleteMapping("/{id}")
    @CacheEvict(cacheNames = "user", key = "#id")
    @ApiOperation("删除用户")
    public boolean delete(@PathVariable String id) {
        return true;
    }

    /**
     * 查询指定用户,同时以用户id为key到缓存名称为“user”的缓存中查询对应的数据,如果找到则不执行查询方法直接返回缓存数据
     * 否则先执行查询方法,并将返回结果设置到缓存中
     * @param id 用户
     * @return com.sword.ehcache.model.User
     * @author sword
     * @date 2021/11/28 21:19
     */
    @GetMapping("/{id}")
    @ApiOperation("查询指定用户")
    @Cacheable(cacheNames = "user", key = "#id")
    public User get(@PathVariable String id) {
        return null;
    }
}

注意事项

  • 缓存对象需要实现序列化接口
  • 两台windows系统的服务器做集群可能会因为防火墙的原因导致无法发现集群成员节点
  • cacheManagerPeerListenerFactory的hostName如果不填的话会使用InetAddress.getLocalHost().getHostAddress()去获取ip,但在linux下InetAddress.getLocalHost().getHostAddress()通常会返回127.0.0.1

相关源码详见gitee