Spring Boot 的 3 種二級緩存落地方式

在高并發系統設計中,緩存是提升性能的關鍵策略之一。隨著業務的發展,單一的緩存方案往往無法同時兼顧性能、可靠性和一致性等多方面需求。

此時,二級緩存架構應運而生,本文將介紹在Spring Boot中實現二級緩存的三種方案。

一、二級緩存概述

1.1 什么是二級緩存

二級緩存是一種多層次的緩存架構,通常由以下兩個層次組成:

  • 一級緩存(本地緩存):直接在應用服務器內存中,訪問速度極快,但容量有限且在分布式環境下無法共享
  • 二級緩存(分布式緩存):獨立的緩存服務,如Redis或Memcached,可被多個應用實例共享,容量更大

二級緩存的工作流程通常是:先查詢本地緩存,若未命中則查詢分布式緩存,仍未命中才訪問數據庫,并將結果回填到各級緩存中。

1.2 為什么需要二級緩存

單一緩存方案存在明顯局限性:

  • 僅使用本地緩存:無法在分布式環境下保持數據一致性,每個實例都需要從數據庫加載數據
  • 僅使用分布式緩存:每次訪問都需要網絡IO,無法發揮本地緩存的性能優勢

二級緩存結合了兩者優勢:

  • 利用本地緩存的高性能,大幅減少網絡IO
  • 通過分布式緩存保證數據一致性
  • 減輕數據庫壓力,提高系統整體吞吐量
  • 更好的故障隔離,即使分布式緩存不可用,本地緩存仍可提供部分服務

二、Spring Cache + Redis方案

2.1 基本原理

該方案利用Spring Cache提供的緩存抽象,配合Caffeine(本地緩存)和Redis(分布式緩存)實現二級緩存。

Spring Cache提供了統一的緩存操作接口,可以通過簡單的注解實現緩存功能。

2.2 實現步驟

2.2.1 添加依賴
<dependencies><!-- Spring Boot Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 緩存支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency><!-- Redis支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Caffeine本地緩存 --><dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId></dependency><!-- 序列化支持 --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId></dependency>
</dependencies>
2.2.2 配置二級緩存管理器
@Configuration
@EnableCaching
public class CacheConfig {@Value("${spring.application.name:app}")private String appName;@Beanpublic 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<String, Cache> cacheMap = new ConcurrentHashMap<>();public LayeringCacheManager(CacheManager localCacheManager, CacheManager remoteCacheManager) {this.localCacheManager = localCacheManager;this.remoteCacheManager = remoteCacheManager;}@Overridepublic Cache getCache(String name) {return cacheMap.computeIfAbsent(name, cacheName -> {Cache localCache = localCacheManager.getCache(cacheName);Cache remoteCache = remoteCacheManager.getCache(cacheName);return new LayeringCache(localCache, remoteCache);});}@Overridepublic Collection<String> getCacheNames() {Set<String> 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;}@Overridepublic String getName() {return localCache.getName();}@Overridepublic Object getNativeCache() {return this;}@Overridepublic 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;}@Overridepublic <T> T get(Object key, Class<T> 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;}@Overridepublic <T> T get(Object key, Callable<T> 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);}}}@Overridepublic void put(Object key, Object value) {remoteCache.put(key, value);  // 先放入遠程緩存localCache.put(key, value);   // 再放入本地緩存}@Overridepublic void evict(Object key) {remoteCache.evict(key);  // 先清遠程緩存localCache.evict(key);   // 再清本地緩存}@Overridepublic void clear() {remoteCache.clear();  // 先清遠程緩存localCache.clear();   // 再清本地緩存}}}
}
2.2.3 使用緩存注解
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate 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);}
}
2.2.4 緩存同步問題

在分布式環境下,需要保證緩存一致性。我們可以通過Redis的發布訂閱機制實現:

@Configuration
public class CacheEvictionConfig {@Beanpublic RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);return container;}@Beanpublic 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));}}@Beanpublic CacheChangePublisher cacheChangePublisher(RedisTemplate<String, String> redisTemplate) {return new CacheChangePublisher(redisTemplate);}// 緩存變更消息發布器public static class CacheChangePublisher {private static final String CACHE_CHANGE_TOPIC = "cache:changes";private final RedisTemplate<String, String> redisTemplate;public CacheChangePublisher(RedisTemplate<String, String> 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@AllArgsConstructorpublic static class CacheChangeMessage {private String cacheName;private Object key;}
}

2.3 優缺點分析

優點:

  1. 集成Spring Cache,使用簡單,只需通過注解即可實現緩存功能
  2. 支持多種緩存實現的無縫切換
  3. 二級緩存邏輯集中管理,便于維護
  4. 支持緩存失效時間、容量等細粒度控制

缺點:

  1. 需要自行實現二級緩存管理器,代碼相對復雜
  2. 緩存同步需要額外實現,有一定復雜度
  3. 自定義緩存加載策略不夠靈活
  4. 對于復雜查詢場景支持有限

2.4 適用場景

  • 需要快速集成緩存功能的項目
  • 使用Spring框架且熟悉Spring Cache機制的團隊
  • 讀多寫少的業務場景
  • 對緩存一致性要求不是特別高的場景

三、自定義二級緩存框架

3.1 基本原理

該方案通過自定義緩存框架,精確控制緩存的讀寫流程、失效策略和同步機制,實現更加貼合業務需求的二級緩存。

這種方式雖然實現復雜度高,但提供了最大的靈活性和控制力。

3.2 實現步驟

