1. Ehcache简介
1.1 什么是Ehcache
Ehcache是Hibernate中默认的CacheProvider。
其缓存的数据可以是存放在内存里面的,也可以是存放在硬盘上的。
核心是CacheManager
1.2 Ehcache缓存过期策略
- FIFO:First In First Out,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。
- LRU:Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。
- LFU:Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。
1.3 Ehcache的特性
1、快速
2、简单
3、多种缓存策略
4、缓存数据有两级:内存和磁盘,因此无须担心容量问题
5、缓存数据会在虚拟机重启的过程中写入磁盘
6、可通过RMI、可插入APi等方式进行分布式缓存
7、具有缓存和缓存管理器的监听接口
8、支持多缓存管理器示例,以及一个实例的多个缓存区域
9、提供hibernate的缓存实现
1.4 EhCache的使用注意点
- 当用Hibernate的方式修改表数据(save,update,delete等等),这时EhCache会自动把缓存中关于此表的所有缓存全部删除掉(这样能达到同步)。但对于数据经常修改的表来说,可能就失去缓存的意义了(不能减轻数据库压力);
1.5 EhCache使用的场合
1.5.1 比较少更新表数据
- EhCache一般要使用在比较少执行write操作的表(包括update,insert,delete等)[Hibernate的二级缓存也都是这样];
1.5.2 对并发要求不是很严格的情况
- 两台机子中的缓存是不能实时同步的;
1.6 ehcache和redis比较
-
ehcache直接在jvm虚拟机中缓存,速度快,效率高;但是缓存共享麻烦,集群分布式应用不方便。
-
redis是通过socket访问到缓存服务器,效率比ehcache低,比数据库快。处理集群和分布式缓存方便,有成熟的方案。如果单个应用对缓存访问要求高可用ehcache,如果是大型系统,存在缓存共享、分布式部署、缓存内容大,建议用redis。
-
ehcache也有缓存共享方案,不过是通过RMI或者Jgroup多播方式进行广播缓存通知更新,缓存共享复杂,维护不方便;简单的共享可以,但涉及缓存恢复、大数据缓存,则不适合。
2. 分布式缓存与本地缓存的区别
2.1 为什么缓存速度比数据库速度快
- 因为关系数据库mysql、sqlserver数据存放在硬盘中,查询实现io操作,速度较慢。 而缓存技术是直接从内存中读取的,速度较快。
2.2 缓存类型
- Jvm内置缓存框架 ehcache(底层同步机制很差,持久化在硬盘上)、OScache也有持久化机制,唯独memcache没有持久化,市面上memcache的持久化是大神改造的。
- 非关系数据库Redis是完全开源免费的,是一个高性能的key-value数据库,支持持久化防止宕机,同时对空间大小有阈值设置,会有淘汰策略,目前市面上主流的数据库Redis、Memcache、Tair(淘宝自研发)。
2.3 分布式缓存与本地缓存的区别
- 分布式缓存一致性更好一点,本地缓存 每个实例都有自己的缓存,可能会存在不一致的情况。
- 本地缓存会占用堆内存,影响垃圾回收、影响系统性能。分布式缓存两大开销会导致其慢于本地缓存,网络延迟和对象序列化进程内缓存适用于较小且频率可见的访问场景,尤其适用于不变对象,对于较大且不可预见的访问,最好采用分布式缓存。
3. SpringBoot集成Ehcache
3.1 搭建SpringBoot项目
- Intellij IDEA 一路 next 或者 https://start.spring.io/
3.2 项目结构
3.3 引入依赖
SpringBoot 相关依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 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 依赖
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.8.1</version>
</dependency>
cache-api 提供基于JSR-107的缓存规范
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
lombok 简化代码
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
工具类,提高效率
<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>
3.4 编写实体类
User实体类。采用 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 做初始化调用。
/*
仓库类。为简化代码(懒得搭建数据库及连接池),
基于 ConcurrentMap 实现的内存数据库
*/
@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);
}
}
用户服务类。简单调用,进行日志记录
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public User getUserById(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获取用户信息
*/
@GetMapping("/{id}")
public User getById(@PathVariable(value="id") @NonNull
@Min(value=1,message = "id为非负数")Integer id){
return userService.getUserById(id);
}
}
3.5 配置 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"></jsr107:defaults>
</service>
<!-- user为该缓存名称,对应@Cacheable的属性cacheNames-->
<cache alias="user">
<!--指定缓存key类型,对应@Cacheable的属性key-->
<key-type>java.lang.Integer</key-type>
<value-type>cn.itcast.pojo.User</value-type>
<expiry>
<!--缓存ttl-->
<ttl unit="minutes">1</ttl>
</expiry>
<listeners>
<listener>
<!--配置listener-->
<class>cn.itcast.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);
}
}
3.6 使用 Ehcache
使用较为简单,只需要在需要使用缓存的地方添加注解 @Cacheable ,以及配置相应属性项即可
一般我们会分析业务,对于一些耗时较长,并且数据不易改变的接口请求做缓存处理。此处我们对 UserRepository 做缓存,用户信息一般不易改变,满足缓存的条件;另一方面,用户信息一般存储在数据库、磁盘中,访问耗时较长,有做缓存的必要。
3.7 验证
此时我们运行项目,直接通过浏览器、Postman 进行测试,并查看其日志即可验证。
3.7.1 第一次请求接口
接口正常返回数据,日志如下:
第一行日志:接口执行到了 UserService 的 getById 方法。
第二行日志:接口执行到了 UserRepository 的 getById 方法。从而知道此次执行并未走缓存,因为我们是第一次请求,还没把数据写入缓存。
第三行日志:配置的 Listener 记录的日志,Ehcache 添加(CREATED)了缓存,key 为 1, oldValue 为 null, newValue 为 User(id=1, name=Alice)
3.7.2 第二次请求接口(与上一次间隔 TTL 以内)
接口正常返回数据,日志如下:
只有一行日志,表示执行到 UserService 的 getById 方法,并未执行到 UserRepository,说明缓存命中成功。
3.7.3 第三次请求(与上一次间隔超过 TTL)
接口正常返回数据,日志如下:
- 第一行日志:接口执行到了 UserService 的 getById 方法。
- 第二行日志:接口执行到了 UserRepository 的 getById 方法。从而知道此次执行并未走缓存。
- 第三行日志:缓存日志,检查到原来的缓存过期了,把缓存值更新为 null。
- 第四行日志:新添加了缓存值
值得注意的是,这里是分了两步进行,先进行校验 TTL ,没过期则直接进行返回缓存数据,过期了则直接设为 null。然后又进行写入缓存数据。
所以我猜想,按照正常执行流程,这个日志应该是有一定的顺序错误,可能是因为异步任务造成。第二行和第三行应该进行替换。
正确流程应该为:
- 先进行校验缓存值是否存在
- 如果缓存不存在,则直接进行查询。如果存在,则根据 TTL 校验缓存是否有效
- 如果有效,则直接返回。如果失效,则将缓存值置为 null,并进行查询,重新写入缓存