Spring Cache 實戰指南

redis中常見的問題

前言

在本文中,我們將探討 Redis 在緩存中的應用,并解決一些常見的緩存問題。為了簡化理解,本文中的一些配置是直接寫死的,實際項目中建議將這些配置寫入配置文件,并通過配置文件讀取。

一、為什么需要緩存?

在Web應用開發中,頻繁的數據庫查詢和復雜的計算操作會顯著影響系統性能。為了提升系統的響應速度和整體性能,緩存機制成為了不可或缺的一部分。Spring Cache通過抽象緩存層,使開發者能夠通過簡單的注解實現方法級別的緩存,從而有效減少重復計算和數據庫訪問,顯著提升系統的響應速度。

前提

本文使用Redis作為緩存管理器(CacheManager),因此你需要確保正確引入并配置Redis。

引入與基本使用(此處由AI代寫,非本文重點)

Spring Cache快速配置

Java配置類示例:

@Configuration
@EnableCaching
@Slf4j
public class RedisCachingAutoConfiguration {@Resourceprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic CacheManager defaultCacheManager() {RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)).cacheDefaults(configuration).build();}
}

三、核心注解深度解析

1. @Cacheable:數據讀取緩存

@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {return userRepository.findById(userId).orElse(null);
}
  • value:指定緩存名稱(必填)
  • key:支持SpEL表達式生成緩存鍵
  • condition:方法執行前判斷(例如userId > 1000才緩存)
  • unless:方法執行后判斷(例如空結果不緩存)
