SpringBoot集成Ehcache实现本地缓存

1. 前言

Ehcache 作为本地缓存,接入简单。
想了解更多知识,可 Google 下 Ehcache,Redis, memcache 等的区别。

我们今天主要将实操,SpringBoot 集成 Ehcache 实现本地缓存。

2. 搭建SpringBoot项目

Intellij IDEA 一路 next 或者 spring.io

2.1 引入依赖

springBoot 相关依赖

<!-- starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- 测试 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 缓存支持,超级重要 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

ehcache 依赖,重头戏

<!-- https://mvnrepository.com/artifact/org.ehcache/ehcache -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.8.1</version>
</dependency>

cache-api 提供基于JSR-107的缓存规范

<!-- https://mvnrepository.com/artifact/javax.cache/cache-api -->
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>

lombok 用于简化代码

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
    <scope>provided</scope>
</dependency>

工具类,提高效率

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.9</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

2.2. 编写业务类

实体类。采用 Lombok 简化代码

@Data
@NoArgsConstructor
@AllArgsConstructor
/**
 * 建造者模式 链式调用
 */
@Accessors(chain = true)
public class User implements Serializable {

    private static final long serialVersionUID = -8451701378160929387L;

    /**
     * 用户ID
     */
    private Integer id;

    /**
     * 用户name
     */
    private String name;
}

仓库类。为简化代码(懒得搭建数据库及连接池),基于 ConcurrentMap 实现的内存数据库。采用 Slf4j 做日志支持,采用 PostConstruct 做初始化调用。

@Slf4j
@Component
public class UserRepository {

    /**
     * 内存数据库
     */
    private Map<Integer, User> userMap = Maps.newConcurrentMap();

    /**
     * 初始化
     */
    @PostConstruct
    private void init() {
        userMap.put(1, new User(1, "Alice"));
        userMap.put(2, new User(2, "Bob"));
    }

    public User getById(Integer id) {
        log.info("UserRepository#getById: id={}", id);
        return userMap.get(id);
    }

}

用户服务类。简单调用,进行日志记录

@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public User getById(Integer id) {
        log.info("UserService#getById: id={}", id);
        return userRepository.getById(id);
    }
}

web 类。提供路由,采用 Validator 做参数校验,接口采用 RESTful 风格。

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 根据ID获取用户信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public User getById(@PathVariable(value = "id")
                            @NonNull @Min(value = 1, message = "id 为非负数") Integer id) {
        return userService.getById(id);
    }

}

3. 配置 Ehcache

基于前文已经进行了依赖的引入,配置 Ehcache 较为简单。

application.yml 配置 Ehcache 配置文件路径,方便 SpringBoot 扫描到配置文件。其中, ehcache.xml 为 Ehcache 配置文件名称。

spring:
  cache:
    jcache:
      config: classpath:ehcache.xml

配置 Listener,目前进行日志记录。

@Slf4j
public class CacheEventLogger implements CacheEventListener<Object, Object> {

    @Override
    public void onEvent(CacheEvent<?, ?> cacheEvent) {
        log.info("cache event logger: type={}, key={}, oldValue={}, newValue={}",
                cacheEvent.getType(),
                cacheEvent.getKey(),
                cacheEvent.getOldValue(),
                cacheEvent.getNewValue());
    }
}

配置 Ehcache

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="
            http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
            http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
    <service>
        <jsr107:defaults enable-statistics="true"/>
    </service>

    <!-- user 为该缓存名称 对应@Cacheable的属性cacheNames-->
    <cache alias="user">
        <!-- 指定缓存 key 类型,对应@Cacheable的属性key -->
        <key-type>java.lang.Integer</key-type>
        <!-- 配置value类型 -->
        <value-type>club.chenlinghong.demo.ehcache.entity.User</value-type>
        <expiry>
            <!-- 缓存 ttl -->
            <ttl unit="minutes">1</ttl>
        </expiry>
        <listeners>
            <listener>
                <!-- 配置Listener -->
                <class>club.chenlinghong.demo.ehcache.listener.CacheEventLogger</class>
                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
                <event-ordering-mode>UNORDERED</event-ordering-mode>
                <events-to-fire-on>CREATED</events-to-fire-on>
                <events-to-fire-on>UPDATED</events-to-fire-on>
                <events-to-fire-on>EXPIRED</events-to-fire-on>
                <events-to-fire-on>REMOVED</events-to-fire-on>
                <events-to-fire-on>EVICTED</events-to-fire-on>
            </listener>
        </listeners>
        <resources>
            <!-- 分配资源大小 -->
            <heap unit="entries">2000</heap>
            <offheap unit="MB">100</offheap>
        </resources>
    </cache>
</config>

@EnableCaching 开启缓存支持

@SpringBootApplication
@EnableCaching
public class EhcacheApplication {

