抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

摘要:本文介绍了Spring的缓存管理。

环境

Windows 10 企业版 LTSC 21H2
Java 1.8
Maven 3.6.3
Spring 5.3.23

1 概述

1.1 定义

缓存是一种存储数据的临时空间,用于提高数据访问速度。在应用程序中,缓存通常用于存储频繁访问的数据,以减少数据库查询或计算开销。

Spring提供了基于注解的缓存抽象,旨在简化缓存的配置和使用,支持适配多种缓存实现。核心思想是将缓存逻辑与业务逻辑分离,通过注解标记需要缓存的方法,基于AOP机制在方法执行前后进行缓存操作,实现透明的缓存管理。

1.2 特点

缓存的特点:

  • 提高性能:减少数据库查询或计算时间。
  • 降低负载:减少数据库或其他服务的压力。
  • 增强可靠性:在服务暂时不可用时仍能提供数据。

1.3 类型

缓存的类型:

  • 内存缓存:ConcurrentHashMap、Caffeine
  • 分布式缓存:Redis、Memcached
  • 磁盘缓存:EhCache

2 配置依赖

添加依赖:

pom.xml
1
2
3
4
5
6
<!-- Spring Context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>

3 核心接口

3.1 Cache

Cache接口代表缓存实例,负责管理缓存数据,包括获取、存储、删除等。

不同的缓存实现都需要实现Cache接口。

常用实现类:

  • ConcurrentHashMapCache:基于ConcurrentHashMap实现的缓存,适用于单节点应用。
  • CaffeineCache:基于Caffeine实现的缓存,适用于高并发场景。
  • RedisCache:基于Redis实现的缓存,适用于分布式应用。
  • MemcachedCache:基于Memcached实现的缓存,适用于高并发场景。
  • EhCacheCache:基于EhCache实现的缓存,适用于本地缓存。

3.2 CacheManager

CacheManager接口代表缓存管理器,负责管理缓存实例。

不同的缓存实现都需要实现CacheManager接口。

常用实现类:

  • ConcurrentMapCacheManager:基于ConcurrentHashMap实现的缓存管理器,适用于单节点应用。
  • CaffeineCacheManager:基于Caffeine实现的缓存管理器,适用于高并发场景。
  • RedisCacheManager:基于Redis实现的缓存管理器,适用于分布式应用。
  • MemcachedCacheManager:基于Memcached实现的缓存管理器,适用于高并发场景。
  • EhCacheCacheManager:基于EhCache实现的缓存管理器,适用于本地缓存。

4 常用注解

4.1 @Cacheable

@Cacheable注解用于方法级别,将方法的返回值缓存起来。当再次调用该方法时,先检查缓存中是否存在结果,如果存在则直接返回缓存值,否则执行方法体并将结果缓存起来。

常用属性:

属性名 作用 取值
value或cacheNames 指定缓存列表名称,可以缓存到多个列表 字符串
key 指定缓存键,支持SpEL表达式 SpEL表达式
condition 指定缓存条件,支持SpEL表达式,只有满足条件时才缓存 SpEL表达式
unless 指定缓存条件,支持SpEL表达式,只有不满足条件时才缓存 SpEL表达式
sync 指定是否同步缓存操作,适用于高并发场景,可以防止缓存击穿 默认为false表示支持多个线程同时操作,设置为true表示只能一个线程同时操作

4.2 @CachePut

@CachePut注解用于方法级别,将方法的返回值更新到缓存中。无论缓存中是否存在该值,都将执行方法体并将结果更新到缓存中。

常用属性:

属性名 作用 取值
value或cacheNames 指定缓存列表名称,可以缓存到多个列表 字符串
key 指定缓存键,支持SpEL表达式 SpEL表达式
condition 指定缓存条件,支持SpEL表达式,只有满足条件时才缓存 SpEL表达式
unless 指定缓存条件,支持SpEL表达式,只有不满足条件时才缓存 SpEL表达式

4.3 @CacheEvict

@CacheEvict注解用于方法级别,将缓存中指定的键对应的值清除。

常用属性:

属性名 作用 取值
value或cacheNames 指定缓存列表名称,可以缓存到多个列表 字符串
key 指定缓存键,支持SpEL表达式 SpEL表达式
condition 指定缓存条件,支持SpEL表达式,只有满足条件时才缓存 SpEL表达式
unless 指定缓存条件,支持SpEL表达式,只有不满足条件时才缓存 SpEL表达式

4.4 @Caching

@Caching注解用于方法级别,可以组合多个缓存操作,适用于需要同时执行多个缓存操作的方法。

4.5 @CacheConfig

@CacheConfig注解用于类级别,指定该类中所有缓存操作的默认配置。

常用属性:

属性名 作用 取值
cacheNames 指定缓存列表名称,可以缓存到多个列表 字符串数组
cacheManager 指定缓存管理器 对象名称

