Spring Cache
本文最后更新于:7 个月前
Spring Cache
是Spring Framework
中提供的缓存抽象机制,它允许开发者通过注解或XML
配置简化缓存的使用。Spring Cache
支持多种缓存实现,包括Ehcache
、Caffeine
、Redis
、Guava
等。下面将对Spring Cache
展开详细的介绍。
缓存
为什么使用缓存
- 性能提升:缓存主要用于减少重复的数据处理和数据检索操作,特别是减少对数据库的直接访问,从而加快数据的访问速度,提高应用程序的响应性能。
- 减少成本:通过减少对后端系统如数据库的请求,缓存可以帮助降低运营成本。尤其是在高流量的应用中,适当的缓存策略可以显著减少需要的资源和相关的成本。
- 提高可伸缩性:缓存可以帮助应对高并发场景,通过缓存常用数据来避免在高流量下数据库成为瓶颈。
Java 缓存管理痛点
- 缺乏统一标准:在
Spring Cache
出现之前,Java应用通常需要依赖于具体的缓存实现(如Ehcache
、Guava Cache
等)或自定义解决方案。这些实现各自有自己的API和配置方式,缺乏统一标准,使得在不同项目或组件间共享缓存逻辑变得复杂。 - 集成复杂性:不同的缓存解决方案需要不同的集成策略,对开发者来说,这意味着需要对每一种技术都有深入的理解。这种复杂性增加了开发和维护的难度,也提高了学习成本。
- 功能局限:许多早期的缓存库专注于缓存数据,但缺乏高级功能,如声明式的缓存操作、自动的缓存刷新和条件缓存等。
Spring Cache
为了解决Java缓存管理中存在的多种问题,提供一个简单、统一且功能强大的缓存管理解决方案,以适应现代应用对性能和可伸缩性的高要求,Spring Cache
诞生了,带来的改变包括:
- 统一的抽象层:
Spring Cache
提供了一个清晰的缓存抽象层,使得开发者可以不关心具体的缓存实现细节,统一使用缓存注解来控制缓存行为。 - 简化配置和使用:通过
Spring
的配置和注解支持,开发者可以很容易地配置和使用缓存,而无需编写大量的模板代码。这降低了使用缓存的门槛,提高了开发效率。 - 灵活性和扩展性:
Spring Cache
不仅支持多种流行的缓存技术,还允许通过自定义CacheManager
来扩展新的缓存实现,提供了极大的灵活性。 - 与Spring生态系统的整合:
Spring Cache
的引入使得缓存策略能够更好地与Spring
的其他项目(如Spring Data
、Spring Security
)整合,提供一致的开发体验。
核心概念
缓存抽象
Spring Cache
通过一组接口来定义缓存的操作,主要包括存储、获取、更新和清除缓存数据。这种抽象层让开发者可以不用关心具体的缓存实现细节,比如使用内存缓存、文件缓存或是分布式缓存系统。
注解
Spring Cache
提供以下几种注解来简化缓存操作的实现:
- **
@Cacheable
**:最常用的注解,用于表示某个方法的返回值是可缓存的。如果缓存中已有相应的值,则方法不会执行,直接从缓存返回数据。 - **
@CacheEvict
**:用于清除缓存数据,支持在方法执行前或执行后清除。 - **
@CachePut
**:总是会执行方法,并将结果放入指定的缓存。这常用于更新缓存数据。 - **
@Caching
**:复合注解,可以同时应用多个其他缓存注解。
缓存解析和存储
Spring Cache
的工作方式是拦截基于注解的方法调用,根据方法的参数和注解的配置决定如何缓存方法的返回值或如何操作缓存。
工作原理
方法拦截
Spring
使用代理模式或AOP
拦截那些被@Cacheable
、@CacheEvict
等注解标记的方法。- 当调用这些方法时,
Spring Cache
的拦截器会介入处理。
缓存键生成
- 默认情况下,
Spring Cache
使用方法的参数生成缓存键。开发者可以通过表达式自定义键值的生成策略。 - 对于
@Cacheable
和@CachePut
注解,如果缓存中存在相应的键,则可以直接返回缓存中的数据或更新数据。
缓存命中
- 当缓存命中时,即缓存中已存在对应的键值,对于
@Cacheable
注解的方法,直接从缓存返回数据,不执行方法体。 - 对于
@CachePut
注解,方法体将被执行,且新的返回值将替换缓存中的旧值。
缓存更新和失效
@CacheEvict
可以用来在适当的时候清除缓存,保持缓存数据的新鲜度。可以配置为方法执行前或执行后清除缓存。@CachePut
用于更新缓存中的数据,确保缓存的数据是最新的。
缓存管理
Spring
提供了CacheManager
接口来管理不同的缓存库。每个缓存库可以配置不同的存储和过期策略。
实现方案
创建一个基础的SpringBoot
项目,添加spring-boot-starter-cache
依赖
1 |
|
启动类中添加@EnableCaching
注解
1 |
|
EHCache
EHCache
是一种广泛使用的开源Java缓存库,适用于提升应用程序的性能,通过减少数据库访问、提供快速的访问速度来减轻后端系统的负载。
概念
内存和磁盘存储:
EHCache
提供内存和磁盘两层存储机制,可以配置为仅内存、内存加磁盘持久化或仅磁盘存储。内存存储提供快速访问,而磁盘存储则支持更大的缓存容量和重启后的数据恢复。
数据元素:
- 在
EHCache
中,每个缓存条目称为一个元素(Element
),每个元素有一个键(Key
)和一个值(Value
)。
- 在
缓存淘汰机制:
- 支持多种缓存淘汰策略,包括最少使用(
LRU
)、最少访问(LFU
)和先进先出(FIFO
)。
- 支持多种缓存淘汰策略,包括最少使用(
实现
SpringBoot
配置文件中设置SpringCache
的类型设置为ehcache
1
spring.cache.type=ehcache
添加Maven依赖
1
2
3
4
5<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.9.2</version>
</dependency>在资源目录下创建
ehcache.xml
配置文件,定义缓存的配置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
35
36
37
38
39<ehcache>
<!-- 配置磁盘存储的路径 -->
<diskStore path="java.io.tmpdir"/>
<!--
1. maxElementsInMemory: 内存中缓存的最大条目数
2. eternal: 是否永不过期
3. timeToIdleSeconds: 元素在缓存中可以处于空闲状态的最大时间(秒),超过此时间未被访问的元素将被淘汰
4. timeToLiveSeconds: 元素在缓存中可以存活的最大时间(秒),从元素被存入缓存开始计算
5. overflowToDisk: 是否允许溢出到磁盘
6. diskPersistent: 是否磁盘持久化
7. diskExpiryThreadIntervalSeconds: 磁盘过期检查线程运行间隔(秒)
8. memoryStoreEvictionPolicy: 内存存储淘汰策略(LRU最近最少使用、LFU最不频繁使用、FIFO先进先出)
-->
<!-- 默认的缓存配置 -->
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
<!-- 定义名为 codeCache 的缓存 -->
<cache name="codeCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"
/>
</ehcache>新增配置类
EhCacheConfig
,读取配置文件ehcache.xml
中的配置1
2
3
4
5
6
7
8
9
10
11@Configuration
public class EhCacheConfig {
@Bean
public CacheManager cacheManager() throws IOException {
net.sf.ehcache.CacheManager ehCacheManager =
new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());
return new EhCacheCacheManager(ehCacheManager);
}
}本次用于测试的
CodeService
,含有属性codeMap
,以及对应的增删改查方法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@Service
public class CodeService {
private static final Logger logger = LoggerFactory.getLogger(CodeService.class);
private final Map<Integer, String> codeMap = new HashMap<>();
@Cacheable(value = "codeCache", keyGenerator = "customCacheKeyGenerator")
public String getCode(int id) {
logger.info("getCode开始执行,id: {}", id);
return codeMap.get(id);
}
@CachePut(value = "codeCache", keyGenerator = "customCacheKeyGenerator")
public String addOrUpdateCode(int id, String description) {
logger.info("addOrUpdateCode开始执行,id: {},description: {}", id, description);
codeMap.put(id, description);
return description;
}
@CacheEvict(value = "codeCache", keyGenerator = "customCacheKeyGenerator")
public void deleteCode(int id) {
logger.info("deleteCode开始执行,id: {}", id);
codeMap.remove(id);
}
@CacheEvict(value = "codeCache", allEntries = true)
public void clearCache() {
// 这里不需要操作codeMap,@CacheEvict注解已经处理了缓存清空
}
}因为缓存是存在于内存或磁盘中的,为了避免缓存的键重复,可以在
Spring Cache
的注解中自定义键的生成策略,如以方法参数id
为键1
@Cacheable(value = "codeCache", key = "#id")
还可以通过实现
KeyGenerator
接口,对这个生成策略进行自定义、动态地生成键,且这种方式也能支持复用。例如创建一个CustomCacheKeyGenerator
,生成策略为simpleName::params[0]
1
2
3
4
5
6
7@Component("customCacheKeyGenerator")
public class CustomCacheKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "::" + params[0];
}
}Spring Cache
注解中指定使用刚才自定义的CustomCacheKeyGenerator
1
@Cacheable(value = "codeCache", keyGenerator = "customCacheKeyGenerator")
将
Spring Cache
的日志级别更改为trace
,以便观察日志信息1
logging.level.org.springframework.cache=trace
单元测试如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@Test
public void codeServiceTest1() {
logger.info("初始化数据,调用addOrUpdateCode方法,设置id=1的值为one");
codeService.addOrUpdateCode(1, "one");
logger.info("初始化后第一次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("初始化后第二次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("调用deleteCode方法");
codeService.deleteCode(1);
logger.info("deleteCode后第一次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("deleteCode后第二次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("第一次调用id=2的getCode方法:{}", codeService.getCode(2));
logger.info("第二次调用id=2的getCode方法:{}", codeService.getCode(2));
logger.info("调用clearCache方法");
codeService.clearCache();
logger.info("clearCache后第一次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("clearCache后第一次调用id=2的getCode方法:{}", codeService.getCode(2));
logger.info("调用addOrUpdateCode方法,设置id=1的值为one");
codeService.addOrUpdateCode(1, "one");
}控制台输出日志如下
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
35
36
37
38
39
40
41
42
43
442024-07-15 00:41:05.629 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 初始化数据,调用addOrUpdateCode方法,设置id=1的值为one
2024-07-15 00:41:05.637 INFO 39528 --- [ main] s.y.springcache.service.CodeService : addOrUpdateCode开始执行,id: 1,description: one
2024-07-15 00:41:05.638 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.addOrUpdateCode(int,java.lang.String)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless=''
2024-07-15 00:41:05.641 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.642 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::1' found in cache 'codeCache'
2024-07-15 00:41:05.642 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 初始化后第一次调用id=1的getCode方法:one
2024-07-15 00:41:05.642 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.642 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::1' found in cache 'codeCache'
2024-07-15 00:41:05.642 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 初始化后第二次调用id=1的getCode方法:one
2024-07-15 00:41:05.642 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 调用deleteCode方法
2024-07-15 00:41:05.642 INFO 39528 --- [ main] s.y.springcache.service.CodeService : deleteCode开始执行,id: 1
2024-07-15 00:41:05.642 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public void space.yangtao.springcache.service.CodeService.deleteCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='',false,false
2024-07-15 00:41:05.642 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Invalidating cache key [CodeService::1] for operation Builder[public void space.yangtao.springcache.service.CodeService.deleteCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='',false,false on method public void space.yangtao.springcache.service.CodeService.deleteCode(int)
2024-07-15 00:41:05.642 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::1' in cache(s) [codeCache]
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.644 INFO 39528 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 1
2024-07-15 00:41:05.644 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : deleteCode后第一次调用id=1的getCode方法:null
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::1' found in cache 'codeCache'
2024-07-15 00:41:05.644 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : deleteCode后第二次调用id=1的getCode方法:null
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::2' in cache(s) [codeCache]
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.644 INFO 39528 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 2
2024-07-15 00:41:05.644 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 第一次调用id=2的getCode方法:null
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.644 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::2' found in cache 'codeCache'
2024-07-15 00:41:05.644 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 第二次调用id=2的getCode方法:null
2024-07-15 00:41:05.645 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 调用clearCache方法
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Invalidating entire cache for operation Builder[public void space.yangtao.springcache.service.CodeService.clearCache()] caches=[codeCache] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='',true,false on method public void space.yangtao.springcache.service.CodeService.clearCache()
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::1' in cache(s) [codeCache]
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.645 INFO 39528 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 1
2024-07-15 00:41:05.645 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : clearCache后第一次调用id=1的getCode方法:null
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::2' in cache(s) [codeCache]
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:41:05.645 INFO 39528 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 2
2024-07-15 00:41:05.645 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : clearCache后第一次调用id=2的getCode方法:null
2024-07-15 00:41:05.645 INFO 39528 --- [ main] s.y.s.SpringcacheApplicationTests : 调用addOrUpdateCode方法,设置id=1的值为one
2024-07-15 00:41:05.645 INFO 39528 --- [ main] s.y.springcache.service.CodeService : addOrUpdateCode开始执行,id: 1,description: one
2024-07-15 00:41:05.645 TRACE 39528 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.addOrUpdateCode(int,java.lang.String)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless=''分析:
- 初始化数据,调用
addOrUpdateCode
方法,设置id=1
的数据,并存入一个key
为CodeService::1
的缓存 - 初始化后的第一二次调用
getCode
,都从缓存中取到了CodeService::1
的数据 - 调用
deleteCode
方法,这个方法会将缓存CodeService::1
失效 deleteCode
后调用getCode
方法,这次没有获取到CodeService::1
的缓存,因此执行了getCode
方法,并将结果缓存了起来deleteCode
后第二次调用getCode
方法取到了上一次getCode
方法设置的缓存- 第一次调用
getCode(id=2)
的方法,没有CodeService::2
,因此执行了getCode
方法并设置了对应的缓存,第二次调用的时候这个缓存被获取到 - 手动调用
clearCache
方法,清除了所有的缓存,之后调用getCode
方法,id=1
或id=2
都无法获取到对应的缓存CodeService::1
或CodeService::2
- 调用
addOrUpdateCode
方法,重新设置id=1
的数据并重新设置缓存
- 初始化数据,调用
上述EhCache
的缓存,都是在内存或磁盘级别,这就带来了一个问题,在多实例的情况下,同一个方法同一个参数的多次调用可能使用不了缓存
编写第二个测试方法
1 |
|
运行结果如下
1 |
|
分析:运行第二次的测试方法,发现获取不到上次单元测试中最后设置的缓存CodeService::1
,原因是因为之前的单元测试实例停止了,内存中找不到缓存,因此需要引入分布式缓存。分布式缓存支持跨多个实例共享缓存数据,常见的解决方法有Redis
或Memcached
。
Redis
Redis
提供了高性能的数据结构服务,非常适合用作分布式缓存系统。需要注意的是,Redis
作为一个独立的内存数据结构服务器,作为Spring Cache
的后端时,配置选项与使用内存或本地缓存系统时有所不同,通常只配置过期时间TTL
以及序列化方式,而其余参数(如最大使用内存、持久化、淘汰策略等)则由Redis
服务器单独配置。
实现
SpringBoot
配置文件中设置Spring Cache
的类型设置为Redis
、并配置Redis
的连接信息1
2
3
4spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379引入Maven依赖
1
2
3
4<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>配置类
RedisCacheConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存过期时间为10分钟
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
// 不缓存空值
.disableCachingNullValues();
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.build();
}
}其他的配置(如缓存
key
的生成类以及service
方法中注解的使用)与EhCache
中配置一致,保持不变测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19@Test
public void codeServiceTest1() {
logger.info("初始化数据,调用addOrUpdateCode方法,设置id=1的值为one");
codeService.addOrUpdateCode(1, "one");
logger.info("初始化后第一次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("初始化后第二次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("调用deleteCode方法");
codeService.deleteCode(1);
logger.info("deleteCode后第一次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("deleteCode后第二次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("第一次调用id=2的getCode方法:{}", codeService.getCode(2));
logger.info("第二次调用id=2的getCode方法:{}", codeService.getCode(2));
logger.info("调用clearCache方法");
codeService.clearCache();
logger.info("clearCache后第一次调用id=1的getCode方法:{}", codeService.getCode(1));
logger.info("clearCache后第一次调用id=2的getCode方法:{}", codeService.getCode(2));
logger.info("调用addOrUpdateCode方法,设置id=1的值为one");
codeService.addOrUpdateCode(1, "one");
}运行结果如下:
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
35
36
37
38
39
40
41
42
43
442024-07-15 00:54:45.618 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 初始化数据,调用addOrUpdateCode方法,设置id=1的值为one
2024-07-15 00:54:45.630 INFO 27052 --- [ main] s.y.springcache.service.CodeService : addOrUpdateCode开始执行,id: 1,description: one
2024-07-15 00:54:45.632 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.addOrUpdateCode(int,java.lang.String)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless=''
2024-07-15 00:54:46.193 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.200 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::1' found in cache 'codeCache'
2024-07-15 00:54:46.200 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 初始化后第一次调用id=1的getCode方法:one
2024-07-15 00:54:46.200 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.201 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::1' found in cache 'codeCache'
2024-07-15 00:54:46.201 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 初始化后第二次调用id=1的getCode方法:one
2024-07-15 00:54:46.201 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 调用deleteCode方法
2024-07-15 00:54:46.201 INFO 27052 --- [ main] s.y.springcache.service.CodeService : deleteCode开始执行,id: 1
2024-07-15 00:54:46.201 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public void space.yangtao.springcache.service.CodeService.deleteCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='',false,false
2024-07-15 00:54:46.201 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Invalidating cache key [CodeService::1] for operation Builder[public void space.yangtao.springcache.service.CodeService.deleteCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='',false,false on method public void space.yangtao.springcache.service.CodeService.deleteCode(int)
2024-07-15 00:54:46.204 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.205 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::1' in cache(s) [codeCache]
2024-07-15 00:54:46.206 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.206 INFO 27052 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 1
2024-07-15 00:54:46.206 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : deleteCode后第一次调用id=1的getCode方法:null
2024-07-15 00:54:46.207 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.207 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::1' found in cache 'codeCache'
2024-07-15 00:54:46.207 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : deleteCode后第二次调用id=1的getCode方法:null
2024-07-15 00:54:46.207 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.208 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::2' in cache(s) [codeCache]
2024-07-15 00:54:46.208 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.208 INFO 27052 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 2
2024-07-15 00:54:46.208 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 第一次调用id=2的getCode方法:null
2024-07-15 00:54:46.208 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.208 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::2' found in cache 'codeCache'
2024-07-15 00:54:46.208 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 第二次调用id=2的getCode方法:null
2024-07-15 00:54:46.208 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 调用clearCache方法
2024-07-15 00:54:46.210 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Invalidating entire cache for operation Builder[public void space.yangtao.springcache.service.CodeService.clearCache()] caches=[codeCache] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='',true,false on method public void space.yangtao.springcache.service.CodeService.clearCache()
2024-07-15 00:54:46.213 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.215 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::1' in cache(s) [codeCache]
2024-07-15 00:54:46.215 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.215 INFO 27052 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 1
2024-07-15 00:54:46.215 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : clearCache后第一次调用id=1的getCode方法:null
2024-07-15 00:54:46.215 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.216 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'CodeService::2' in cache(s) [codeCache]
2024-07-15 00:54:46.216 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::2' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:54:46.216 INFO 27052 --- [ main] s.y.springcache.service.CodeService : getCode开始执行,id: 2
2024-07-15 00:54:46.216 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : clearCache后第一次调用id=2的getCode方法:null
2024-07-15 00:54:46.216 INFO 27052 --- [ main] s.y.s.SpringcacheApplicationTests : 调用addOrUpdateCode方法,设置id=1的值为one
2024-07-15 00:54:46.216 INFO 27052 --- [ main] s.y.springcache.service.CodeService : addOrUpdateCode开始执行,id: 1,description: one
2024-07-15 00:54:46.217 TRACE 27052 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.addOrUpdateCode(int,java.lang.String)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless=''可以发现与之前
EhCache
使用内存作为缓存方案的测试结果并没有什么差别,这就说明了Spring Cache
提供的缓存抽象层使得开发者可以不关心具体的缓存实现细节此时再运行第二个方法
1
2
3
4
5@Test
public void codeServiceTest2() {
logger.info("实例2开始测试------------------------------------");
logger.info("第二个实例调用id=1的getCode方法:{}", codeService.getCode(1));
}运行结果如下
1
2
3
42024-07-15 00:55:49.319 INFO 30368 --- [ main] s.y.s.SpringcacheApplicationTests : 实例2开始测试------------------------------------
2024-07-15 00:55:49.324 TRACE 30368 --- [ main] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'CodeService::1' for operation Builder[public java.lang.String space.yangtao.springcache.service.CodeService.getCode(int)] caches=[codeCache] | key='' | keyGenerator='customCacheKeyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
2024-07-15 00:55:49.910 TRACE 30368 --- [ main] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'CodeService::1' found in cache 'codeCache'
2024-07-15 00:55:49.910 INFO 30368 --- [ main] s.y.s.SpringcacheApplicationTests : 第二个实例调用id=1的getCode方法:one可以发现这次使用分布式缓存以后,第二次测试也可以获取到之前的缓存
CodeService::1
,因为这个缓存是存在于Redis
数据库中的,查看数据库也可以验证这一点,如下:1
2
3
4
5127.0.0.1:6379> keys *
1) "codeCache::CodeService::2"
2) "codeCache::CodeService::1"
127.0.0.1:6379> get codeCache::CodeService::1
"\xac\xed\x00\x05t\x00\x03one"
注解补充说明
注解
@Caching
注解可以组合多个缓存注解,使得在一个方法上可以同时应用@Cacheable
、@CachePut
和@CacheEvict
注解。这对于需要对一个方法进行多重缓存逻辑处理的情况非常有用。
示例:在更新代码时同时更新缓存并在特定条件下删除另一个缓存条目
1 |
|
@Caching
提供了组合多种缓存操作的能力,在复杂的缓存逻辑中非常有用,但对于简单的缓存需求,使用@Caching
可能反而会引入不必要的复杂性。
注解属性
value/cacheNames
:指定缓存操作的缓存名称。至少需要指定一个缓存名称,因为缓存的目标是根据名称进行存储和检索。示例:@Cacheable(value = "myCache")
key
:使用Spring Expression Language (SpEL)
表达式指定缓存的键。如果不指定,则默认使用所有的方法参数组合作为键。示例:@Cacheable(value = "myCache", key = "#id")
keyGenerator
:指定用来生成缓存键的自定义KeyGenerator
bean 的名称。如果设置了这个属性,那么key
属性应当忽略。示例:@Cacheable(value = "myCache", keyGenerator = "myCustomKeyGenerator")
cacheManager
:指定用来管理缓存操作的CacheManager
bean的名称。只在你配置了多个CacheManager
的情况下需要使用。示例:@Cacheable(value = "myCache", cacheManager = "myCacheManager")
cacheResolver
:指定用来解析使用哪个缓存的CacheResolver
bean的名称。如果设置了这个属性,cacheManager
属性应当忽略。示例:@Cacheable(value = "myCache", cacheResolver = "myCacheResolver")
condition
:一个SpEL
表达式,当表达式为true时,方法返回值会被缓存。这可以用来在某些条件下才缓存结果。示例:@Cacheable(value = "myCache", condition = "#id > 10")
unless
:一个SpEL
表达式,当表达式为true时,即使方法被缓存拦截,返回值也不会被放入缓存。这通常用于防止将不想缓存的数据放入缓存。示例:@Cacheable(value = "myCache", unless = "#result == null")
sync
:指定是否使用同步模式。如果多个线程同时访问一个计算量大的缓存方法,只有一个线程将执行方法,其他线程将等待这个结果被计算出来,避免多次执行相同的方法。示例:@Cacheable(value = "myCache", sync = true)
allEntries
:指定是否需要从缓存中移除所有条目。设置为true
时,不考虑key
属性的设置,将清空整个缓存。示例:@CacheEvict(value = "myCache", allEntries = true)
beforeInvocation
:指定缓存清除操作是在方法执行之前还是之后进行,默认情况下为false,即方法执行结束前不清除缓存,常用于可能抛出异常的方法(先清除缓存再执行方法,确保无论数据操作成功与否,缓存都不会返回旧数据)。示例:@CacheEvict(value = "myCache", beforeInvocation = true)
缓存管理
cacheManager
定义:cacheManager
属性用于指定管理缓存的CacheManager
实例。在Spring应用中,CacheManager
负责创建和维护缓存实例。
用途:当应用配置了多个CacheManager
时,可以通过cacheManager
属性明确指定某个方法或类使用哪一个CacheManager
。这是通常在有多种缓存存储(如内存、Redis、EhCache等)时需要考虑的情况。
示例:使用cacheManager
属性指定缓存管理器。
1 |
|
cacheResolver
定义:cacheResolver
属性用于指定一个CacheResolver
实例,CacheResolver
负责在运行时解析使用哪个缓存。它提供了一种更灵活的方式来动态决定缓存。
用途:cacheResolver
在需要根据特定条件动态决定使用哪个缓存时非常有用,例如,基于方法的输入参数或者其他运行时数据。这使得它在复杂逻辑或动态环境中尤其有用。
示例:使用cacheResolver
动态解析缓存。
定义一个动态解析器
CodeDynamicCacheResolver
,这个解析器重写了获取缓存的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class CodeDynamicCacheResolver extends SimpleCacheResolver implements CacheResolver {
public CodeDynamicCacheResolver(CacheManager cacheManager) {
super(cacheManager);
}
@Override
protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> context) {
// 假设第一方法的第一个参数都是id
int key = (int) context.getArgs()[0];
if (key > 10000) {
return Collections.singleton("code::big");
} else {
return Collections.singleton("code::small");
}
}
}Spring中定义
codeDynamicCacheResolver
这个Bean1
2
3
4@Bean
public CodeDynamicCacheResolver codeDynamicCacheResolver(CacheManager cacheManager) {
return new CodeDynamicCacheResolver(cacheManager);
}在
Spring Cache
注解中使用1
@Cacheable(cacheResolver = "codeDynamicCacheResolver")
总结
cacheManager
用于静态地指定使用哪个缓存管理器,通常在应用启动时配置好,之后不会改变。而cacheResolver
提供了一种动态决定使用哪个缓存的机制,它允许开发者在运行时基于复杂的业务逻辑来选择缓存。选择cacheManager
还是cacheResolver
取决于应用的具体需求和缓存策略的复杂程度。在多缓存管理器场景下,简单的配置可以使用cacheManager
;而在需要高度动态和可定制的缓存解决方案中,cacheResolver
提供了必要的灵活性。
服务器缓存总结
Java服务器端的缓存可以大致分为几种类型,每种类型适用于不同的场景,并具有不同的实现方式,具体的选择以及实施方案需根据应用场景和需求进行具体分析再确定。
缓存类型
本地缓存
- 定义:存储在单个服务实例的内存中的缓存。
常用实现:
EHCache
、Caffeine
。适用场景:适合非分布式或服务实例不需要共享缓存的场景。
分布式缓存:
定义:缓存数据在多个服务实例间共享,通常独立于服务实例运行。
常用实现:
Redis
、Memcached
。适用场景:适用于需要缓存共享、高可用性和扩展性的分布式系统。
数据库缓存:
定义:缓存直接与数据库交互,通常用于缓存查询结果。
常用实现:
MyBatis
缓存、Hibernate
二级缓存。适用场景:适用于减少数据库负载、提高查询效率。
实现方式
EHCache
特点:支持内存和磁盘存储,可配置为本地缓存。
优势:易于集成,适用于单体应用或服务的本地缓存,支持复杂的缓存策略。
劣势:在分布式环境中没有内建的数据一致性支持。
Redis
特点:高性能键值存储数据库,支持数据结构的多样性。
优势:支持持久化,数据可以在多个节点间复制,支持事务和高级数据结构,适合做分布式缓存。
劣势:如果未正确配置,持久化和数据复制可能会影响性能。
Memcached
特点:高性能的分布式内存对象缓存系统。
优势:简单易用,适用于减轻数据库负载,提高读取速度。
劣势:不支持持久化,缓存数据在重启后丢失,不支持复杂的数据类型。
MyBatis/Hibernate 缓存
特点:集成在
ORM
框架中,自动处理SQL
级别的缓存。优势:无需手动处理缓存逻辑,适合缓存数据库查询结果。
劣势:控制较少,依赖于
ORM
框架的实现和配置。
总结
本文详细介绍了Spring Cache
的工作原理和实践应用,展示了其如何帮助开发者优化应用性能和扩展性。随着技术的不断进步,Spring Cache
也在不断地进化,为开发者提供更多的功能和更好的集成。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!