在高并发系统设计中,缓存是提升性能的关键策略之一。随着业务的发展,单一的缓存方案往往无法同时兼顾性能、可靠性和一致性等多方面需求。
此时,二级缓存架构应运而生,本文将介绍在Spring Boot中实现二级缓存的三种方案。
二级缓存是一种多层次的缓存架构,通常由以下两个层次组成:
二级缓存的工作流程通常是:先查询本地缓存,若未命中则查询分布式缓存,仍未命中才访问数据库,并将结果回填到各级缓存中。
单一缓存方案存在明显局限性:
二级缓存结合了两者优势:
该方案利用Spring Cache提供的缓存抽象,配合Caffeine(本地缓存)和Redis(分布式缓存)实现二级缓存。
Spring Cache提供了统一的缓存操作接口,可以通过简单的注解实现缓存功能。
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-cache
org.springframework.boot
spring-boot-starter-data-redis
com.github.ben-manes.caffeine
caffeine
com.fasterxml.jackson.core
jackson-databind
@Configuration
@EnableCaching
public class CacheConfig {
@Value("${spring.application.name:app}")
private String appName;
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 创建Redis缓存管理器
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(getRedisCacheConfigurationWithTtl(3600)) // 默认1小时过期
.withCacheConfiguration("userCache", getRedisCacheConfigurationWithTtl(1800)) // 用户缓存30分钟
.withCacheConfiguration("productCache", getRedisCacheConfigurationWithTtl(7200)) // 产品缓存2小时
.build();
// 创建Caffeine缓存管理器
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(100) // 初始容量
.maximumSize(1000) // 最大容量
.expireAfterWrite(5, TimeUnit.MINUTES) // 写入后5分钟过期
.recordStats()); // 开启统计
// 创建二级缓存管理器
return new LayeringCacheManager(caffeineCacheManager, redisCacheManager);
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(long seconds) {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(seconds))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues()
.computePrefixWith(cacheName -> appName + ":" + cacheName + ":");
}
// 二级缓存管理器实现
public static class LayeringCacheManager implements CacheManager {
private final CacheManager localCacheManager;
private final CacheManager remoteCacheManager;
private final Map cacheMap = new ConcurrentHashMap<>();
public LayeringCacheManager(CacheManager localCacheManager, CacheManager remoteCacheManager) {
this.localCacheManager = localCacheManager;
this.remoteCacheManager = remoteCacheManager;
}
@Override
public Cache getCache(String name) {
return cacheMap.computeIfAbsent(name, cacheName -> {
Cache localCache = localCacheManager.getCache(cacheName);
Cache remoteCache = remoteCacheManager.getCache(cacheName);
return new LayeringCache(localCache, remoteCache);
});
}
@Override
public Collection getCacheNames() {
Set names = new LinkedHashSet<>();
names.addAll(localCacheManager.getCacheNames());
names.addAll(remoteCacheManager.getCacheNames());
return names;
}
// 二级缓存实现
static class LayeringCache implements Cache {
private final Cache localCache;
private final Cache remoteCache;
public LayeringCache(Cache localCache, Cache remoteCache) {
this.localCache = localCache;
this.remoteCache = remoteCache;
}
@Override
public String getName() {
return localCache.getName();
}
@Override
public Object getNativeCache() {
return this;
}
@Override
public ValueWrapper get(Object key) {
// 先查本地缓存
ValueWrapper wrapper = localCache.get(key);
if (wrapper != null) {
return wrapper;
}
// 本地未命中,查远程缓存
wrapper = remoteCache.get(key);
if (wrapper != null) {
Object value = wrapper.get();
// 回填本地缓存
localCache.put(key, value);
}
return wrapper;
}
@Override
public T get(Object key, Class type) {
// 先查本地缓存
T value = localCache.get(key, type);
if (value != null) {
return value;
}
// 本地未命中,查远程缓存
value = remoteCache.get(key, type);
if (value != null) {
// 回填本地缓存
localCache.put(key, value);
}
return value;
}
@Override
public T get(Object key, Callable valueLoader) {
// 先查本地缓存
try {
T value = localCache.get(key, () -> {
// 本地未命中,查远程缓存
try {
return remoteCache.get(key, valueLoader);
} catch (Exception e) {
// 远程缓存未命中或异常,执行valueLoader加载数据
T newValue = valueLoader.call();
if (newValue != null) {
remoteCache.put(key, newValue); // 填充远程缓存
}
return newValue;
}
});
return value;
} catch (Exception e) {
// 本地缓存异常,尝试直接读远程缓存
try {
return remoteCache.get(key, valueLoader);
} catch (Exception ex) {
if (ex instanceof RuntimeException) {
throw (RuntimeException) ex;
}
throw new IllegalStateException(ex);
}
}
}
@Override
public void put(Object key, Object value) {
remoteCache.put(key, value); // 先放入远程缓存
localCache.put(key, value); // 再放入本地缓存
}
@Override
public void evict(Object key) {
remoteCache.evict(key); // 先清远程缓存
localCache.evict(key); // 再清本地缓存
}
@Override
public void clear() {
remoteCache.clear(); // 先清远程缓存
localCache.clear(); // 再清本地缓存
}
}
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
@Cacheable(cacheNames = "userCache", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Override
@CachePut(cacheNames = "userCache", key = "#user.id")
public User saveUser(User user) {
return userRepository.save(user);
}
@Override
@CacheEvict(cacheNames = "userCache", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
在分布式环境下,需要保证缓存一致性。我们可以通过Redis的发布订阅机制实现:
@Configuration
public class CacheEvictionConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
@Bean
public RedisCacheMessageListener redisCacheMessageListener(RedisMessageListenerContainer listenerContainer,
CacheManager cacheManager) {
return new RedisCacheMessageListener(listenerContainer, cacheManager);
}
// 缓存消息监听器
public static class RedisCacheMessageListener {
private static final String CACHE_CHANGE_TOPIC = "cache:changes";
private final CacheManager cacheManager;
public RedisCacheMessageListener(RedisMessageListenerContainer listenerContainer, CacheManager cacheManager) {
this.cacheManager = cacheManager;
listenerContainer.addMessageListener((message, pattern) -> {
String body = new String(message.getBody());
CacheChangeMessage cacheMessage = JSON.parseObject(body, CacheChangeMessage.class);
// 清除本地缓存
Cache cache = cacheManager.getCache(cacheMessage.getCacheName());
if (cache != null) {
if (cacheMessage.getKey() != null) {
cache.evict(cacheMessage.getKey());
} else {
cache.clear();
}
}
}, new ChannelTopic(CACHE_CHANGE_TOPIC));
}
}
@Bean
public CacheChangePublisher cacheChangePublisher(RedisTemplate redisTemplate) {
return new CacheChangePublisher(redisTemplate);
}
// 缓存变更消息发布器
public static class CacheChangePublisher {
private static final String CACHE_CHANGE_TOPIC = "cache:changes";
private final RedisTemplate redisTemplate;
public CacheChangePublisher(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void publishCacheEvict(String cacheName, Object key) {
CacheChangeMessage message = new CacheChangeMessage(cacheName, key);
redisTemplate.convertAndSend(CACHE_CHANGE_TOPIC, JSON.toJSONString(message));
}
public void publishCacheClear(String cacheName) {
CacheChangeMessage message = new CacheChangeMessage(cacheName, null);
redisTemplate.convertAndSend(CACHE_CHANGE_TOPIC, JSON.toJSONString(message));
}
}
// 缓存变更消息
@Data
@AllArgsConstructor
public static class CacheChangeMessage {
private String cacheName;
private Object key;
}
}
优点:
缺点:
该方案通过自定义缓存框架,精确控制缓存的读写流程、失效策略和同步机制,实现更加贴合业务需求的二级缓存。
这种方式虽然实现复杂度高,但提供了最大的灵活性和控制力。
public interface Cache {
V get(K key);
void put(K key, V value);
void remove(K key);
void clear();
long size();
boolean containsKey(K key);
}
public interface CacheLoader {
V load(K key);
}
public class LocalCache implements Cache {
private final com.github.benmanes.caffeine.cache.Cache cache;
public LocalCache(long maximumSize, long expireAfterWriteSeconds) {
this.cache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(expireAfterWriteSeconds, TimeUnit.SECONDS)
.recordStats()
.build();
}
@Override
public V get(K key) {
return cache.getIfPresent(key);
}
@Override
public void put(K key, V value) {
if (value != null) {
cache.put(key, value);
}
}
@Override
public void remove(K key) {
cache.invalidate(key);
}
@Override
public void clear() {
cache.invalidateAll();
}
@Override
public long size() {
return cache.estimatedSize();
}
@Override
public boolean containsKey(K key) {
return cache.getIfPresent(key) != null;
}
public CacheStats stats() {
return cache.stats();
}
}
public class RedisCache implements Cache {
private final RedisTemplate redisTemplate;
private final String cachePrefix;
private final long expireSeconds;
private final Class valueType;
public RedisCache(RedisTemplate redisTemplate,
String cachePrefix,
long expireSeconds,
Class valueType) {
this.redisTemplate = redisTemplate;
this.cachePrefix = cachePrefix;
this.expireSeconds = expireSeconds;
this.valueType = valueType;
}
private String getCacheKey(K key) {
return cachePrefix + ":" + key.toString();
}
@Override
public V get(K key) {
String cacheKey = getCacheKey(key);
return (V) redisTemplate.opsForValue().get(cacheKey);
}
@Override
public void put(K key, V value) {
if (value != null) {
String cacheKey = getCacheKey(key);
redisTemplate.opsForValue().set(cacheKey, value, expireSeconds, TimeUnit.SECONDS);
}
}
@Override
public void remove(K key) {
String cacheKey = getCacheKey(key);
redisTemplate.delete(cacheKey);
}
@Override
public void clear() {
Set keys = redisTemplate.keys(cachePrefix + ":*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
@Override
public long size() {
Set keys = redisTemplate.keys(cachePrefix + ":*");
return keys != null ? keys.size() : 0;
}
@Override
public boolean containsKey(K key) {
String cacheKey = getCacheKey(key);
return Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey));
}
}
public class TwoLevelCache implements Cache {
private final Cache localCache;
private final Cache remoteCache;
private final CacheLoader cacheLoader;
private final String cacheName;
private final CacheEventPublisher eventPublisher;
public TwoLevelCache(Cache localCache,
Cache remoteCache,
CacheLoader cacheLoader,
String cacheName,
CacheEventPublisher eventPublisher) {
this.localCache = localCache;
this.remoteCache = remoteCache;
this.cacheLoader = cacheLoader;
this.cacheName = cacheName;
this.eventPublisher = eventPublisher;
}
@Override
public V get(K key) {
// 先查本地缓存
V value = localCache.get(key);
if (value != null) {
return value;
}
// 本地未命中,查远程缓存
value = remoteCache.get(key);
if (value != null) {
// 回填本地缓存
localCache.put(key, value);
return value;
}
// 远程也未命中,加载数据
if (cacheLoader != null) {
value = cacheLoader.load(key);
if (value != null) {
// 填充缓存
put(key, value);
}
}
return value;
}
@Override
public void put(K key, V value) {
if (value != null) {
// 先放入远程缓存,再放入本地缓存
remoteCache.put(key, value);
localCache.put(key, value);
}
}
@Override
public void remove(K key) {
// 先清远程缓存,再清本地缓存
remoteCache.remove(key);
localCache.remove(key);
// 发布缓存失效事件
if (eventPublisher != null) {
eventPublisher.publishCacheEvictEvent(cacheName, key);
}
}
@Override
public void clear() {
// 先清远程缓存,再清本地缓存
remoteCache.clear();
localCache.clear();
// 发布缓存清空事件
if (eventPublisher != null) {
eventPublisher.publishCacheClearEvent(cacheName);
}
}
@Override
public long size() {
return remoteCache.size();
}
@Override
public boolean containsKey(K key) {
return localCache.containsKey(key) || remoteCache.containsKey(key);
}
}
@Component
public class CacheEventPublisher {
private final RedisTemplate redisTemplate;
private static final String CACHE_EVICT_TOPIC = "cache:evict";
private static final String CACHE_CLEAR_TOPIC = "cache:clear";
public CacheEventPublisher(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void publishCacheEvictEvent(String cacheName, Object key) {
Map message = new HashMap<>();
message.put("cacheName", cacheName);
message.put("key", key);
redisTemplate.convertAndSend(CACHE_EVICT_TOPIC, JSON.toJSONString(message));
}
public void publishCacheClearEvent(String cacheName) {
Map message = new HashMap<>();
message.put("cacheName", cacheName);
redisTemplate.convertAndSend(CACHE_CLEAR_TOPIC, JSON.toJSONString(message));
}
}
@Component
public class CacheEventListener {
private final Map> cacheMap;
public CacheEventListener(RedisMessageListenerContainer listenerContainer,
Map> cacheMap) {
this.cacheMap = cacheMap;
// 监听缓存失效事件
MessageListener evictListener = (message, pattern) -> {
String body = new String(message.getBody());
Map map = JSON.parseObject(body, Map.class);
String cacheName = (String) map.get("cacheName");
Object key = map.get("key");
TwoLevelCache
@Component
public class TwoLevelCacheManager {
private final RedisTemplate redisTemplate;
private final CacheEventPublisher eventPublisher;
private final Map> cacheMap = new ConcurrentHashMap<>();
public TwoLevelCacheManager(RedisTemplate redisTemplate,
CacheEventPublisher eventPublisher) {
this.redisTemplate = redisTemplate;
this.eventPublisher = eventPublisher;
}
public TwoLevelCache getCache(String cacheName,
Class valueType,
CacheLoader cacheLoader) {
return getCache(cacheName, valueType, cacheLoader, 1000, 300, 3600);
}
@SuppressWarnings("unchecked")
public TwoLevelCache getCache(String cacheName,
Class valueType,
CacheLoader cacheLoader,
long localMaxSize,
long localExpireSeconds,
long remoteExpireSeconds) {
return (TwoLevelCache) cacheMap.computeIfAbsent(cacheName, name -> {
LocalCache localCache = new LocalCache<>(localMaxSize, localExpireSeconds);
RedisCache remoteCache = new RedisCache<>(redisTemplate, name, remoteExpireSeconds, valueType);
return new TwoLevelCache<>(localCache, remoteCache, cacheLoader, name, eventPublisher);
});
}
public Map> getCacheMap() {
return Collections.unmodifiableMap(cacheMap);
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private TwoLevelCacheManager cacheManager;
private TwoLevelCache userCache;
@PostConstruct
public void init() {
userCache = cacheManager.getCache("user", User.class, this::loadUser, 1000, 300, 1800);
}
private User loadUser(Long id) {
return userRepository.findById(id).orElse(null);
}
@Override
public User getUserById(Long id) {
return userCache.get(id);
}
@Override
public User saveUser(User user) {
User savedUser = userRepository.save(user);
userCache.put(user.getId(), savedUser);
return savedUser;
}
@Override
public void deleteUser(Long id) {
userRepository.deleteById(id);
userCache.remove(id);
}
}
优点:
缺点:
JetCache是阿里开源的一款Java缓存抽象框架,原生支持二级缓存,并提供丰富的缓存功能,如缓存自动刷新、异步加载、分布式锁等。它在API设计上类似Spring Cache,但功能更加强大和灵活。
org.springframework.boot
spring-boot-starter-web
com.alicp.jetcache
jetcache-starter-redis
2.7.1
# application.yml
jetcache:
statIntervalMinutes: 15
areaInCacheName: false
hidePackages: com.example
local:
default:
type: caffeine
limit: 1000
keyConvertor: fastjson
expireAfterWriteInMillis: 300000 # 5分钟
remote:
default:
type: redis
keyConvertor: fastjson
valueEncoder: java
valueDecoder: java
poolConfig:
minIdle: 5
maxIdle: 20
maxTotal: 50
host: ${redis.host}
port: ${redis.port}
expireAfterWriteInMillis: 1800000 # 30分钟
在启动类上启用JetCache:
@SpringBootApplication
@EnableMethodCache(basePackages = "com.example")
@EnableCreateCacheAnnotation
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
@Cached(name = "user:", key = "#id", cacheType = CacheType.BOTH, expire = 1800)
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
@Override
@CacheUpdate(name = "user:", key = "#user.id", value = "#user")
public User saveUser(User user) {
return userRepository.save(user);
}
@Override
@CacheInvalidate(name = "user:", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;
@CreateCache(name = "product:", cacheType = CacheType.BOTH, expire = 3600, localExpire = 600)
private Cache productCache;
@Override
public Product getProductById(Long id) {
// 自动加载功能,若缓存未命中,会执行lambda中的逻辑并将结果缓存
return productCache.computeIfAbsent(id, this::loadProduct);
}
private Product loadProduct(Long id) {
return productRepository.findById(id).orElse(null);
}
@Override
public Product saveProduct(Product product) {
Product savedProduct = productRepository.save(product);
productCache.put(product.getId(), savedProduct);
return savedProduct;
}
@Override
public void deleteProduct(Long id) {
productRepository.deleteById(id);
productCache.remove(id);
}
// 批量操作
@Override
public List getProductsByIds(List ids) {
Map productMap = productCache.getAll(ids);
List missedIds = ids.stream()
.filter(id -> !productMap.containsKey(id))
.collect(Collectors.toList());
if (!missedIds.isEmpty()) {
List missedProducts = productRepository.findAllById(missedIds);
Map missedProductMap = missedProducts.stream()
.collect(Collectors.toMap(Product::getId, p -> p));
// 更新缓存
productCache.putAll(missedProductMap);
// 合并结果
productMap.putAll(missedProductMap);
}
return ids.stream()
.map(productMap::get)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
@Service
public class StockServiceImpl implements StockService {
@Autowired
private StockRepository stockRepository;
// 自动刷新缓存,适合库存等频繁变化的数据
@CreateCache(name = "stock:",
cacheType = CacheType.BOTH,
expire = 60, // 1分钟后过期
localExpire = 10, // 本地缓存10秒过期
refreshPolicy = RefreshPolicy.BACKGROUND, // 后台刷新
penetrationProtect = true) // 防止缓存穿透
private Cache stockCache;
@Override
public Stock getStockById(Long productId) {
return stockCache.computeIfAbsent(productId, this::loadStock);
}
private Stock loadStock(Long productId) {
return stockRepository.findByProductId(productId).orElse(new Stock(productId, 0));
}
@Override
public void updateStock(Long productId, int newQuantity) {
stockRepository.updateQuantity(productId, newQuantity);
stockCache.remove(productId); // 直接失效缓存,后台自动刷新会加载新值
}
}
@RestController
@RequestMapping("/cache")
public class CacheStatsController {
@Autowired
private CacheManager cacheManager;
@GetMapping("/stats")
public Map getCacheStats() {
Collection caches = cacheManager.getCache(null);
Map statsMap = new HashMap<>();
for (Cache cache : caches) {
statsMap.put(cache.config().getName(), cache.getStatistics());
}
return statsMap;
}
}
优点:
缺点:
选择合适的二级缓存方案需要考虑项目规模、团队技术栈、性能需求、功能需求等多方面因素。
无论选择哪种方案,合理的缓存策略、完善的监控体系和优秀的运维实践都是构建高效缓存系统的关键。
在实际应用中,缓存并非越多越好,应当根据业务特点和系统架构,在性能、复杂度和一致性之间找到平衡点。