Skip to content

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: 6379

2.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: true

8.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、多级缓存
自定义 KeyKeyGenerator、SpEL 表达式
缓存条件condition、unless
缓存问题穿透、击穿、雪崩、一致性
缓存监控Actuator、自定义监控

下一章将学习 Spring Boot 安全配置。