    public static void main(String[] args) {
        SpringApplication.run(EhcacheApplication.class, args);
    }

}

4. 使用 Ehcache

使用较为简单,只需要在需要使用缓存的地方添加注解 @Cacheable ,以及配置相应属性项即可

一般我们会分析业务,对于一些耗时较长,并且数据不易改变的接口请求做缓存处理。此处我们对 UserRepository 做缓存,用户信息一般不易改变,满足缓存的条件;另一方面,用户信息一般存储在数据库、磁盘中,访问耗时较长,有做缓存的必要。

@Slf4j
@Component
public class UserRepository {

    /**
     * 内存数据库
     */
    private Map<Integer, User> userMap = Maps.newConcurrentMap();

    /**
     * 初始化
     */
    @PostConstruct
    private void init() {
        userMap.put(1, new User(1, "Alice"));
        userMap.put(2, new User(2, "Bob"));
    }

    /**
     * 此处的 cacheNames 需要和 ehcache 配置文件的配置项一致
     */
    @Cacheable(cacheNames = "user", key = "#id")
    public User getById(Integer id) {
        log.info("UserRepository#getById: id={}", id);
        return userMap.get(id);
    }

}

完整代码:https://github.com/chenlinghong/demo

5. 测试

此时我们运行项目,直接通过浏览器、Postman 进行测试,并查看其日志即可验证。

5.1. 第一次请求接口

接口正常返回数据,日志如下:

2019-09-20 01:21:23.511  INFO 13548 --- [nio-8080-exec-1] c.c.d.e.service.impl.UserServiceImpl     : UserService#getById: id=1
2019-09-20 01:21:23.544  INFO 13548 --- [nio-8080-exec-1] c.c.d.ehcache.repository.UserRepository  : UserRepository#getById: id=1
2019-09-20 01:21:23.559  INFO 13548 --- [e [_default_]-0] c.c.d.ehcache.listener.CacheEventLogger  : cache event logger: type=CREATED, key=1, oldValue=null, newValue=User(id=1, name=Alice)

第一行日志:接口执行到了 UserService 的 getById 方法。
第二行日志:接口执行到了 UserRepository 的 getById 方法。从而知道此次执行并未走缓存,因为我们是第一次请求,还没把数据写入缓存。
第三行日志:配置的 Listener 记录的日志,Ehcache 添加(CREATED)了缓存,key 为 1, oldValue 为 null, newValue 为 User(id=1, name=Alice)

5.2. 第二次请求接口(与上一次间隔 TTL 以内)

接口正常返回数据,日志如下:

2019-09-20 01:24:46.441  INFO 13548 --- [io-8080-exec-10] c.c.d.e.service.impl.UserServiceImpl     : UserService#getById: id=1

只有一行日志,表示执行到 UserService 的 getById 方法,并未执行到 UserRepository,说明缓存命中成功。

5.3. 第三次请求(与上一次间隔超过 TTL)

接口正常返回数据,日志如下:

2019-09-20 01:28:16.156  INFO 13548 --- [nio-8080-exec-6] c.c.d.e.service.impl.UserServiceImpl     : UserService#getById: id=1
2019-09-20 01:28:16.161  INFO 13548 --- [nio-8080-exec-6] c.c.d.ehcache.repository.UserRepository  : UserRepository#getById: id=1
2019-09-20 01:28:16.167  INFO 13548 --- [e [_default_]-2] c.c.d.ehcache.listener.CacheEventLogger  : cache event logger: type=EXPIRED, key=1, oldValue=User(id=1, name=Alice), newValue=null
2019-09-20 01:28:16.169  INFO 13548 --- [e [_default_]-2] c.c.d.ehcache.listener.CacheEventLogger  : cache event logger: type=CREATED, key=1, oldValue=null, newValue=User(id=1, name=Alice)

第一行日志:接口执行到了 UserService 的 getById 方法。
第二行日志:接口执行到了 UserRepository 的 getById 方法。从而知道此次执行并未走缓存。
第三行日志:缓存日志,检查到原来的缓存过期了,把缓存值更新为 null。
第四行日志:新添加了缓存值

值得注意的是,这里是分了两步进行,先进行校验 TTL ,没过期则直接进行返回缓存数据,过期了则直接置为 null。然后又进行写入缓存数据。
所以我猜想,按照正常执行流程,这个日志应该是有一定的顺序错误,可能是因为异步任务造成。第二行和第三行应该进行替换。

正确流程应该为:
1、先进行校验缓存值是否存在
2、如果缓存不存在,则直接进行查询。如果存在,则根据 TTL 校验缓存是否有效
3、如果有效,则直接返回。如果失效,则将缓存值置为 null,并进行查询,重新写入缓存

知识有限,如有错误,欢迎留言指正。

参考

http://www.ehcache.org
ehcache github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章