Springboot缓存
说明
随着时间的积累,应用的使用用户不断增加,数据规模也越来越大,往往数据库查询操作会成为影响用户使用体验的瓶颈,此时使用缓存往往是解决这一问题非常好的手段之一。
Spring 3开始提供了强大的基于注解的缓存支持,可以通过注解配置方式低侵入的给原有Spring应用增加缓存功能,提高数据访问性能。
在Spring Boot中对于缓存的支持,提供了一系列的自动化配置,使我们可以非常方便的使用缓存。
快速入门
User实体的定义
1 |
|
User实体的数据访问实现
1 |
|
单元测试类
插入User表一条用户名为AAA,年龄为10的数据。并通过findByName函数完成两次查询,具体代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Chapter51ApplicationTests {
private UserService userService;
CacheManager cacheManager;
public void test1() {
User user1 = userService.getUserByName("张三");
System.out.println("-----------------");
User user2 = userService.getUserByName("张三");
}
}在没有加入缓存之前,我们可以先执行一下这个案例,可以看到如下的日志:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@43a09ce2] was not registered for synchronization because synchronization is not active
2021-06-20 22:46:41.449 INFO 11548 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-06-20 22:46:41.792 INFO 11548 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@278166606 wrapping com.mysql.cj.jdbc.ConnectionImpl@3667faa8] will not be managed by Spring
==> Preparing: select id, name, age from user WHERE ( name = ? )
==> Parameters: 张三(String)
<== Columns: id, name, age
<== Row: 1, 张三, 23
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@43a09ce2]
-----------------
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6ebc9573] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@513241240 wrapping com.mysql.cj.jdbc.ConnectionImpl@3667faa8] will not be managed by Spring
==> Preparing: select id, name, age from user WHERE ( name = ? )
==> Parameters: 张三(String)
<== Columns: id, name, age
<== Row: 1, 张三, 23
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6ebc9573]两次查询都执行了两次SQL,都是对MySQL数据库的查询。
引入缓存
第一步:在
pom.xml
中引入cache依赖,添加如下内容:1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>第二步:在Spring Boot主类中增加
@EnableCaching
注解开启缓存功能,如下:1
2
3
4
5
6
7
8
9
public class Chapter51Application {
public static void main(String[] args) {
SpringApplication.run(Chapter51Application.class, args);
}
}第三步:在数据访问接口中,增加缓存配置注解,如:
1
2
3
4
5
6
7
8
9
10
11
12
13// 查询,使用缓存 查询结果为空则不缓存
public User getUserByName( { String name)
UserExample example = new UserExample();
Criteria criteria = example.createCriteria();
criteria.andNameEqualTo(name.trim());
List<User> users = userMapper.selectByExample(example);
if (users.size() > 1) {
return null;
} else {
return users.get(0);
}
}第四步:再来执行以下单元测试,可以在控制台中输出了下面的内容
1
2
3
4
5
6
7
8
9
10
11
12
13Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fd05028] was not registered for synchronization because synchronization is not active
2021-06-20 22:48:59.046 INFO 15704 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-06-20 22:48:59.439 INFO 15704 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
JDBC Connection [HikariProxyConnection@1157484092 wrapping com.mysql.cj.jdbc.ConnectionImpl@712c5463] will not be managed by Spring
==> Preparing: select id, name, age from user WHERE ( name = ? )
==> Parameters: 张三(String)
<== Columns: id, name, age
<== Row: 1, 张三, 23
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fd05028]
-----------------
到这里,我们可以看到,在调用第二次
getUserByName
函数时,没有再执行select语句,也就直接减少了一次数据库的读取操作。为了可以更好的观察,缓存的存储,我们可以在单元测试中注入
CacheManager
。1
2
private CacheManager cacheManager;使用debug模式运行单元测试,观察
CacheManager
中的缓存集users以及其中的User对象的缓存加深理解。
ConcurrentMap Cache
Spring boot默认使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager来实现缓存
ConcurrentMapCache实质是一个ConcurrentHashMap集合对象java内置,所以无需引入其他依赖,也没有额外的配置
ConcurrentMapCache的自动装配声明在SimpleCacheConfiguration中,如果需要也可对它进行额外的装
1
2
3
4
5
6
7
8
9
10
11
12
13//注册1个id为cacheManager,类型为ConcurrentMapCacheManager的bean
public ConcurrentMapCacheManager cacheManager() {
//实例化ConcurrentMapCacheManager
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
//读取配置文件,如果配置有spring.cache.cache-names=xx,xx,则进行配置cacheNames,默认是没有配置的
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
//调用CacheManagerCustomizers#customize 进行个性化设置,在该方法中是遍历其持有的List
return this.customizerInvoker.customize(cacheManager);
}
缓存提供者
在Spring Boot中通过
@EnableCaching
注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:
- Generic
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- EhCache 2.x
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Simple
Caffeine Cache
- Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代,基于LRU算法实现,支持多种缓存过期策略。
- 具体查看这里 https://www.cnblogs.com/liujinhua306/p/9808500.html
Caffeine参数说明
- initialCapacity=[integer]: 初始的缓存空间大小
- maximumSize=[long]: 缓存的最大条数
- maximumWeight=[long]: 缓存的最大权重
- expireAfterAccess=[duration]: 最后一次写入或访问后经过固定时间过期
- expireAfterWrite=[duration]: 最后一次写入后经过固定时间过期
- refreshAfterWrite=[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存 refreshAfterWrite requires a LoadingCache
- weakKeys: 打开key的弱引用
- weakValues:打开value的弱引用
- softValues:打开value的软引用
- recordStats:开发统计功能
注意
- refreshAfterWrite必须实现LoadingCache,跟expire的区别是,指定时间过后,expire是remove该key,下次访问是同步去获取返回新值,而refresh则是指定时间后,不会remove该key,下次访问会触发刷新,新值没有回来时返回旧值
- expireAfterWrite和expireAfterAccess同事存在时,以expireAfterWrite为准。
- maximumSize和maximumWeight不可以同时使用
- weakValues和softValues不可以同时使用
导入依赖
1 | <!-- 使用 caffeine https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine --> |
Caffeine配置
通过配置文件来设置Caffeine
1
2
3
4
5
6spring:
cache:
cache-names: outLimit,notOutLimit
caffeine:
spec: initialCapacity=50,maximumSize=500,expireAfterWrite=5s,refreshAfterWrite=7s #
type: caffeine通过bean装配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public CacheManager cacheManagerWithCaffeine() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
Caffeine caffeine = Caffeine.newBuilder()
.initialCapacity(100) //cache的初始容量值
.maximumSize(1000) //maximumSize用来控制cache的最大缓存数量,maximumSize和maximumWeight不可以同时使用,
.maximumWeight(100) //控制最大权重
.expireAfter(customExpireAfter) //自定义过期
.refreshAfterWrite(5, TimeUnit.SECONDS); //使用refreshAfterWrite必须要设置cacheLoader
cacheManager.setCaffeine(caffeine);
cacheManager.setCacheLoader(cacheLoader); //缓存加载方案
cacheManager.setCacheNames(getNames()); //缓存名称列表
cacheManager.setAllowNullValues(false);
return cacheManager;
}配置文件结合Bean装配
1
2
3
4
5
6
7
8
9
10
11
private String caffeineSpec;
public CacheManager cacheManagerWithCaffeineFromSpec(){
CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
Caffeine caffeine = Caffeine.from(spec); // 或使用 Caffeine caffeine = Caffeine.from(caffeineSpec);
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeine);
cacheManager.setCacheNames(getNames());
return cacheManager;
}
实现cacheLoader
CacheLoader是cache的一种加载策略,key不存在或者key过期之类的都可以通过CacheLoader来自定义获得/重新获得数据。
使用refreshAfterWrite必须要设置cacheLoader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CacheConfig {
public CacheLoader<Object, Object> cacheLoader() {
CacheLoader<Object, Object> cacheLoader = new CacheLoader<Object, Object>() {
public Object load(Object key) throws Exception {
return null;
}
// 达到配置文件中的refreshAfterWrite所指定的时候回处罚这个事件方法
public Object reload(Object key, Object oldValue) throws Exception {
return oldValue; //可以在这里处理重新加载策略,本例子,没有处理重新加载,只是返回旧值。
}
};
return cacheLoader;
}
}CacheLoader实质是一个监听,处上述load与reload还包含,expireAfterCreate,expireAfterUpdate,expireAfterRead等可以很灵活的配置CacheLoade
具体详细配置可查看这里 https://www.jianshu.com/p/15d0a9ce37dd
EhCache
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认CacheProvider。
Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。
它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
接下来我们通过下面的几步操作,就可以轻松的把上面的缓存应用改成使用ehcache缓存管理。
引入ehcache依赖
在
pom.xml
中引入ehcache依赖, 在Spring Boot的parent管理下,不需要指定具体版本,会自动采用Spring Boot中指定的版本号。1
2
3
4<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
加入配置
配置文件
1
2
3spring.cache.type=EHCACHE # 配置ehcache缓存
# 指定ehcache配置文件路径 ,可以不用写,因为默认就是这个路径,SpringBoot会自动扫描
spring.cache.ehcache.config=classpath:/ehcache.xml在
src/main/resources
目录下创建:ehcache.xml
, ehcache.xml只需要放到类路径下面,SpringBoot会自动扫描1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<!--
磁盘存储:指定一个文件目录,当EHCache把数据写到硬盘上时,将把数据写到这个文件目录下
path:指定在硬盘上存储对象的路径
path可以配置的目录有:
user.home(用户的家目录)
user.dir(用户当前的工作目录)
java.io.tmpdir(默认的临时目录)
ehcache.disk.store.dir(ehcache的配置目录)
绝对路径(如:d:\\ehcache)
查看路径方法:String tmpDir = System.getProperty("java.io.tmpdir");
-->
<diskStore path="java.io.tmpdir" />
<!--
defaultCache:默认的缓存配置信息,如果不加特殊说明,则所有对象按照此配置项处理
maxElementsInMemory:设置了缓存的上限,最多存储多少个记录对象
eternal:代表对象是否永不过期 (指定true则下面两项配置需为0无限期)
timeToIdleSeconds:最大的发呆时间 /秒
timeToLiveSeconds:最大的存活时间 /秒
overflowToDisk:是否允许对象被写入到磁盘
说明:下列配置自缓存建立起600秒(10分钟)有效 。
在有效的600秒(10分钟)内,如果连续120秒(2分钟)未访问缓存,则缓存失效。
就算有访问,也只会存活600秒。
-->
<defaultCache maxElementsInMemory="10000" eternal="false"
timeToIdleSeconds="600" timeToLiveSeconds="600" overflowToDisk="true" />
<!-- 按缓存名称的不同管理策略 -->
<cache name="users" maxElementsInMemory="10000" eternal="false"
timeToIdleSeconds="120" timeToLiveSeconds="600" overflowToDisk="true" />
</ehcache>
装配
SpringBoot会为我们自动配置
EhCacheCacheManager
这个Bean,如果想自定义设置一些个性化参数时,通过Java Config形式配置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CacheConfig {
public CacheManager cacheManager() {
return new EhCacheCacheManager(ehCacheCacheManager().getObject());
}
public EhCacheManagerFactoryBean ehCacheCacheManager() {
EhCacheManagerFactoryBean cmfb = new EhCacheManagerFactoryBean();
cmfb.setConfigLocation(new ClassPathResource("ehcache.xml"));
cmfb.setShared(true);
return cmfb;
}
}
测试
执行测试代码,debug模式下可以看到采用EhCacheCacheManager,第二次查询不走数据库
Redis
Redis 优势
- 分布式
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性
导入依赖
就只需要这一个依赖!不需要
spring-boot-starter-cache
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>导入这一个依赖时,SpringBoot的CacheManager就会使用RedisCache。
Redis使用模式使用pool2连接池,在需要时引用下面的依赖
1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
</dependency>
配置Redis
1 | spring.redis.database=1 # Redis数据库索引(默认为0) |
装配
- 如果需要自定义缓存配置可以通过,继承CachingConfigurerSupport类,手动装配,如果一切使用默认配置可不必
序列化类型
1 |
|
过期时间
配置文件方式
1
2
3
4
5
6
7spring:
cache:
type: REDIS
ehcache:
config: classpath:/ehcache.xml
redis:
time-to-live: 20000 #缓存超时时间ms代码配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* 通过RedisCacheManager配置过期时间
*
* @param redisConnectionFactory
* @return
*/
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)); // 设置缓存有效期一小时
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
完整的装配类
1 | package tk.fulsun.demo.config; |
Redis编程模板
除了使用注解,Spring boot集成 Redis 客户端jedis。封装Redis 连接池,以及操作模板,可以方便的显示的在方法的代码中处理缓存对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private StringRedisTemplate stringRedisTemplate;//操作key-value都是字符串
private RedisTemplate redisTemplate;//操作key-value都是对象
private RedisCacheManager redisCacheManager;
/**
* Redis常见的五大数据类型:
* stringRedisTemplate.opsForValue();[String(字符串)]
* stringRedisTemplate.opsForList();[List(列表)]
* stringRedisTemplate.opsForSet();[Set(集合)]
* stringRedisTemplate.opsForHash();[Hash(散列)]
* stringRedisTemplate.opsForZSet();[ZSet(有序集合)]
*/
public void test(){
stringRedisTemplate.opsForValue().append("msg","hello");
Cache emp = redisCacheManager.getCache("emp");
emp.put("111", "222");
}