Spring Boot 缓存机制
1. 缓存概述
1.1 什么是缓存
缓存是一种临时存储机制,用于存储频繁访问的数据,以减少数据库访问次数,提高系统性能。
1.2 缓存类型
| 类型 | 说明 | 应用场景 |
|---|---|---|
| 本地缓存 | JVM 内存缓存 | 单机应用、小数据量 |
| 分布式缓存 | Redis、Memcached | 分布式系统、大数据量 |
| 多级缓存 | 本地 + 分布式 | 高并发、低延迟 |
1.3 Spring Cache 抽象
Spring Cache 提供了统一的缓存抽象,支持多种缓存实现:
- JDK ConcurrentMap
- Ehcache
- Redis
- Caffeine
2. 快速入门
2.1 添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>2.2 启用缓存
java
@SpringBootApplication
@EnableCaching
public class MyappApplication {
public static void main(String[] args) {
SpringApplication.run(MyappApplication.class, args);
}
}2.3 配置缓存
yaml
spring:
cache:
type: redis
redis:
time-to-live: 600000
cache-null-values: false
key-prefix: myapp:
use-key-prefix: true
data:
redis:
host: localhost
port: 63792.4 基本使用
java
@Service
public class UserService {
private final UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "users", key = "#username")
public User findByUsername(String username) {
return userRepository.findByUsername(username).orElse(null);
}
@CachePut(value = "users", key = "#user.id")
public User save(User user) {
return userRepository.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void deleteById(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(value = "users", allEntries = true)
public void deleteAll() {
userRepository.deleteAll();
}
}3. 缓存注解
3.1 @Cacheable
用于查询方法,先查缓存,缓存不存在则执行方法并缓存结果:
java
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "users", key = "#username", unless = "#result == null")
public User findByUsername(String username) {
return userRepository.findByUsername(username).orElse(null);
}
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}3.2 @CachePut
用于更新方法,始终执行方法并更新缓存:
java
@CachePut(value = "users", key = "#user.id")
public User update(User user) {
return userRepository.save(user);
}
@CachePut(value = "users", key = "#result.id")
public User create(User user) {
return userRepository.save(user);
}3.3 @CacheEvict
用于删除方法,清除缓存:
java
@CacheEvict(value = "users", key = "#id")
public void deleteById(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(value = "users", allEntries = true)
public void deleteAll() {
userRepository.deleteAll();
}
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteById(Long id) {
userRepository.deleteById(id);
}3.4 @Caching
组合多个缓存操作:
java
@Caching(
put = {
@CachePut(value = "users", key = "#user.id"),
@CachePut(value = "users", key = "#user.username")
}
)
public User save(User user) {
return userRepository.save(user);
}
@Caching(
evict = {
@CacheEvict(value = "users", key = "#id"),
@CacheEvict(value = "userOrders", key = "#id")
}
)
public void deleteById(Long id) {
userRepository.deleteById(id);
}3.5 @CacheConfig
类级别缓存配置:
java
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
@Cacheable(key = "#id")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@CachePut(key = "#user.id")
public User save(User user) {
return userRepository.save(user);
}
@CacheEvict(key = "#id")
public void deleteById(Long id) {
userRepository.deleteById(id);
}
}4. 缓存配置
4.1 Redis 缓存配置
java
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.prefixCacheNameWith("myapp:");
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put("users", defaultConfig.entryTtl(Duration.ofHours(1)));
cacheConfigurations.put("products", defaultConfig.entryTtl(Duration.ofMinutes(10)));
cacheConfigurations.put("categories", defaultConfig.entryTtl(Duration.ofDays(1)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.transactionAware()
.build();
}
}4.2 Caffeine 本地缓存
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>java
@Configuration
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(30))
.initialCapacity(100)
.maximumSize(1000));
return cacheManager;
}
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(30))
.expireAfterAccess(Duration.ofMinutes(10))
.initialCapacity(100)
.maximumSize(1000)
.recordStats()
.build();
}
}4.3 多级缓存
java
@Configuration
public class MultiLevelCacheConfig {
@Bean
@Primary
public CacheManager multiLevelCacheManager(
CacheManager redisCacheManager,
CacheManager caffeineCacheManager) {
return new CompositeCacheManager(
caffeineCacheManager,
redisCacheManager
);
}
}5. 自定义缓存 Key
5.1 KeyGenerator
java
@Component
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
sb.append(param.toString()).append(":");
}
return sb.toString();
}
}
@Service
public class UserService {
@Cacheable(value = "users", keyGenerator = "customKeyGenerator")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
}5.2 SpEL 表达式
java
@Service
public class UserService {
@Cacheable(value = "users", key = "'user:' + #id")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "users", key = "#user.id + ':' + #user.username")
public User findByUser(User user) {
return user;
}
@Cacheable(value = "users", key = "T(String).valueOf(#id).concat(':').concat(#name)")
public User findByIdAndName(Long id, String name) {
return userRepository.findByIdAndName(id, name);
}
}6. 缓存条件
6.1 condition
满足条件时才缓存:
java
@Cacheable(value = "users", key = "#id", condition = "#id > 0")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "products", key = "#id", condition = "#root.target.enabled")
public Product findById(Long id) {
return productRepository.findById(id).orElse(null);
}6.2 unless
满足条件时不缓存:
java
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "products", key = "#id", unless = "#result.price > 10000")
public Product findById(Long id) {
return productRepository.findById(id).orElse(null);
}
@CachePut(value = "users", key = "#user.id", unless = "#result.status == 'INACTIVE'")
public User update(User user) {
return userRepository.save(user);
}7. 缓存问题处理
7.1 缓存穿透
缓存穿透是指查询不存在的数据,每次都会查询数据库:
java
@Service
public class UserService {
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "users", key = "#id", cacheNull = true)
public User findByIdWithNullCache(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "users", key = "#id")
public Optional<User> findByIdOptional(Long id) {
return userRepository.findById(id);
}
}7.2 缓存击穿
缓存击穿是指热点数据过期,大量请求同时访问数据库:
java
@Service
public class ProductService {
private final Map<String, ReentrantLock> locks = new ConcurrentHashMap<>();
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
String lockKey = "product:" + id;
ReentrantLock lock = locks.computeIfAbsent(lockKey, k -> new ReentrantLock());
lock.lock();
try {
Product product = productRepository.findById(id).orElse(null);
return product;
} finally {
lock.unlock();
locks.remove(lockKey);
}
}
}7.3 缓存雪崩
缓存雪崩是指大量缓存同时过期:
java
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30 + ThreadLocalRandom.current().nextInt(10)));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.build();
}
}7.4 缓存一致性
java
@Service
public class UserService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Transactional
@CachePut(value = "users", key = "#user.id")
public User update(User user) {
User saved = userRepository.save(user);
return saved;
}
@Transactional
@CacheEvict(value = "users", key = "#id")
public void deleteById(Long id) {
userRepository.deleteById(id);
}
@Transactional
@Caching(evict = {
@CacheEvict(value = "users", key = "#id"),
@CacheEvict(value = "userOrders", key = "#id"),
@CacheEvict(value = "userProfile", key = "#id")
})
public void deleteWithRelated(Long id) {
orderRepository.deleteByUserId(id);
profileRepository.deleteByUserId(id);
userRepository.deleteById(id);
}
}8. 缓存监控
8.1 缓存统计
java
@Configuration
public class CacheMetricsConfig {
@Bean
public CacheMetricsRegistrar cacheMetricsRegistrar(MeterRegistry meterRegistry,
CacheManager cacheManager) {
CacheMetricsRegistrar registrar = new CacheMetricsRegistrar(meterRegistry,
new DefaultCacheMetricsConvention());
if (cacheManager instanceof RedisCacheManager) {
((RedisCacheManager) cacheManager).getCacheNames()
.forEach(name -> registrar.bindCacheToRegistry(cacheManager.getCache(name)));
}
return registrar;
}
}8.2 Actuator 端点
yaml
management:
endpoints:
web:
exposure:
include: caches,metrics
endpoint:
caches:
enabled: true8.3 自定义缓存监控
java
@Component
public class CacheMonitor {
private final CacheManager cacheManager;
@Scheduled(fixedRate = 60000)
public void monitorCache() {
cacheManager.getCacheNames().forEach(cacheName -> {
Cache cache = cacheManager.getCache(cacheName);
if (cache != null) {
log.info("Cache: {}, Statistics: {}", cacheName, cache.getStatistics());
}
});
}
}9. 实战案例
9.1 商品缓存
java
@Service
public class ProductService {
private final ProductRepository productRepository;
private final RedisTemplate<String, Object> redisTemplate;
private static final String PRODUCT_CACHE = "products";
private static final String PRODUCT_LOCK = "product:lock:";
@Cacheable(value = PRODUCT_CACHE, key = "#id", unless = "#result == null")
public Product findById(Long id) {
return productRepository.findById(id).orElse(null);
}
@Cacheable(value = PRODUCT_CACHE, key = "'list:' + #categoryId + ':' + #page")
public Page<Product> findByCategory(Long categoryId, int page, int size) {
return productRepository.findByCategoryId(categoryId,
PageRequest.of(page, size));
}
@CachePut(value = PRODUCT_CACHE, key = "#product.id")
public Product save(Product product) {
return productRepository.save(product);
}
@CacheEvict(value = PRODUCT_CACHE, key = "#id")
public void deleteById(Long id) {
productRepository.deleteById(id);
}
@CacheEvict(value = PRODUCT_CACHE, allEntries = true)
public void refreshAll() {
log.info("Refreshed all product cache");
}
public Product findByIdWithLock(Long id) {
String lockKey = PRODUCT_LOCK + id;
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (Boolean.TRUE.equals(acquired)) {
try {
Product product = findById(id);
if (product == null) {
product = productRepository.findById(id).orElse(null);
if (product != null) {
redisTemplate.opsForValue().set(
PRODUCT_CACHE + "::" + id,
product,
Duration.ofHours(1)
);
}
}
return product;
} finally {
redisTemplate.delete(lockKey);
}
}
return findById(id);
}
}9.2 用户会话缓存
java
@Service
public class SessionService {
private final RedisTemplate<String, Object> redisTemplate;
private static final String SESSION_PREFIX = "session:";
private static final Duration SESSION_TIMEOUT = Duration.ofHours(2);
public void createSession(String sessionId, User user) {
String key = SESSION_PREFIX + sessionId;
redisTemplate.opsForValue().set(key, user, SESSION_TIMEOUT);
}
public User getSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
return (User) redisTemplate.opsForValue().get(key);
}
public void refreshSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
redisTemplate.expire(key, SESSION_TIMEOUT);
}
public void deleteSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
redisTemplate.delete(key);
}
public boolean isSessionValid(String sessionId) {
String key = SESSION_PREFIX + sessionId;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
}10. 小结
本章学习了 Spring Boot 缓存机制的核心内容:
| 内容 | 要点 |
|---|---|
| 缓存注解 | @Cacheable、@CachePut、@CacheEvict |
| 缓存配置 | Redis、Caffeine、多级缓存 |
| 自定义 Key | KeyGenerator、SpEL 表达式 |
| 缓存条件 | condition、unless |
| 缓存问题 | 穿透、击穿、雪崩、一致性 |
| 缓存监控 | Actuator、自定义监控 |
下一章将学习 Spring Boot 安全配置。