緩存 - 方法註解組件開發

来源:https://www.cnblogs.com/cnx01/archive/2022/10/23/16818865.html
-Advertisement-
Play Games

緩存概述 解決不同設備間速度不匹配問題。 互聯網分層架構:降低資料庫壓力,提升系統整體性能,縮短訪問時間。 高併發問題 緩存併發(擊穿):緩存過期後將嘗試從後端資料庫獲取數據 緩存穿透:不存在的 key,請求直接落庫查詢 緩存雪崩:緩存大面積失效,請求直接落庫查詢 需求說明 通過在方法上增加緩存註解 ...


緩存概述

解決不同設備間速度不匹配問題。
互聯網分層架構:降低資料庫壓力,提升系統整體性能,縮短訪問時間。

高併發問題

  • 緩存併發(擊穿):緩存過期後將嘗試從後端資料庫獲取數據
  • 緩存穿透:不存在的 key,請求直接落庫查詢
  • 緩存雪崩:緩存大面積失效,請求直接落庫查詢

需求說明

  1. 通過在方法上增加緩存註解,調用方法時根據指定 key 緩存返回數據,再次調用從緩存中獲取
  2. 可通過註解指定不同的緩存時長
  3. 避免緩存擊穿:緩存失效後使用互斥鎖限制查庫數量
  4. 避免緩存穿透:對於 null 支持短時間存儲
  5. 避免緩存雪崩:可支持每個 key 增加隨機時長

一、Spring Cache 整合 Redis 實現

利用 Spring Cache 處理 Redis 緩存數據
Spring Cache 註解 @Cacheable 攜帶的 sync() 屬性可支持互斥鎖限制單個線程處理,可避免緩存擊穿
註意開啟 Spring Cache 需要在配置類(或啟動類)上增加 @EnableCaching

1 :緩存管理器註入配置時,處理 緩存空間 cacheNames() / value() 與時長對應

可根據 配置或代碼寫死 指定不同 緩存空間 緩存時長
此方式以 緩存空間名 為標識區分時長,未配置的緩存空間走全局設定

點擊查看代碼 ExpandRedisConfig.java
# yml 配置
expand-cache-config:
    ttl-map: '{"yml-ttl":1000,"hello":2000}'

// 引入配置
@Value("#{${expand-cache-config.ttl-map:null}}")
private Map<String, Long> ttlMap;

/**
 * 註入緩存管理器及處理配置中的緩存時長
 */
@Bean(BEAN_REDIS_CACHE_MANAGER)
public RedisCacheManager expandRedisCacheManager(RedisConnectionFactory factory) {
    /*
        使用 Jackson 作為值序列化處理器
        FastJson 存在部分轉換問題如:Set 存儲後因為沒有對應的類型保存無法轉換為 JSONArray(實現 List ) 導致失敗
    */
    ObjectMapper om = JsonUtil.createJacksonObjectMapper();
    GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(om);

    // 配置key、value 序列化(解決亂碼的問題)
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            // key 使用 string 序列化方式
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8))
            // value 使用 jackson 序列化方式
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer))
            // 配置緩存空間名稱首碼
            .prefixCacheNameWith("spring:cache:")
            // 配置全局緩存過期時間
            .entryTtl(Duration.ofMinutes(30L));
    // 專門指定某些緩存空間的配置,如果過期時間,這裡的 key 為緩存空間名稱
    Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
    // 代碼寫死示例
    configMap.put("world", config.entryTtl(Duration.ofSeconds(60)));
    Set<Map.Entry<String, Long>> entrySet =
            Optional.ofNullable(ttlMap).map(Map::entrySet).orElse(Collections.emptySet());
    for (Map.Entry<String, Long> entry : entrySet) {
        // 指定特定緩存空間對應的過期時間
        configMap.put(entry.getKey(), config.entryTtl(Duration.ofSeconds(entry.getValue())));
    }
    
    RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(factory);
    // 使用自定義緩存管理器附帶自定義參數隨機時間,註意此處為全局設定,5-最小隨機秒,30-最大隨機秒
    return new ExpandRedisCacheManager(redisCacheWriter, config, configMap, 5, 30);
}

2 :在緩存空間名 cacheNames() / value() 中附帶時間字元串

自定義緩存管理器繼承 RedisCacheManager,重寫創建緩存處理器方法,拿到緩存空間名與緩存配置進行更新緩存時長處理
此方式以 緩存空間名中非指定時間部分 為標識區分時長,緩存空間名不指定時間走全局設定
使用示例:@Cacheable(cacheNames = "prefix#5m", cacheManager = "expandRedisCacheManager")