3.2.1 定義緩存接口
public interface Cache<K, V> {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<K, V> {V load(K key);
}
3.2.2 實現本地緩存
public class LocalCache<K, V> implements Cache<K, V> {private final com.github.benmanes.caffeine.cache.Cache<K, V> cache;public LocalCache(long maximumSize, long expireAfterWriteSeconds) {this.cache = Caffeine.newBuilder().maximumSize(maximumSize).expireAfterWrite(expireAfterWriteSeconds, TimeUnit.SECONDS).recordStats().build();}@Overridepublic V get(K key) {return cache.getIfPresent(key);}@Overridepublic void put(K key, V value) {if (value != null) {cache.put(key, value);}}@Overridepublic void remove(K key) {cache.invalidate(key);}@Overridepublic void clear() {cache.invalidateAll();}@Overridepublic long size() {return cache.estimatedSize();}@Overridepublic boolean containsKey(K key) {return cache.getIfPresent(key) != null;}public CacheStats stats() {return cache.stats();}
}
3.2.3 實現Redis分布式緩存
public class RedisCache<K, V> implements Cache<K, V> {private final RedisTemplate<String, Object> redisTemplate;private final String cachePrefix;private final long expireSeconds;private final Class<V> valueType;public RedisCache(RedisTemplate<String, Object> redisTemplate, String cachePrefix, long expireSeconds,Class<V> valueType) {this.redisTemplate = redisTemplate;this.cachePrefix = cachePrefix;this.expireSeconds = expireSeconds;this.valueType = valueType;}private String getCacheKey(K key) {return cachePrefix + ":" + key.toString();}@Overridepublic V get(K key) {String cacheKey = getCacheKey(key);return (V) redisTemplate.opsForValue().get(cacheKey);}@Overridepublic void put(K key, V value) {if (value != null) {String cacheKey = getCacheKey(key);redisTemplate.opsForValue().set(cacheKey, value, expireSeconds, TimeUnit.SECONDS);}}@Overridepublic void remove(K key) {String cacheKey = getCacheKey(key);redisTemplate.delete(cacheKey);}@Overridepublic void clear() {Set<String> keys = redisTemplate.keys(cachePrefix + ":*");if (keys != null && !keys.isEmpty()) {redisTemplate.delete(keys);}}@Overridepublic long size() {Set<String> keys = redisTemplate.keys(cachePrefix + ":*");return keys != null ? keys.size() : 0;}@Overridepublic boolean containsKey(K key) {String cacheKey = getCacheKey(key);return Boolean.TRUE.equals(redisTemplate.hasKey(cacheKey));}
}
3.2.4 實現二級緩存
public class TwoLevelCache<K, V> implements Cache<K, V> {private final Cache<K, V> localCache;private final Cache<K, V> remoteCache;private final CacheLoader<K, V> cacheLoader;private final String cacheName;private final CacheEventPublisher eventPublisher;public TwoLevelCache(Cache<K, V> localCache, Cache<K, V> remoteCache, CacheLoader<K, V> cacheLoader,String cacheName,CacheEventPublisher eventPublisher) {this.localCache = localCache;this.remoteCache = remoteCache;this.cacheLoader = cacheLoader;this.cacheName = cacheName;this.eventPublisher = eventPublisher;}@Overridepublic 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;}@Overridepublic void put(K key, V value) {if (value != null) {// 先放入遠程緩存,再放入本地緩存remoteCache.put(key, value);localCache.put(key, value);}}@Overridepublic void remove(K key) {// 先清遠程緩存,再清本地緩存remoteCache.remove(key);localCache.remove(key);// 發布緩存失效事件if (eventPublisher != null) {eventPublisher.publishCacheEvictEvent(cacheName, key);}}@Overridepublic void clear() {// 先清遠程緩存,再清本地緩存remoteCache.clear();localCache.clear();// 發布緩存清空事件if (eventPublisher != null) {eventPublisher.publishCacheClearEvent(cacheName);}}@Overridepublic long size() {return remoteCache.size();}@Overridepublic boolean containsKey(K key) {return localCache.containsKey(key) || remoteCache.containsKey(key);}
}
3.2.5 緩存事件發布和訂閱
@Component
public class CacheEventPublisher {private final RedisTemplate<String, String> redisTemplate;private static final String CACHE_EVICT_TOPIC = "cache:evict";private static final String CACHE_CLEAR_TOPIC = "cache:clear";public CacheEventPublisher(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}public void publishCacheEvictEvent(String cacheName, Object key) {Map<String, Object> 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<String, Object> message = new HashMap<>();message.put("cacheName", cacheName);redisTemplate.convertAndSend(CACHE_CLEAR_TOPIC, JSON.toJSONString(message));}
}@Component
public class CacheEventListener {private final Map<String, TwoLevelCache<?, ?>> cacheMap;public CacheEventListener(RedisMessageListenerContainer listenerContainer, Map<String, TwoLevelCache<?, ?>> cacheMap) {this.cacheMap = cacheMap;// 監聽緩存失效事件MessageListener evictListener = (message, pattern) -> {String body = new String(message.getBody());Map<String, Object> map = JSON.parseObject(body, Map.class);String cacheName = (String) map.get("cacheName");Object key = map.get("key");TwoLevelCache<Object, Object> cache = (TwoLevelCache<Object, Object>) cacheMap.get(cacheName);if (cache != null) {// 只清除本地緩存,遠程緩存已經由發布者清除((LocalCache<Object, Object>)cache.getLocalCache()).remove(key);}};// 監聽緩存清空事件MessageListener clearListener = (message, pattern) -> {String body = new String(message.getBody());Map<String, Object> map = JSON.parseObject(body, Map.class);String cacheName = (String) map.get("cacheName");TwoLevelCache<Object, Object> cache = (TwoLevelCache<Object, Object>) cacheMap.get(cacheName);if (cache != null) {// 只清除本地緩存,遠程緩存已經由發布者清除((LocalCache<Object, Object>)cache.getLocalCache()).clear();}};listenerContainer.addMessageListener(evictListener, new ChannelTopic("cache:evict"));listenerContainer.addMessageListener(clearListener, new ChannelTopic("cache:clear"));}
}
3.2.6 緩存管理器
@Component
public class TwoLevelCacheManager {private final RedisTemplate<String, Object> redisTemplate;private final CacheEventPublisher eventPublisher;private final Map<String, TwoLevelCache<?, ?>> cacheMap = new ConcurrentHashMap<>();public TwoLevelCacheManager(RedisTemplate<String, Object> redisTemplate, CacheEventPublisher eventPublisher) {this.redisTemplate = redisTemplate;this.eventPublisher = eventPublisher;}public <K, V> TwoLevelCache<K, V> getCache(String cacheName, Class<V> valueType, CacheLoader<K, V> cacheLoader) {return getCache(cacheName, valueType, cacheLoader, 1000, 300, 3600);}@SuppressWarnings("unchecked")public <K, V> TwoLevelCache<K, V> getCache(String cacheName, Class<V> valueType, CacheLoader<K, V> cacheLoader,long localMaxSize,long localExpireSeconds,long remoteExpireSeconds) {return (TwoLevelCache<K, V>) cacheMap.computeIfAbsent(cacheName, name -> {LocalCache<K, V> localCache = new LocalCache<>(localMaxSize, localExpireSeconds);RedisCache<K, V> remoteCache = new RedisCache<>(redisTemplate, name, remoteExpireSeconds, valueType);return new TwoLevelCache<>(localCache, remoteCache, cacheLoader, name, eventPublisher);});}public Map<String, TwoLevelCache<?, ?>> getCacheMap() {return Collections.unmodifiableMap(cacheMap);}
}
3.2.7 使用示例
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate TwoLevelCacheManager cacheManager;private TwoLevelCache<Long, User> userCache;@PostConstructpublic void init() {userCache = cacheManager.getCache("user", User.class, this::loadUser, 1000, 300, 1800);}private User loadUser(Long id) {return userRepository.findById(id).orElse(null);}@Overridepublic User getUserById(Long id) {return userCache.get(id);}@Overridepublic User saveUser(User user) {User savedUser = userRepository.save(user);userCache.put(user.getId(), savedUser);return savedUser;}@Overridepublic void deleteUser(Long id) {userRepository.deleteById(id);userCache.remove(id);}
}

3.3 優缺點分析

優點:

  1. 完全自定義,可以根據業務需求靈活定制
  2. 精確控制緩存的加載、更新和失效邏輯
  3. 可以針對不同業務場景設計不同的緩存策略
  4. 緩存監控和統計更加全面

缺點:

  1. 開發工作量大,需要實現所有緩存邏輯
  2. 代碼復雜度高,需要考慮多種邊界情況
  3. 不能直接利用Spring等框架提供的緩存抽象
  4. 維護成本較高

3.4 適用場景

  • 對緩存性能和行為有精確控制需求的項目
  • 緩存策略復雜,標準框架難以滿足的場景
  • 大型項目,有專人負責緩存框架開發和維護
  • 特殊業務需求,如精確的過期策略、按條件批量失效等

四、JetCache框架方案

4.1 基本原理

JetCache是阿里開源的一款Java緩存抽象框架,原生支持二級緩存,并提供豐富的緩存功能,如緩存自動刷新、異步加載、分布式鎖等。它在API設計上類似Spring Cache,但功能更加強大和靈活。

4.2 實現步驟

4.2.1 添加依賴
<dependencies><!-- Spring Boot Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- JetCache核心 --><dependency><groupId>com.alicp.jetcache</groupId><artifactId>jetcache-starter-redis</artifactId><version>2.7.1</version></dependency>
</dependencies>
4.2.2 配置JetCache
# application.yml
jetcache:statIntervalMinutes: 15areaInCacheName: falsehidePackages: com.examplelocal:default:type: caffeinelimit: 1000keyConvertor: fastjsonexpireAfterWriteInMillis: 300000  # 5分鐘remote:default:type: rediskeyConvertor: fastjsonvalueEncoder: javavalueDecoder: javapoolConfig:minIdle: 5maxIdle: 20maxTotal: 50host: ${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);}
}
4.2.3 使用注解方式
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate 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);}
}
4.2.4 使用API方式
@Service
public class ProductServiceImpl implements ProductService {@Autowiredprivate ProductRepository productRepository;@CreateCache(name = "product:", cacheType = CacheType.BOTH, expire = 3600, localExpire = 600)private Cache<Long, Product> productCache;@Overridepublic Product getProductById(Long id) {// 自動加載功能,若緩存未命中,會執行lambda中的邏輯并將結果緩存return productCache.computeIfAbsent(id, this::loadProduct);}private Product loadProduct(Long id) {return productRepository.findById(id).orElse(null);}@Overridepublic Product saveProduct(Product product) {Product savedProduct = productRepository.save(product);productCache.put(product.getId(), savedProduct);return savedProduct;}@Overridepublic void deleteProduct(Long id) {productRepository.deleteById(id);productCache.remove(id);}// 批量操作@Overridepublic List<Product> getProductsByIds(List<Long> ids) {Map<Long, Product> productMap = productCache.getAll(ids);List<Long> missedIds = ids.stream().filter(id -> !productMap.containsKey(id)).collect(Collectors.toList());if (!missedIds.isEmpty()) {List<Product> missedProducts = productRepository.findAllById(missedIds);Map<Long, Product> 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());}
}
4.2.5 高級特性:自動刷新和異步加載
@Service
public class StockServiceImpl implements StockService {@Autowiredprivate StockRepository stockRepository;// 自動刷新緩存,適合庫存等頻繁變化的數據@CreateCache(name = "stock:", cacheType = CacheType.BOTH, expire = 60,  // 1分鐘后過期localExpire = 10,  // 本地緩存10秒過期refreshPolicy = RefreshPolicy.BACKGROUND,  // 后臺刷新penetrationProtect = true)  // 防止緩存穿透private Cache<Long, Stock> stockCache;@Overridepublic Stock getStockById(Long productId) {return stockCache.computeIfAbsent(productId, this::loadStock);}private Stock loadStock(Long productId) {return stockRepository.findByProductId(productId).orElse(new Stock(productId, 0));}@Overridepublic void updateStock(Long productId, int newQuantity) {stockRepository.updateQuantity(productId, newQuantity);stockCache.remove(productId);  // 直接失效緩存,后臺自動刷新會加載新值}
}
4.2.6 緩存統計與監控
@RestController
@RequestMapping("/cache")
public class CacheStatsController {@Autowiredprivate CacheManager cacheManager;@GetMapping("/stats")public Map<String, CacheStats> getCacheStats() {Collection<Cache> caches = cacheManager.getCache(null);Map<String, CacheStats> statsMap = new HashMap<>();for (Cache cache : caches) {statsMap.put(cache.config().getName(), cache.getStatistics());}return statsMap;}
}

4.3 優缺點分析

優點:

  1. 原生支持二級緩存,使用簡單
  2. 提供注解和API兩種使用方式,靈活性強
  3. 內置多種高級特性,如自動刷新、異步加載、分布式鎖等
  4. 完善的緩存統計和監控支持
  5. 社區活躍,文檔完善

缺點:

  1. 增加項目依賴,引入第三方框架
  2. 配置相對復雜
  3. 學習成本相對較高

4.4 適用場景

  • 需要開箱即用的二級緩存解決方案
  • 對緩存有豐富需求的項目,如自動刷新、異步加載等
  • 微服務架構,需要統一的緩存抽象

五、總結

選擇合適的二級緩存方案需要考慮項目規模、團隊技術棧、性能需求、功能需求等多方面因素。

無論選擇哪種方案,合理的緩存策略、完善的監控體系和優秀的運維實踐都是構建高效緩存系統的關鍵。

在實際應用中,緩存并非越多越好,應當根據業務特點和系統架構,在性能、復雜度和一致性之間找到平衡點。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/84176.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/84176.shtml
英文地址,請注明出處:http://en.pswp.cn/web/84176.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Android Studio Profiler使用

一:memory 參考文獻: AndroidStudio之內層泄漏工具Profiler使用指南_android studio profiler-CSDN博客

Zephyr boot

<!DOCTYPE html> <html lang"zh-CN"> <head> <meta charset"UTF-8"> <meta name"viewport" content"widthdevice-width, initial-scale1.0"> <title>Zephyr設備初始化機制交互式解析…

騰訊地圖Web版解決熱力圖被輪廓覆蓋的問題

前言 你好&#xff0c;我是喵喵俠。 還記得那天傍晚&#xff0c;我正對著電腦調試一個騰訊地圖的熱力圖頁面。項目是一個區域人流密度可視化模塊&#xff0c;我加了一個淡藍色的輪廓圖層用于表示區域范圍&#xff0c;熱力圖放在下面用于展示人流熱度。效果一預覽&#xff0c;…

【JVMGC垃圾回收場景總結】

文章目錄 CMS在并發標記階段&#xff0c;已經被標記的對象&#xff0c;又被新生代跨帶引用&#xff0c;這時JVM會怎么處理?為什么 Minor GC 會發生 STW&#xff1f;有哪些對象是在棧上分配的&#xff1f;對象在 JVM 中的內存結構為什么需要對齊填充&#xff1f;JVM 對象分配空…

3_STM32開發板使用(STM32F103ZET6)

STM32開發板使用(STM32F103ZET6) 一、概述 當前所用開發板為正點原子精英板,MCU: STM32F103ZET6。一般而言,拿到板子之后先要對板子有基礎的認識,包括對開發板上電開機、固件下載、調試方法這三個部分有基本的掌握。 二、系統開機 2.1 硬件連接 直接接電源線或Type-c線…

crackme012

crackme012 名稱值軟件名稱attackiko.exe加殼方式無保護方式serial編譯語言Delphi v1.0調試環境win10 64位使用工具x32dbg,PEid破解日期2025-06-18 -發現是 16位windows 程序環境還沒搭好先留坑

CppCon 2016 學習:I Just Wanted a Random Integer

你想要一個隨機整數&#xff0c;用于模擬隨機大小的DNA讀取片段&#xff08;reads&#xff09;&#xff0c;希望覆蓋不同長度范圍&#xff0c;也能測試邊界情況。 代碼部分是&#xff1a; #include <cstdlib> auto r std::rand() % 100;它生成一個0到99之間的隨機整數&…

MySQL層級查詢實戰:無函數實現部門父路徑

本次需要擊斃的MySQL函數 函數主要用于獲取部門的完整層級路徑&#xff0c;方便在應用程序或SQL查詢中直接調用&#xff0c;快速獲得部門的上下級關系信息。執行該函數之后簡單使用SQL可以實現數據庫中部門名稱查詢。例如下面sql select name,GetDepartmentParentNames(du.de…

Python初學者教程:如何從文本中提取IP地址

Python初學者教程:如何從文本中提取IP地址 在網絡安全和數據分析領域,經常需要從文本文件中提取IP地址。本文將引導您使用Python創建一個簡單但實用的工具,用于從文本文件提取所有IP地址并將其保存到新文件中。即使您是編程新手,也可以跟隨本教程學習Python的基礎知識! …

【Redis】Redis核心探秘:數據類型的編碼實現與高速訪問之道

&#x1f4da;?前言 &#x1f31f;&#x1f31f;&#x1f31f;精彩導讀 本次我們將全面剖析Redis的核心技術要點&#xff0c;包括其豐富的數據類型體系、高效的編碼方式以及秒級響應的性能奧秘。對于渴望深入理解Redis底層機制的技術愛好者&#xff0c;這是一次難得的學習機會…

Halcon —— 多種二維碼檢測

工業視覺實戰&#xff1a;Halcon多類型二維碼識別技術詳解 在工業自動化場景中&#xff0c;兼容多種二維碼類型是提高生產線靈活性的關鍵。本文將深入解析Halcon實現Data Matrix、QR Code和PDF417三種主流二維碼的兼容識別方案&#xff0c;并重點解釋核心算子參數。 一、多類型…

安卓vscodeAI開發實例

前言 前些天發現了一個巨牛的人工智能免費學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到網站 目錄 一、安卓開發基礎與工具鏈革新 1.1 Android Studio的局限性分析 1.2 VSCode在移動開發中的崛起 1.3 跨平臺開發工具鏈對比…

③通用搜索---解析FastAdmin中的表格列表的功能

深度解析FastAdmin中的表格列表的功能-CSDN博客文章瀏覽閱讀25次。本文將FastAdmin框架的CRUD功能配置要點進行了系統梳理。官方文檔與開發經驗相結合&#xff0c;詳細介紹了菜單顯示、TAB過濾、通用搜索、工具欄按鈕、動態統計、快速搜索等17項功能的配置方法。包括字段渲染&a…

DeepSeek 助力 Vue3 開發:打造絲滑的日歷(Calendar),日歷_項目里程碑示例(CalendarView01_22)

前言&#xff1a;哈嘍&#xff0c;大家好&#xff0c;今天給大家分享一篇文章&#xff01;并提供具體代碼幫助大家深入理解&#xff0c;徹底掌握&#xff01;創作不易&#xff0c;如果能幫助到大家或者給大家一些靈感和啟發&#xff0c;歡迎收藏關注哦 &#x1f495; 目錄 Deep…

Python爬蟲實戰:獲取Diesel電商數據并分析

1. 引言 在當今數字化時代,電商平臺積累了海量的產品和用戶數據。通過對這些數據的挖掘和分析,企業可以深入了解市場動態、消費者需求和競爭態勢,從而制定更有效的營銷策略和產品規劃。Diesel 作為知名的時尚品牌,其在電商平臺上的表現備受關注。本研究旨在通過 Python 爬…

Spring RestTemplate + MultiValueMap vs OkHttp 多值參數的處理

&#x1f4cc; Spring RestTemplate vs OkHttp&#xff1a;多值參數處理 一、MultiValueMap 與 FormBody 的差異 特性RestTemplate MultiValueMapOkHttp FormBody多值參數支持? 原生支持&#xff08;add("key", "value") 自動追加&#xff09;? 需顯…

GelSight視觸覺3D輪廓儀賦能Beomni人形機器人觸覺遙測,開啟人形機器人觸覺應用新場景

在智能制造、航空航天等領域&#xff0c;傳統機器人常面臨操作精度不足、環境適應力弱等問題。GelSight觸覺傳感技術與Beomni人形機器人的融合&#xff0c;為這些場景提供了新可能 —— 通過亞微米級觸覺感知能力&#xff0c;操作員可遠程感知物體表面細節&#xff0c;在復雜環…

python設置word的字體顏色

這個錯誤是由于python-docx的RGBColor對象沒有.rgb屬性導致的。正確的屬性訪問方式是分別獲取紅、綠(g)、藍(b)三個分量。以下是修復方案&#xff1a; 錯誤原因分析 RGBColor對象的結構如下&#xff1a; from docx.shared import RGBColorcolor RGBColor(255, 204, 51) pri…

推薦模型之GBDT-LR

一、概念 GBDT-LR模型由FaceBook&#xff08;現在的Meta&#xff09;團隊于2014年在論文《Practial Lessons from Predicting Clicks on Ads at Facebook》中提出&#xff0c;目標是用于預測FaceBook的廣告點擊量&#xff08;實際上廣告和推薦領域很多算法模型都是共用的&#…

Java實現Excel圖片URL篩選與大小檢測

Java實現Excel圖片URL篩選與大小檢測 在數據處理場景中&#xff0c;我們常需篩選Excel中的圖片URL。本文分享一個完整的Java方案&#xff0c;涵蓋從讀取圖片URL到檢測有效性、篩選大小&#xff0c;再到生成新Excel文件的全過程&#xff0c;同時講解開發與優化過程&#xff0c;…