5 配置方式

创建服务类:

java
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
@Service
public class UserService {
// 模拟数据库操作
private Map<Integer, User> userMap = new HashMap<>();
// 初始化数据
public UserService() {
userMap.put(1, new User(1, "张三"));
userMap.put(2, new User(2, "李四"));
}
// 获取用户,使用缓存
@Cacheable(value = "users", key = "#id")
public User findById(Integer id) {
System.out.println("数据库获取用户");
return userMap.get(id);
}
// 更新用户,更新缓存
@CachePut(value = "users", key = "#user.id")
public User updateById(User user) {
userMap.put(user.getId(), user);
return user;
}
// 删除用户,清除缓存
@CacheEvict(value = "users", key = "#id")
public void deleteById(Integer id) {
userMap.remove(id);
}
}

5.1 半注解配置

spring.xml配置文件中使用cache:annotation-driven标签启用缓存注解,需要添加cache命名空间。

创建配置文件:

spring.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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- 启用注解扫描 -->
<context:component-scan base-package="com.example"/>

<!-- 缓存管理器 -->
<bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager">
<!-- 缓存名称 -->
<property name="cacheNames">
<list>
<!-- 定义用户缓存列表 -->
<value>users</value>
</list>
</property>
</bean>

<!-- 启用缓存注解 -->
<cache:annotation-driven cache-manager="cacheManager"/>
</beans>

创建启动类:

java
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
public class DemoApplication {
public static void main(String[] args) {
// 加载配置文件
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
// 获取服务对象
UserService userService = context.getBean(UserService.class);
// 第一次查询用户,从数据库获取
System.out.println("第一次查询用户:");
System.out.println("第一次查询用户:" + userService.findById(1));
// 第二次查询用户,从缓存获取
System.out.println("第二次查询用户:");
System.out.println("第二次查询用户:" + userService.findById(1));
// 更新用户
System.out.println("更新用户:");
userService.updateById(new User(1, "王五"));
// 再次查询用户
System.out.println("查询更新的用户:" + userService.findById(1));
// 删除用户
System.out.println("删除用户:");
userService.deleteById(1);
// 再次查询用户
System.out.println("查询删除的用户:" + userService.findById(1));
// 关闭容器
context.close();
}
}

5.2 全注解配置

使用@EnableCaching注解启用缓存注解,代替在XML配置文件中使用cache:annotation-driven标签。

创建配置类:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableCaching
@ComponentScan("com.example")
public class DemoConfig {
// 缓存管理器
@Bean
public CacheManager cacheManager() {
// 创建缓存管理器
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
// 设置缓存名称
cacheManager.setCacheNames(Arrays.asList("users"));
// 返回缓存管理器
return cacheManager;
}
}

修改启动类:

java
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
public class DemoApplication {
public static void main(String[] args) {
// 加载配置类
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DemoConfig.class);
// 获取服务对象
UserService userService = context.getBean(UserService.class);
// 第一次查询用户,从数据库获取
System.out.println("第一次查询用户:");
System.out.println("第一次查询用户:" + userService.findById(1));
// 第二次查询用户,从缓存获取
System.out.println("第二次查询用户:");
System.out.println("第二次查询用户:" + userService.findById(1));
// 更新用户
System.out.println("更新用户:");
userService.updateById(new User(1, "王五"));
// 再次查询用户
System.out.println("查询更新的用户:" + userService.findById(1));
// 删除用户
System.out.println("删除用户:");
userService.deleteById(1);
// 再次查询用户
System.out.println("查询删除的用户:" + userService.findById(1));
// 关闭容器
context.close();
}
}

6 常见问题

6.1 缓存穿透

缓存穿透是指查询不存在的数据,导致每次都访问数据库。

解决方案:

  • 缓存空对象,设置较短的过期时间,避免后续请求继续访问数据库。
  • 使用布隆过滤器过滤不存在的数据,避免查询数据库。

6.2 缓存雪崩

缓存雪崩是指大量缓存同时过期,导致数据库压力骤增。

解决方案:

  • 为缓存设置随机过期时间,避免缓存同时过期。
  • 使用缓存预热技术,在缓存过期前提前加载数据到缓存中。

6.3 缓存击穿

缓存击穿是指热点数据过期,导致大量请求同时访问数据库。

解决方案:

  • 使用@Cacheable(sync = true)进行本地同步。
  • 使用线程锁保护数据库,确保在缓存过期时只有一个线程去查询数据库,其他线程等待结果返回。

6.4 缓存失效

缓存失效情况:

  • 只有外部的方法调用被AOP拦截才会缓存,同一个类内部的方法调用不会缓存。
  • 使用缓存注解标记的方法必须是public方法。
  • 使用缓存注解必须在配置类中使用@EnableCaching注解开启基于注解的缓存。

评论