點擊查看代碼 ExpandRedisCacheManager.java
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
    String theName = name;
    if (name.contains(NAME_SPLIT_SYMBOL)) {
        // 名稱中存在#標記,修改實際名稱,替換預設配置的緩存時長為指定緩存時長
        String[] nameArr = name.split(NAME_SPLIT_SYMBOL);
        theName = nameArr[0];
        Duration duration = TimeUtil.parseDuration(nameArr[1]);
        if (duration != null) {
            cacheConfig = cacheConfig.entryTtl(duration);
        }
    }
    
    // 使用自定義緩存處理器附帶自定義參數隨機時間,將註入的隨機時間傳遞
    return new ExpandRedisCache(theName, cacheWriter, cacheConfig, this.minRandomSecond,
        this.maxRandomSecond);
}

3 :自定義緩存註解支持 Spring Cache

  • 自定義註解 使用 @Cacheable 標識,可支持 Spring Cache 處理
  • 自定義註解增加設置緩存時長的屬性: timeout() + unit() ,需要與註入註解的初始化配置方生效
  • 自定義緩存註解過期時間初始化配置,利用 Spring Component Bean 獲取到使用自定義註解的方法,利用反射獲取註解屬性並設置緩存空間過期時間;Map 處理,同一名稱緩存空間將會出現替換情景
  • 此處理實質仍是以緩存空間名 cacheNames() / value() 中非時間部分為標識區分時長
點擊查看代碼 ExpandCacheExpireConfig.java
/**
 * Spring Bean 載入後處理
 *  獲取所有 @Component 註解的 Bean 判斷類中方法是否存在 @SpringCacheable 註解,存在進行過期時間設置
 */
@PostConstruct
public void init() {
    Map<String, Object> beanMap = beanFactory.getBeansWithAnnotation(Component.class);
    if (MapUtil.isEmpty(beanMap)) {
        return;
    }

    beanMap.values().forEach(item ->
        ReflectionUtils.doWithMethods(item.getClass(), method -> {
            ReflectionUtils.makeAccessible(method);
            putConfigTtl(method);
        })
    );

    expandRedisCacheManager.initializeCaches();
}

/**
 * 利用反射設置方法註解上配置的過期時間
 * @param method 註解了自定義緩存的方法
 */
private void putConfigTtl(Method method) {
    ExpandCacheable annotation = method.getAnnotation(ExpandCacheable.class);
    if (annotation == null) {
        return;
    }

    String[] cacheNames = annotation.cacheNames();
    if (ArrayUtil.isEmpty(cacheNames)) {
        cacheNames = annotation.value();
    }

    // 反射獲取緩存管理器初始化配置並設值
    Map<String, RedisCacheConfiguration> initialCacheConfiguration =
            (Map<String, RedisCacheConfiguration>)
                ReflectUtil.getFieldValue(expandRedisCacheManager, "initialCacheConfiguration");
    RedisCacheConfiguration defaultCacheConfig =
            (RedisCacheConfiguration)
                ReflectUtil.getFieldValue(expandRedisCacheManager, "defaultCacheConfig");
    Duration ttl = Duration.ofSeconds(annotation.unit().toSeconds(annotation.timeout()));
    for (String cacheName : cacheNames) {
        initialCacheConfiguration.put(cacheName, defaultCacheConfig.entryTtl(ttl));
    }
}

4 :自定義緩存處理器,在設置緩存時處理時長

繼承 Spring 緩存處理器 RedisCache ,重寫設置緩存方法
可針對 null 進行短時間存儲避免緩存穿透、增加隨機時長避免緩存雪崩

點擊查看代碼 ExpandRedisCache.java
@Override
public void put(Object key, @Nullable Object value) {
    Object cacheValue = preProcessCacheValue(value);
    // 替換父類設置緩存時長處理
    Duration duration = getDynamicDuration(cacheValue);
    cacheWriter.put(name, createAndConvertCacheKey(key),
            serializeCacheValue(cacheValue), duration);
}

/**
 * 獲取動態時長
 */
private Duration getDynamicDuration(Object cacheValue) {
    // 如果緩存值為 null,固定返回時長為 30s 避免緩存穿透
    if (NullValue.INSTANCE.equals(cacheValue)) {
        return Duration.ofSeconds(30);
    }

    int randomInt = RandomUtil.randomInt(this.minRandomSecond, this.maxRandomSecond);
    return this.cacheConfig.getTtl().plus(Duration.ofSeconds(randomInt));
}

小結

  • 優點:使用 Spring 自帶功能,通用性強
  • 缺點:針對緩存空間處理緩存時長,緩存時間一致可能導致緩存雪崩,自定義處理需要理解相應源碼實現

參考:

二、自定義 AOP 實現

使用 Spring AOP 切麵對註解攔截以支持方法調用結果進行緩存。

