文章目录
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,并进行查询,重新写入缓存
知识有限,如有错误,欢迎留言指正。