芋道源码 ------ Spring Boot 缓存 Cache 入门
一、概述
(一)缓存的必要性
随着系统访问量的增加,数据库往往成为性能瓶颈。为了减少数据库的压力,提高系统的响应速度,我们可以通过缓存来优化系统性能。
(二)缓存策略
常见的缓存策略包括:
- 读写分离:将读操作分流到从节点,避免主节点压力过多。
- 分库分表:将读写操作分摊到多个节点,避免单节点压力过多。
- 缓存系统:使用缓存系统(如 Redis、Ehcache)来存储经常访问的数据,减少对数据库的直接访问。
(三)Spring Cache
Spring 3.1 引入了基于注解的缓存技术,通过 @Cacheable
等注解简化缓存逻辑,支持多种缓存实现。Spring Cache 的特点包括:
- 通过少量的配置注解即可使得既有代码支持缓存。
- 支持开箱即用,无需安装和部署额外第三方组件即可使用缓存。
- 支持 Spring 表达式语言(SpEL),能使用对象的任何属性或者方法来定义缓存的 key 和 condition。
- 支持 AspectJ,并通过其实现任何方法的缓存支持。
- 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性。
二、注解
(一)@Cacheable
@Cacheable
注解用于方法上,缓存方法的执行结果。执行过程如下:
- 判断方法执行结果的缓存。如果有,则直接返回该缓存结果。
- 执行方法,获得方法结果。
- 根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
- 返回方法结果。
常用属性:
cacheNames
:缓存名。必填。[]
数组,可以填写多个缓存名。key
:缓存的 key 。允许空。如果为空,则默认方法的所有参数进行组合。如果非空,则需要按照 SpEL 配置。例如,@Cacheable(value = "users", key = "#id")
,使用方法参数id
的值作为缓存的 key 。condition
:基于方法入参,判断要缓存的条件。允许空。如果非空,则需要按照 SpEL 配置。例如,@Cacheable(condition="#id > 0")
,需要传入的id
大于零。unless
:基于方法返回,判断不缓存的条件。允许空。如果非空,则需要按照 SpEL 配置。例如,@Cacheable(unless="#result == null")
,如果返回结果为null
,则不进行缓存。
不常用属性:
keyGenerator
:自定义 key 生成器 KeyGenerator Bean 的名字。允许空。如果设置,则key
失效。cacheManager
:自定义缓存管理器 CacheManager Bean 的名字。允许空。一般不填写,除非有多个 CacheManager Bean 的情况下。cacheResolver
:自定义缓存解析器 CacheResolver Bean 的名字。允许空。sync
:在获得不到缓存的情况下,是否同步执行方法。默认为false
,表示无需同步。如果设置为true
,则执行方法时,会进行加锁,保证同一时刻,有且仅有一个方法在执行,其它线程阻塞等待。
(二)@CachePut
@CachePut
注解用于方法上,缓存方法的执行结果。与 @Cacheable
不同,它的执行过程如下:
- 执行方法,获得方法结果。也就是说,无论是否有缓存,都会执行方法。
- 根据是否满足缓存的条件。如果满足,则缓存方法结果到缓存。
- 返回方法结果。
一般来说,@Cacheable
搭配读操作,实现缓存的被动写;@CachePut
配置写操作,实现缓存的主动写。
(三)@CacheEvict
@CacheEvict
注解用于方法上,删除缓存。相比 @CachePut
,它额外多了两个属性:
allEntries
:是否删除缓存名(cacheNames
)下,所有 key 对应的缓存。默认为false
,只删除指定 key 的缓存。beforeInvocation
:是否在方法执行前删除缓存。默认为false
,在方法执行后删除缓存。
(四)@Caching
@Caching
注解用于方法上,可以组合使用多个 @Cacheable
、@CachePut
、@CacheEvict
注解。不太常用,可以暂时忽略。
(五)@CacheConfig
@CacheConfig
注解用于类上,共享如下四个属性的配置:
cacheNames
keyGenerator
cacheManager
cacheResolver
(六)@EnableCaching
@EnableCaching
注解用于标记开启 Spring Cache 功能,所以一定要添加。
三、Spring Boot 集成
(一)依赖
在 Spring Boot 里,提供了 spring-boot-starter-cache
库,实现 Spring Cache 的自动化配置,通过 CacheAutoConfiguration
配置类。
(二)缓存工具和框架
在 Java 后端开发中,常见的缓存工具和框架列举如下:
- 本地缓存:Guava LocalCache、Ehcache、Caffeine 。Ehcache 的功能更加丰富,Caffeine 的性能要比 Guava LocalCache 好。
- 分布式缓存:Redis、Memcached、Tair 。Redis 最为主流和常用。
(三)自动配置
在这些缓存方案当中,spring-boot-starter-cache
怎么知道使用哪种呢?在默认情况下,Spring Boot 会按照如下顺序,自动判断使用哪种缓存方案,创建对应的 CacheManager 缓存管理器。
private static final Map<CacheType, Class<?>> MAPPINGS;
static {
Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
}
最差的情况下,会使用 SimpleCacheConfiguration
。因为自动判断可能和我们希望使用的缓存方案不同,此时我们可以手动配置 spring.cache.type
指定类型。
四、Ehcache 示例
(一)引入依赖
在 pom.xml
文件中,引入相关依赖。
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
(二)应用配置文件
在 resources
目录下,创建 application.yaml
配置文件。配置如下:
spring:
cache:
type: ehcache
(三)Ehcache 配置文件
在 resources
目录下,创建 ehcache.xml
配置文件。配置如下:
<ehcache>
<cache name="users"
maxElementsInMemory="1000"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
(四)Application
创建 Application.java
类,代码如下:
@SpringBootApplication
@EnableCaching
public class Application {
}
(五)UserDO
在 cn.iocoder.springboot.lab21.cache.dataobject
包路径下,创建 UserDO.java
类,用户 DO 。代码如下:
@TableName(value = "users")
public class UserDO {
private Integer id;
private String username;
private String password;
private Date createTime;
@TableLogic
private Integer deleted;
}
(六)UserMapper
在 cn.iocoder.springboot.lab21.cache.mapper
包路径下,创建 UserMapper
接口。代码如下:
@Repository
@CacheConfig(cacheNames = "users")
public interface UserMapper extends BaseMapper<UserDO> {
@Cacheable(key = "#id")
UserDO selectById(Integer id);
@CachePut(key = "#user.id")
default UserDO insert0(UserDO user) {
this.insert(user);
return user;
}
@CacheEvict(key = "#id")
int deleteById(Integer id);
}
(七)UserMapperTest
创建 UserMapperTest
测试类,我们来测试一下简单的 UserMapper
的每个操作。核心代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserMapperTest {
private static final String CACHE_NAME_USER = "users";
@Autowired
private UserMapper userMapper;
@Autowired
private CacheManager cacheManager;
@Test
public void testCacheManager() {
System.out.println(cacheManager);
}
@Test
public void testSelectById() {
Integer id = 1;
UserDO user = userMapper.selectById(id);
System.out.println("user:" + user);
Assert.assertNotNull("缓存为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
user = userMapper.selectById(id);
System.out.println("user:" + user);
}
@Test
public void testInsert() {
UserDO user = new UserDO();
user.setUsername(UUID.randomUUID().toString());
user.setPassword("nicai");
user.setCreateTime(new Date());
user.setDeleted(0);
userMapper.insert0(user);
Assert.assertNotNull("缓存为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
}
@Test
public void testDeleteById() {
UserDO user = new UserDO();
user.setUsername(UUID.randomUUID().toString());
user.setPassword("nicai");
user.setCreateTime(new Date());
user.setDeleted(0);
userMapper.insert0(user);
Assert.assertNotNull("缓存为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
userMapper.deleteById(user.getId());
Assert.assertNull("缓存不为空", cacheManager.getCache(CACHE_NAME_USER).get(user.getId(), UserDO.class));
}
}
五、Redis 示例
(一)引入依赖
在 pom.xml
文件中,引入相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
(二)应用配置文件
在 resources
目录下,创建 application.yaml
配置文件。配置如下:
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
timeout: 0
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
cache:
type: redis
(三)Application
与 4.4 Application
一致。
(四)UserDO
与 4.5 UserDO
一致。差别在于,需要让 UserDO
实现 Serializable
接口。
(五)UserMapper
与 4.6 UserMapper
一致。
(六)UserMapperTest
与 4.7 UserMapperTest
基本一致。
六、面试回答思路和答案
(一)缓存的必要性
问题:为什么需要使用缓存?
回答:随着系统访问量的增加,数据库往往成为性能瓶颈。缓存可以减少数据库的压力,提高系统的响应速度。缓存系统(如 Redis、Ehcache)可以存储经常访问的数据,减少对数据库的直接访问。
(二)Spring Cache
问题:什么是 Spring Cache?
回答 :Spring Cache 是 Spring 3.1 引入的基于注解的缓存技术,通过 @Cacheable
等注解简化缓存逻辑,支持多种缓存实现。Spring Cache 的特点包括:通过少量的配置注解即可使得既有代码支持缓存;支持开箱即用;支持 Spring 表达式语言(SpEL);支持 AspectJ;支持自定义 key 和自定义缓存管理者。
(三)@Cacheable
问题 :@Cacheable
注解的使用方法和属性有哪些?
回答 :@Cacheable
注解用于方法上,缓存方法的执行结果。常用属性包括:cacheNames
(缓存名,必填),key
(缓存的 key,允许空),condition
(基于方法入参,判断要缓存的条件),unless
(基于方法返回,判断不缓存的条件)。
(四)@CachePut
问题 :@CachePut
注解的作用是什么?
回答 :@CachePut
注解用于方法上,缓存方法的执行结果。与 @Cacheable
不同,它的执行过程如下:执行方法,获得方法结果;根据是否满足缓存的条件,如果满足,则缓存方法结果到缓存;返回方法结果。
(五)@CacheEvict
问题 :@CacheEvict
注解的作用是什么?
回答 :@CacheEvict
注解用于方法上,删除缓存。相比 @CachePut
,它额外多了两个属性:allEntries
(是否删除缓存名下所有 key 对应的缓存),beforeInvocation
(是否在方法执行前删除缓存)。
(六)Spring Boot 缓存集成
问题:如何在 Spring Boot 中集成缓存?
回答 :在 Spring Boot 中,可以通过引入 spring-boot-starter-cache
依赖来集成缓存。Spring Boot 会自动判断使用哪种缓存方案,也可以通过 spring.cache.type
手动指定。
(七)Ehcache 和 Redis
问题:Ehcache 和 Redis 的区别是什么?
回答:Ehcache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider 。Redis 是一个基于键值对的内存数据库,支持多种数据类型,如字符串、哈希、列表、集合、有序集合等。Ehcache 适合用于本地缓存,Redis 适合用于分布式缓存。
(八)缓存策略
问题:常见的缓存策略有哪些?
回答:常见的缓存策略包括:读写分离(将读操作分流到从节点,避免主节点压力过多),分库分表(将读写操作分摊到多个节点,避免单节点压力过多),缓存系统(使用缓存系统(如 Redis、Ehcache)来存储经常访问的数据,减少对数据库的直接访问)。
(九)缓存击穿
问题:什么是缓存击穿?如何解决?
回答:缓存击穿是指多个线程同时访问同一个缓存键,导致缓存失效,多个线程同时查询数据库,造成数据库压力。解决方法包括:使用互斥锁(如 Redis 的 SETNX 命令),保证同一时刻只有一个线程查询数据库;使用缓存预热,在系统启动时预先加载缓存。
(十)缓存穿透
问题:什么是缓存穿透?如何解决?
回答:缓存穿透是指查询一个不存在的数据,导致缓存和数据库都没有命中,直接查询数据库。解决方法包括:使用布隆过滤器,快速判断数据是否存在;对不存在的数据也进行缓存,设置较短的过期时间。
(十一)缓存雪崩
问题:什么是缓存雪崩?如何解决?
回答:缓存雪崩是指大量缓存同时失效,导致大量请求直接查询数据库,造成数据库压力。解决方法包括:使用缓存预热,在系统启动时预先加载缓存;设置不同的过期时间,避免大量缓存同时失效。
(十二)缓存的过期策略
问题:缓存的过期策略有哪些?
回答:缓存的过期策略包括:设置固定过期时间(如 Redis 的 EXPIRE 命令),根据访问频率动态调整过期时间,根据数据的重要性动态调整过期时间。
(十三)缓存的淘汰策略
问题:缓存的淘汰策略有哪些?
回答:缓存的淘汰策略包括:先进先出(FIFO),最近最少使用(LRU),最不经常使用(LFU),随机淘汰。
(十四)缓存的序列化
问题:缓存的序列化有哪些方式?
回答:缓存的序列化方式包括:Java 序列化,JSON 序列化,Protobuf 序列化。Java 序列化简单,但性能较差;JSON 序列化性能较好,但需要手动实现序列化和反序列化;Protobuf 序列化性能最好,但需要定义.proto 文件。
(十五)缓存的分布式锁
问题:如何在分布式环境下实现缓存的锁?
回答:在分布式环境下,可以使用 Redis 的 SETNX 命令实现分布式锁。SETNX 命令可以保证同一时刻只有一个线程获取到锁,从而避免缓存击穿等问题。
(十六)缓存的事务
问题:缓存是否支持事务?
回答:缓存本身不支持事务,但可以通过与数据库事务结合来实现。例如,在数据库事务提交后,更新缓存;在数据库事务回滚后,回滚缓存。
(十七)缓存的监控
问题:如何监控缓存的使用情况?
回答:可以使用 Redis 的 INFO 命令监控缓存的使用情况,包括内存使用、命中率、过期键数等。也可以使用第三方监控工具,如 Prometheus 和 Grafana。
(十八)缓存的优化
问题:如何优化缓存的性能?
回答:优化缓存性能的方法包括:选择合适的缓存策略,使用高效的序列化方式,合理设置缓存的过期时间和淘汰策略,使用分布式锁避免缓存击穿,监控缓存的使用情况并及时调整。
(十九)缓存的命中率
问题:如何计算缓存的命中率?
回答:缓存的命中率可以通过以下公式计算:命中率 = 命中次数 /(命中次数 + 未命中次数)。可以通过监控工具获取命中次数和未命中次数。
(二十)缓存的场景
问题:缓存适用于哪些场景?
回答:缓存适用于以下场景:经常访问的数据,如用户信息、商品信息等;数据变化不频繁,如配置信息;对实时性要求不高的数据,如统计信息。
(二十一)缓存的局限性
问题:缓存有哪些局限性?
回答:缓存的局限性包括:缓存数据与数据库数据不一致,缓存击穿、缓存穿透、缓存雪崩等问题,缓存的内存有限,缓存的序列化和反序列化可能影响性能,缓存的分布式锁实现复杂。
(二十二)缓存的未来发展趋势
问题:缓存技术未来的发展趋势是什么?
回答:缓存技术未来的发展趋势包括:与数据库的深度融合,支持事务和一致性;支持更多的数据类型和查询方式;提供更好的性能和扩展性;提供更便捷的监控和管理工具。
以上就是对 Spring Boot 缓存 Cache 入门的详细讲解和面试回答思路及答案。希望对初学者有所帮助,祝大家学习愉快!