1. 註解屬性定義

  • 支持不同類型緩存 key: key() + keyType()
  • 支持依據條件( SpEL 表達式)設定排除不走緩存: unless()
  • 支持緩存 key 自定義過期時長( Redis 緩存): timeout() + unit()
  • 支持緩存 key 自定義過期時長增加隨機時長( Redis 緩存): addRandomDuration() ,註意固定了隨機範圍,可避免緩存雪崩
  • 支持本地緩存設置:useLocal() + localTimeout() ,註意本地緩存存在全局最大時長限制

2. AOP 切麵處理

  • 緩存存儲數據時加鎖(synchronized)執行,避免緩存擊穿
  • 對 null 值進行固定格式字元串緩存,避免緩存穿透
點擊查看代碼 MethodCacheAspect.java
/**
 * 利用 AOP 環繞通知對註解方法返回進行緩存處理
 */
@Around("@annotation(cn.eastx.practice.demo.cache.config.custom.MethodCacheable)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodCacheableOperation operation = MethodCacheableOperation.convert(joinPoint);
    if (Objects.isNull(operation)) {
        return joinPoint.proceed();
    }

    Object result = getCacheData(operation);
    if (Objects.nonNull(result)) {
        return convertCacheData(result);
    }

    // 加鎖處理同步執行
    synchronized (operation.getKey().intern()) {
        result = getCacheData(operation);
        if (Objects.nonNull(result)) {
            return convertCacheData(result);
        }

        result = joinPoint.proceed();
        setDataCache(operation, result);
    }

    return result;
}

/**
 * 設置數據緩存
 *  特殊值緩存需要轉換,特殊值包括 null
 * @param operation 操作對象
 * @param data 數據
 */
private void setDataCache(MethodCacheableOperation operation, Object data) {
    // null緩存處理,固定存儲時長,防止緩存穿透
    if (Objects.isNull(data)) {
        redisUtil.setEx(operation.getKey(), NULL_VALUE, SPECIAL_VALUE_DURATION);
        return;
    }

    // 存在實際數據緩存處理
    redisUtil.setEx(operation.getKey(), data, operation.getDuration());
    if (Boolean.TRUE.equals(operation.getUseLocal())) {
        LocalCacheUtil.put(operation.getKey(), data, operation.getLocalDuration());
    }
}

小結

  • 優點:自定義 Spring AOP 實現,可定製化處理程度較高,當前以支持兩級緩存(Redis 緩存 + 本地緩存)
  • 缺點:相對於 Spring 自帶 Cache ,部分功能存在缺失不夠完善

其他

本文 demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-cache

推薦閱讀:

作者:EastX

本文來自博客園,轉載請註明原文鏈接:https://www.cnblogs.com/cnx01/p/16818865.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 簡介 工廠模式屬於創建型模式,可以分為三種:簡單工廠、工廠模式、抽象工廠。 通俗講就是用於如何優雅的創建對象而設計。當開發者不知道建什麼對象,或者創建方式過於複雜的時候去使用(比如引入一個大composer項目或大型sdk,有些時候確實不知道需要使用那些對象,此時就需要參考官方文檔,通過包里或sdk ...
  • 位置: from rest_framework.views import APIView 繼承APIView類視圖形式的路由: path('booksapiview/', views.BooksAPIView.as_view()), #在這個地方應該寫個函數記憶體地址 繼承APIView類的視圖函數: ...
  • 什麼是CopyOnWrite容器 【1】CopyOnWrite容器是基於併發模式Copy-on-Write模式(最簡單的併發解決方案)實現的用於避免共用的數據集合。 【2】CopyOnWrite容器又被成為寫時複製的容器,即當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行C ...
  • 年份天數 題目 輸入某年某月某日,判斷這一天是這一年的第幾天?特殊情況,閏年時需考慮二月多加一天 解答 year = int(input("input year: "))month = int(input("input month: "))day = int(input("input day: ") ...
  • 前言:本篇博客詳細介紹了項目管理工具 Git 的下載安裝、環境變數配置、使用以及一些常用命令,參考了網上一些博主的介紹。有些博客只介紹下載安裝,或者只介紹 Git 命令,沒有綜合到一起。通過閱讀此博文,能夠讓對 Git 的配置與使用達到比較通達的理解,如果喜歡,請點贊收藏。 博文目錄: 一、Git ...
  • 位置: 1.找到自己項目用的解釋器存儲位置 H:\pythonProject\Lib\site-packages\django\views\generic\base.py 在base.py里有一個View類 2.也可以通過from django.views import View 按住ctrl點擊V ...
  • 正則表達式02 5.4正則表達式語法02 5.4.6捕獲分組 詳見5.3.3 例子 package li.regexp; import java.util.regex.Matcher; import java.util.regex.Pattern; //演示分組 public class RegEx ...
  • Lists,提供了很多api方便操作。例如:Lists.partition(List list,int size) Lists.partition(List list,int size)將list集合進行切割然後填充到一個List集合里。官方介紹 使用場景: 比如記憶體中有大量數據,需要迴圈調用某個方 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...