摘要:本文介绍了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.xml1 2 3 4 5 6
| <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 配置方式
创建服务类:
java1 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.xml1 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>
|
创建启动类:
java1 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标签。
创建配置类:
java1 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; } }
|
修改启动类:
java1 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注解开启基于注解的缓存。
条