屬性執行時機訪問變量作用場景
condition方法執行前判斷只能訪問方法參數(如 #argName決定是否執行緩存邏輯(包括是否執行方法體)
unless方法執行后判斷可以訪問方法參數和返回值(如 #result決定是否將方法返回值存入緩存(不影響是否執行方法體)

2. @CachePut:強制更新緩存

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {return userRepository.save(user);
}

適用場景:數據更新后同步緩存,確保后續讀取的是最新數據。

3. @CacheEvict:精準清除緩存

@CacheEvict(value = "users", key = "#userId", beforeInvocation = true)
public void deleteUser(Long userId) {userRepository.deleteById(userId);
}
  • 刪除指定條目:通過key精準定位
  • 清空整個緩存allEntries = true
  • beforeInvocation:方法執行前清除(避免執行失敗導致臟數據)

4. @Caching:組合操作

@Caching(put = @CachePut(value = "users", key = "#user.id"),evict = @CacheEvict(value = "userList", allEntries = true)
)
public User updateUserProfile(User user) {// 業務邏輯
}

5. @CacheConfig:類級別配置

@Service
@CacheConfig(cacheNames = "products")
public class ProductService {// 類中方法默認使用products緩存
}

工程化實踐解決方案

前面的示例內容由AI編寫,經過測試可用。然而,在實際使用中,這些用法可能不符合某些場景需求,或者使用起來不夠方便。以下是一些常見問題及解決方案:

  1. 自動生成的key格式為{cacheable.value}::{cacheable.key},為什么一定是"::"兩個冒號?
    (查看源碼org.springframework.data.redis.cache.CacheKeyPrefix
    如果需要為key統一加前綴,可以在RedisCacheConfiguration中設置。

  2. 批量刪除時,@CacheEvict不夠靈活。

    • 方案一:使用@CacheEvict并設置allEntriestrue,但這樣會刪除所有value相同的緩存,可能會誤刪不需要清除的數據。
    • 方案二:手動調用刪除緩存。
    • 方案三:自定義批量刪除緩存注解。
  3. 大部分場景下,使用某個固定屬性值作為緩存時,增刪改操作每次都要寫key取某個值,非常繁瑣。

    • 方案一:自定義KeyGenerator
  4. 高并發場景下如何確保數據的一致性和系統的穩定性?

    • 方案一:在單體架構中,可以在構建CacheManager時指定RedisCacheWriterlockingRedisCacheWriter,并在@CachePut@CacheEvict中指定帶鎖的CacheManager
    • 方案二:在集群環境中,可以在@CachePut@CacheEvict對應的方法上加分布式鎖(如Redisson)。
  5. 如何防止緩存雪崩?

    • 定義多個緩存管理器,每個管理器有不同的過期時間。
    • 在方法上指定使用哪個緩存管理器。
  6. 如何防止緩存穿透?

    • 緩存框架中允許緩存null,未找到的數據可以直接緩存空值。

統一修改前綴與定義key序列化

import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;import javax.annotation.Resource;
import java.time.Duration;/***redisCache配置** @author weiwenbin* @date 2025/03/11 下午5:15*/
@Configuration
@EnableCaching
@Slf4j
public class RedisCachingAutoConfiguration {@Resourceprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic CacheManager defaultNoLockingCacheManager() {String keyPre = "hatzi";String directoryName = "cache";RedisCacheConfiguration configuration = getCacheConfiguration(Duration.ofHours(1), keyPre, directoryName);return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)).cacheDefaults(configuration).build();}/*** 緩存的異常處理*/@Beanpublic CacheErrorHandler errorHandler() {// 異常處理,當Redis發生異常時,打印日志,但是程序正常走log.info("初始化 -> [{}]", "Redis CacheErrorHandler");return new CacheErrorHandler() {@Overridepublic void handleCacheGetError(RuntimeException e, Cache cache, Object key) {log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);}@Overridepublic void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);}@Overridepublic void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);}@Overridepublic void handleCacheClearError(RuntimeException e, Cache cache) {log.error("Redis occur handleCacheClearError:", e);}};}public static RedisCacheConfiguration getCacheConfiguration(Duration duration, String keyPre, String directoryName) {RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(duration);/*** 默認CacheKeyPrefix 中分隔符為"::" 我想改成":" 所以這樣寫* 20250315放棄serializeKeysWith是因為自定義批量刪除注解serializeKeysWith設置的前綴未生效*/configuration = configuration.computePrefixWith(cacheName -> {String pre = "";if (StrUtil.isNotBlank(keyPre)) {pre += keyPre + ":";}if (StrUtil.isNotBlank(directoryName)) {pre += directoryName + ":";}return pre + cacheName + ":";});return configuration;}
}

自定義KeyGenerator

自定義KeyGenerator

@Component
@Slf4j
public class PkKeyGenerator implements KeyGenerator {@Override@Nonnullpublic Object generate(@Nonnull Object target, @Nonnull Method method, Object... params) {if (params.length == 0) {log.info("PkKeyGenerator key defaultKey");return "defaultKey";}for (Object param : params) {if (param == null) {continue;}if (param instanceof PkKeyGeneratorInterface) {PkKeyGeneratorInterface pkKeyGenerator = (PkKeyGeneratorInterface) param;String key = pkKeyGenerator.cachePkVal();if (StrUtil.isBlank(key)) {return "defaultKey";}log.info("PkKeyGenerator key :{}", key);return key;}}log.info("PkKeyGenerator key defaultKey");return "defaultKey";}
}

自定義接口

public interface PkKeyGeneratorInterface {String cachePkVal();
}

入參實現接口

public class SysTenantQueryDTO implements PkKeyGeneratorInterface, Serializable {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "id")private Long id;@Overridepublic String cachePkVal() {return id.toString();}
}

注解中使用

@Cacheable(value = "sysTenant", keyGenerator = "pkKeyGenerator")
public SysTenantVO getVOInfoBy(SysTenantQueryDTO queryDTO) {// 業務代碼
}

自定義注解批量刪除

工具類

public class CacheDataUtils {/*** 批量鍵清除方法* 該方法用于從指定的緩存中清除一批鍵對應的緩存對象* 主要解決批量清除緩存的需求,提高緩存管理的靈活性和效率** @param cacheManager 緩存管理器,用于管理緩存* @param cacheName    緩存名稱,用于指定需要操作的緩存* @param keys         需要清除的鍵集合,這些鍵對應的緩存對象將會被清除*/public static void batchEvict(CacheManager cacheManager, String cacheName, Collection<?> keys) {// 檢查傳入的鍵集合是否為空,如果為空則直接返回,避免不必要的操作if (CollUtil.isEmpty(keys)) {return;}// 獲取指定名稱的緩存對象Cache cache = cacheManager.getCache(cacheName);// 檢查緩存對象是否存在,如果存在則逐個清除傳入的鍵對應的緩存對象if (cache != null) {keys.forEach(cache::evict);}}
}

自定義注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BatchCacheEvict {/*** 目標緩存名稱** @return String[]*/String[] cacheNames() default {};/*** 緩存鍵(SpEL表達式)** @return String*/String key();/*** 指定CacheManager Bean名稱** @return String*/String cacheManager() default "";/*** 是否在方法執行前刪除* 建議后置刪除** @return boolean*/boolean beforeInvocation() default false;/*** 條件表達式(SpEL)** @return String*/String condition() default "";
}

切面編程

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.hatzi.core.enums.SystemResultEnum;
import com.hatzi.core.exception.BaseException;
import com.hatzi.sys.cache.annotation.BatchCacheEvict;
import com.hatzi.sys.cache.util.CacheDataUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.cache.CacheManager;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;import java.util.Collection;/*** 批量清除緩存切面類* 用于處理帶有 @BatchCacheEvict 注解的方法,進行緩存的批量清除操作** @author weiwenbin*/
@Aspect
@Component
@Slf4j
public class BatchCacheEvictAspect {// SpEL 解析器private final ExpressionParser parser = new SpelExpressionParser();// 參數名發現器(用于解析方法參數名)private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();/*** 處理批量清除緩存的操作** @param joinPoint  切入點* @param batchEvict 批量清除緩存注解* @return 方法執行結果* @throws Throwable 可能拋出的異常*/@Around("@annotation(batchEvict)")public Object handleBatchEvict(ProceedingJoinPoint joinPoint, BatchCacheEvict batchEvict) throws Throwable {// 條件判斷if (StrUtil.isNotBlank(batchEvict.condition()) && !isConditionPassed(joinPoint, batchEvict.condition())) {log.info("handleBatchEvict isConditionPassed is false");return joinPoint.proceed();}// 空值檢查if (ArrayUtil.isEmpty(batchEvict.cacheNames()) || StrUtil.isEmpty(batchEvict.key())) {log.info("handleBatchEvict cacheNames or key is empty");return joinPoint.proceed();}// 前置刪除if (batchEvict.beforeInvocation()) {evictCaches(joinPoint, batchEvict);}try {Object result = joinPoint.proceed();// 后置刪除if (!batchEvict.beforeInvocation()) {evictCaches(joinPoint, batchEvict);}return result;} catch (Exception ex) {log.error(ex.getMessage());throw ex;}}/*** 執行緩存的批量清除操作** @param joinPoint  切入點* @param batchEvict 批量清除緩存注解*/private void evictCaches(ProceedingJoinPoint joinPoint, BatchCacheEvict batchEvict) {// 創建 SpEL 上下文EvaluationContext context = createEvaluationContext(joinPoint);String cachedManagerName = batchEvict.cacheManager();String keyExpr = batchEvict.key();String[] cacheNames = batchEvict.cacheNames();//獲取緩存對象CacheManager cacheManager = getCacheManager(cachedManagerName);//解析key的值Object key = parser.parseExpression(keyExpr).getValue(context);if (!(key instanceof Collection)) {log.error("keyExpr 類型錯誤必須是Collection的子類");throw new BaseException(SystemResultEnum.INTERNAL_SERVER_ERROR);}for (String cacheName : cacheNames) {CacheDataUtils.batchEvict(cacheManager, cacheName, (Collection<?>) key);}}/*** 創建 SpEL 上下文** @param joinPoint 切入點* @return SpEL 上下文對象*/private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 構建 SpEL 上下文(支持方法參數名解析)return new MethodBasedEvaluationContext(joinPoint.getTarget(),signature.getMethod(),joinPoint.getArgs(),parameterNameDiscoverer);}/*** 獲取緩存管理器對象** @param cacheManagerName 緩存管理器名稱* @return 緩存管理器對象*/private CacheManager getCacheManager(String cacheManagerName) {return StrUtil.isBlank(cacheManagerName) ?SpringUtil.getBean(CacheManager.class) :SpringUtil.getBean(cacheManagerName, CacheManager.class);}/*** 判斷條件是否滿足** @param joinPoint 切入點* @param condition 條件表達式* @return 是否滿足條件*/private boolean isConditionPassed(ProceedingJoinPoint joinPoint, String condition) {return Boolean.TRUE.equals(parser.parseExpression(condition).getValue(createEvaluationContext(joinPoint), Boolean.class));}
}

使用

@Override
@Transactional(rollbackFor = {Exception.class})
@BatchCacheEvict(cacheNames = "sysTenant", key = "#idList")
public Boolean delByIds(List<Long> idList) {// 手動刪除// CacheDataUtils.batchEvict(SpringUtil.getBean("defaultCacheManager", CacheManager.class),"sysTenant", idList);// 業務代碼
}

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

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

相關文章

區塊鏈開發技術公司:引領數字經濟的創新力量

在數字化浪潮席卷全球的今天&#xff0c;區塊鏈技術作為新興技術的代表&#xff0c;正以其獨特的去中心化、不可篡改和透明性等特點&#xff0c;深刻改變著各行各業的發展格局。區塊鏈開發技術公司&#xff0c;作為這一領域的先鋒和推動者&#xff0c;正不斷研發創新&#xff0…

EJS緩存解決多頁面相同閃動問題

基于 EJS 的模板引擎特性及其緩存機制&#xff0c;以下是關于緩存相同模塊的詳細解答&#xff1a; 一、EJS 緩存機制的核心能力 模板編譯緩存 EJS 默認會將編譯后的模板函數緩存在內存中&#xff0c;當相同模板文件被多次渲染時&#xff0c;會直接復用已編譯的模板函數&#x…

多條件排序(C# and Lua)

C# 升序排序 OrderBy 按升序對序列的元素進行排序 ThenBy 按升序對序列中的元素執行后續排序 降序排序 OrderByDescending 按降序對序列的元素排序 ThenByDescending 按降序對序列中的元素執行后續排序 public class Fruit {public int id;public string name;publi…

React19源碼系列之Hooks(useId)

useId的介紹 https://zh-hans.react.dev/reference/react/useId useId 是 React 18 引入的一個新 Hook&#xff0c;主要用于生成全局唯一的 ID。在開發中&#xff0c;我們經常需要為元素&#xff08;如表單元素、模態框等&#xff09;生成唯一 ID&#xff0c;以便在 JavaScri…

經典面試題:C/C++中static關鍵字的三大核心作用與實戰應用

一、修飾局部變量&#xff1a;改變生命周期&#xff0c;保留跨調用狀態 核心作用&#xff1a; ?延長生命周期&#xff1a;將局部變量從棧區移至靜態存儲區&#xff08;數據段或BSS段&#xff09;&#xff0c;生命周期與程序一致?保留狀態&#xff1a;變量在函數多次調用間保…

Redisson 分布式鎖原理

加鎖原理 # 如果鎖不存在 if (redis.call(exists, KEYS[1]) 0) then# hash結構,鎖名稱為key,線程唯一標識為itemKey&#xff0c;itemValue為一個計數器。支持相同客戶端線程可重入,每次加鎖計數器1.redis.call(hincrby, KEYS[1], ARGV[2], 1);# 設置過期時間redis.call(pexpi…

【數據結構】棧與隊列:基礎 + 競賽高頻算法實操(含代碼實現)

什么是棧&#xff1f;什么是隊列&#xff1f; 什么是先進后出&#xff1f;什么是先進先出&#xff1f; 了解基礎之后&#xff0c;又如何用來寫算法題&#xff1f; 帶著這些疑問&#xff0c;讓我帶領你&#xff0c;走進棧與隊列的世界 棧與隊列 棧&#xff1a; 1、棧的基本…

單元化架構在字節跳動的落地實踐

資料來源&#xff1a;火山引擎-開發者社區 什么是單元化 單元化的核心理念是將業務按照某種維度劃分成一個個單元&#xff0c; 理想情況下每個單元內部都是完成所有業務操作的自包含集合&#xff0c;能獨立處理業務流程&#xff0c;各個單元均有其中一部分數據&#xff0c;所有…

基于Python的垃圾短信分類

垃圾短信分類 1 垃圾短信分類問題介紹 1.1 垃圾短信 隨著移動互聯科技的高速發展&#xff0c;信息技術在不斷改變著我們的生活&#xff0c;讓我們的生活更方便&#xff0c;其中移動通信技術己經在我們生活起到至關重要的作用&#xff0c;與我們每個人人息息相關。短信作為移…

leetcode1971.尋找圖中是否存在路徑

初嘗并查集&#xff0c;直接套用模板 class Solution { private:vector<int> father;void init() {for(int i0;i<father.size();i)father[i]i;}int find(int v) {return vfather[v]?v:father[v]find(father[v]);//路徑壓縮}bool isSame(int u,int v){ufind(u);vfind…

QAI AppBuilder 快速上手(7):目標檢測應用實例

YOLOv8_det是YOLO 系列目標檢測模型&#xff0c;專為高效、準確地檢測圖像中的物體而設計。該模型通過引入新的功能和改進點&#xff0c;如因式分解卷積&#xff08;factorized convolutions&#xff09;和批量歸一化&#xff08;batch normalization&#xff09;&#xff0c;在…

景聯文科技:以高質量數據標注推動人工智能領域創新與發展

在當今這個由數據驅動的時代&#xff0c;高質量的數據標注對于推動機器學習、自然語言處理&#xff08;NLP&#xff09;、計算機視覺等領域的發展具有不可替代的重要性。數據標注過程涉及對原始數據進行加工&#xff0c;通過標注特定對象的特征來生成能夠被機器學習模型識別和使…

MySQL 索引下推

概念 索引下推&#xff08;Index Condition Pushdown&#xff0c;簡稱 ICP&#xff09; 是 MySQL 5.6 版本中提供的一項索引優化功能&#xff0c;它允許存儲引擎在索引遍歷過程中&#xff0c;執行部分 WHERE字句的判斷條件&#xff0c;直接過濾掉不滿足條件的記錄&#xff0c;…

NVIDIA Dynamo源碼編譯

Ref https://github.com/PyO3/maturin Rust 程序設計語言 代碼庫&#xff1a; https://github.com/ai-dynamo/dynamo https://github.com/ai-dynamo/nixl dynamo/container/Dockerfile.vllm 相關whl包 官方提供了4個whl包 ai_dynamo # 這個包ubuntu 22.04也可以用&…

【Android】安卓原生應用播放背景音樂與音效(筆記)

本文提供完整的音頻管理器代碼&#xff0c;涵蓋了背景音樂&#xff08;BGM&#xff09;和短音效的播放控制。無論是游戲中的音效&#xff0c;還是應用中的背景音樂&#xff0c;通過 AudioManager&#xff0c;你可以方便地管理和控制音頻資源。 前言 在 Android 開發中&#xf…

Unity | 游戲數據配置

目錄 一、ScriptableObject 1.創建ScriptableObject 2.創建asset資源 3.asset資源的讀取與保存 二、Excel轉JSON 1.Excel格式 2.導表工具 (1)處理A格式Excel (2)處理B格式Excel 三、解析Json文件 1.讀取test.json文件 四、相關插件 在游戲開發中,策劃…

2025信創即時通訊排行:安全合規與生態適配雙輪驅動

隨著信息技術應用創新&#xff08;信創&#xff09;戰略的深化&#xff0c;國產即時通訊工具在政企市場的滲透率顯著提升。2025年作為“十四五”規劃收官之年&#xff0c;信創產業迎來規模化應用關鍵節點。本文將從認證標準、市場表現、技術架構、行業適配四大維度&#xff0c;…

關于TVS管漏電流的問題?

問題描述&#xff1a; 在量產的帶電池故事機生產中&#xff0c;工廠產線測試電流時&#xff0c;有1臺機器電流比正常機器大10mA左右。 原因分析&#xff1a; 1、分析電路原理圖&#xff0c;去除可能出現問題的電壓或器件&#xff08;不影響系統&#xff09;&#xff0c;發現…

RAG 架構地基工程-Retrieval 模塊的系統設計分享

目錄 一、知識注入的關鍵前奏——RAG 系統中的檢索綜述 &#xff08;一&#xff09;模塊定位&#xff1a;連接語言模型與知識世界的橋梁 &#xff08;二&#xff09;核心任務&#xff1a;四大關鍵問題的協調解法 &#xff08;三&#xff09;系統特征&#xff1a;性能、精度…

Java-servlet(七)詳細講解Servlet注解

Java-servlet&#xff08;七&#xff09;詳細講解Servlet注解 前言一、注解的基本概念二、Override 注解2.1 作用與優勢2.2 示例代碼 三、Target 注解3.1 定義與用途3.2 示例代碼 四、WebServlet 注解4.1 作用4.2 示例代碼 五、反射與注解5.1 反射的概念5.2 注解與反射的結合使…