iT邦幫忙

2

VScode 開發應用系統專案(7 -3 ) - Spring Boot Cache — L1 + L2 快取暫存處理

  • 分享至 

  • xImage
  •  

Spring Boot Cache — Redis 快取暫存處理 (提供開關)

概述

  • 之前發文分享過 Spring Boot Cache — Caffeine & Redis 快取暫存處理,後來遇到系統可能沒有 Redis Server而無法使用 L2 Cache的狀況,所以調整架構,增加一個 redis.l2.cache.enabled 開關,以便關閉Redis L2 Cache,單使用Caffeine L1 Cache。
  • 另外每個Cache Method 應更有不同的 TTL (暫存時間),所以調整架構,增加CachePolicy物件,分別對Caffeine L1 Cache 以及Redis L2 Cache 分別設定各自cacheManager。

準備與檢核

  1. VScode 開發應用系統專案 (1) - 啟動Spring Boot Web專案。
  1. 工具類程式已經準備好可以使用。
  1. Spring Boot資料庫存取密碼保護。
  1. Spring Boot Cache — Caffeine 快取暫存處理
  1. Spring Boot Cache — Redis 快取暫存處理

一、總覽 Spring Boot Redis 設定檔案

1. application.properties

  • 新增redis.l2.cache.enabled 開關變數與Redis相關設定。
    實務上正確做法應該於不同環境例如 application-dev.properties/application-uat.properties 分別增加一個 redis.l2.cache.enabled變數,這裡先設定於通用的application.properties

關閉Redis相關設定

---省略其他設定---

#L2 Cache 關閉 (Redis) 
spring.main.allow-bean-definition-overriding=true
redis.l2.cache.enabled=false
# 關閉 Spring Data Redis repositories 功能,避免不必要的 Redis 相關功能被啟用
spring.data.redis.repositories.enabled=false
# 指定 Session 儲存庫為 none,表示不使用任何 Session 儲存庫,適用於不需要 Session 管理的應用程式
spring.session.store-type=none

開啟Redis相關設定

---省略其他設定---

# 當 Redis 開啟時 (L1 + L2 模式) ---
# 建議同時設定 Session 刷新模式(由 Redis 索引管理)
spring.main.allow-bean-definition-overriding=true
redis.l2.cache.enabled=true
# 指定 Session 儲存庫為 Redis
spring.session.store-type=redis
# 使用 Redis 索引管理 Session 刷新,確保 Session 在 Redis 中的有效性和一致性
spring.session.redis.repository-type=indexed
# Redis 連線設定
redis_host=redis.lewishome.tw
redis_port=6379
# 若沒有實作加密機制,則直接使用明文密碼(不建議)
redis_password=${KEYSTORE:redis_password}

**調整後的 application.properties **

#各類資料庫的 url 參考:
# H2 database ==> jdbc:h2:mem:testdb
# SQL-Server  ==> jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
#  example    ==> jdbc:sqlserver://sql.lewis-home.tw:1433;databasename=MBS;encrypt=false
# MySQL       ==> jdbc:mysql://[hosts][:portNumber][/database]
#  example    ==> jdbc:mysql://mysql.lewis-home.tw:33060
# AS400(Jt400)==> jdbc:as400://[hosts][;property=value]  
#  example    ==> jdbc:as400://as400system;transaction isolation=none;translate binary=true;date format=iso;prompt=false
# Oracle      ==> jdbc:oracle:thin:@[HOST][:PORT]:SID or jdbc:oracle:thin:@//[HOST][:PORT]/SERVICE
#  example    ==> jdbc:oracle:thin:@oracle.lewis-home.tw:1521:oracle.lewis-home.tw
# postgres    ==> jdbc:postgresql://@[netloc][:port][/dbname][?param1=value1&...]
#  example    ==> jdbc:postgresql://postsql.lewis-home.tw:5432/database

#各類資料庫的 driver class Name
# H2 database ==> org.h2.Driver
# SQL-Server  ==> com.microsoft.sqlserver.jdbc.SQLServerDriver
# MySQL       ==> com.mysql.jdbc.Driver
# AS400(Jt400)==> com.ibm.as400.access.AS400JDBCDriver
# Oracle      ==> oracle.jdbc.driver.OracleDriver
# postgres    ==> org.postgresql.Driver

#Store primary Datasource (這些是自訂的變數名稱,只要與程式內取用的設定一致即可)
primary.datasource.enabled=true
primary.datasource.jdbcurl=jdbc:sqlserver://sql.lewishome.tw:1433;databasename=DbMuser1;encrypt=false;characterEncoding=utf-8
primary.datasource.username=Muser1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
# Encrypted in KeyStore with alias: primary_datasource_password
primary.datasource.password=${KEYSTORE:primary_datasource_password}
primary.datasource.driver_class_name=com.microsoft.sqlserver.jdbc.SQLServerDriver
primary.datasource.hibernate.hbm2ddl.auto=update


#Secondary Datasource 是否有需要
secondary.datasource.enabled=true
# secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/pdb1
secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/orclpdb.lewishome.tw
secondary.datasource.username=OUSER1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
# Encrypted in KeyStore with alias: secondary_datasource_password
secondary.datasource.password=${KEYSTORE:secondary_datasource_password}
secondary.datasource.driver_class_name=oracle.jdbc.OracleDriver
secondary.datasource.hibernate.hbm2ddl.auto=update
# secondary.datasource.hibernate.dialect=org.hibernate.dialect.Oracle12cDialect

# # CSRF protection whitelist - add /doMenu endpoint
# csrf.endpoint.white-list=/jwtAuth/**;/systemApiTest/**;/opt/fonts/**;{base}/callback/**;/home;/doMenu

# mail_resource_jndi=java=jboss/mail/mailSmtp
mail.smtp.host=mail.lewis-home.tw
mail.smtp.port=25
mail.sender=lewis@lewis-home.tw
mail.sender_name=lewis.yang
mail.subject.prefix=[WebAppSystem]


# #L2 Cache 關閉 (Redis) 
# redis.l2.cache.enabled=false
# # 關閉 Spring Data Redis repositories 功能,避免不必要的 Redis 相關功能被啟用
# spring.data.redis.repositories.enabled=false
# # 指定 Session 儲存庫為 none,表示不使用任何 Session 儲存庫,適用於不需要 Session 管理的應用程式
# spring.session.store-type=none


# 當 Redis 開啟時 (L1 + L2 模式) ---
# 建議同時設定 Session 刷新模式(由 Redis 索引管理)
spring.main.allow-bean-definition-overriding=true
redis.l2.cache.enabled=true
# 指定 Session 儲存庫為 Redis
spring.session.store-type=redis
# 使用 Redis 索引管理 Session 刷新,確保 Session 在 Redis 中的有效性和一致性
spring.session.redis.repository-type=indexed
# Redis 連線設定
redis_host=redis.lewishome.tw
redis_port=6379
# 若沒有實作加密機制,則直接使用明文密碼(不建議)
redis_password=${KEYSTORE:redis_password}

2. WebappApplication.java
位置: tw.lewishome.webapp;

  • 因為pom 有引用 spring-session-data-redis & spring-boot-starter-data-redis套件,單我們可能會關閉 Redis (redis.l2.cache.enabled=false),所以於 Sring boot 啟動時,排除Redis 相關auto configuration,
---省略其他設定---
@SpringBootApplication(exclude = {
		// 當Redis開關關閉時,我們不希望 Spring Session 去碰 Redis
		RedisAutoConfiguration.class
		,RedisRepositoriesAutoConfiguration.class
		// ,SessionAutoConfiguration.class
})

**調整後的 WebappApplication.java **

package tw.lewishome.webapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;

import lombok.extern.slf4j.Slf4j;

/**
 * WebappApplication 是 Spring Boot 應用程式的主要入口點。
 * 此類別負責啟動應用程式並加載 Spring 上下文。
 * 
 * @author Lewis
 * @version 1.0
 */
@SpringBootApplication(exclude = {
		// 當Redis開關關閉時,我們不希望 Spring Session 去碰 Redis
		RedisAutoConfiguration.class
		,RedisRepositoriesAutoConfiguration.class
		// ,SessionAutoConfiguration.class
})
@Slf4j
public class WebappApplication {

	/**
	 * Fix for javadoc warning :
	 * use of default constructor, which does not provide a comment
	 * Constructs a new AsyncServiceWorkerSample instance.
	 * This is the default constructor, implicitly provided by the compiler
	 * if no other constructors are defined.
	 */
	private WebappApplication() {
		// This constructor is intentionally empty. Nothing special is needed here.
	}

	/**
	 * 主方法是應用程式的進入點。
	 * 此方法啟動 Spring 應用程式。
	 *
	 * @param args 命令列參數
	 */
	public static void main(String[] args) {
		log.info("┌────────────────────────────────────────────────────┐");
		log.info("│ SpringApplication WebappApplication started!       │");
		log.info("└────────────────────────────────────────────────────┘");
		SpringApplication springApplication = new SpringApplication(WebappApplication.class);
		// 手動添加 EnvironmentPostProcessor,以確保在應用程式啟動時處理安全屬性(如加密的密碼)
		springApplication.addInitializers(new PropertySecureProcessInitializer(
				new PropertySecureProcessor(),
				new PropertySecureConverter()));
		springApplication.run(args);
	}

}

3. CachePolicy.java

-位置: package tw.lewishome.webapp.base.cache;

package tw.lewishome.webapp.base.cache;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * 集中定義系統中所有快取空間的名稱與過期時間
 */
public class CachePolicy {

    // 統一管理快取名稱字串,避免 typo
    public static final String USER_MENU_ITEMS = "catchUserMenuItems";
    public static final String USER_AUTH_SEQ = "catchUserAuthSeq";
    public static final String SHORT_LIVED = "shortLivedCache";

    /**
     * L1 (Caffeine) 的特定配置 
     * 回傳 Map<CacheName, TTL_Minutes>
     */
    public static Map<String, Integer> getL1CustomPolicies() {
        Map<String, Integer> policies = new HashMap<>();
        policies.put(USER_MENU_ITEMS, 1); // 1 分鐘 (測試用)
        policies.put(USER_AUTH_SEQ, 3);    // 3 分鐘 (對應原本的預設)
        policies.put(SHORT_LIVED, 1);      // 1 分鐘
        return policies;
    }

    /**
     * L2 (Redis) 的特定配置
     * 回傳 Map<CacheName, Duration>
     */
    public static Map<String, Duration> getL2CustomPolicies() {
        Map<String, Duration> policies = new HashMap<>();
        policies.put(USER_MENU_ITEMS, Duration.ofMinutes(30));
        policies.put(USER_AUTH_SEQ, Duration.ofHours(12));
        return policies;
    }
}

4. CaffeineConfiguration.java

-位置: package tw.lewishome.webapp.base.cache.caffeine;

package tw.lewishome.webapp.base.cache.caffeine;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.AnnotationCacheOperationSource;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Role;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.CachePolicy;
import tw.lewishome.webapp.base.cache.redis.MultiCacheInterceptor;

/**
 * Caffeine 快取設定(CaffeineConfiguration)。
 * 
 * 修正重點:
 * 1. 統一管理攔截器,解決 NoUniqueBeanDefinitionException。
 * 2. 自動探測 RedisManager (L2),支援動態聯動與降級。
 */
@EnableCaching
@Configuration
@Slf4j
public class CaffeineConfiguration {

    /**
     * 建立 L1 (Caffeine) CacheManager
     */
    @Bean(name = "caffeineCacheManager")
    @Primary
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public static CacheManager caffeineCacheManager() {
        SimpleCacheManager cacheManager = new SimpleCacheManager();
        List<CaffeineCache> caches = new ArrayList<>();

        CachePolicy.getL1CustomPolicies().forEach((name, ttlMinutes) -> {
            caches.add(createCaffeineCache(name, ttlMinutes));
        });

        cacheManager.setCaches(caches);
        log.info("┌────────────────────────────────────────────────────┐");
        log.info("│ [L1 Cache] CaffeineCacheManager 初始化完成         │");
        log.info("└────────────────────────────────────────────────────┘");
        return cacheManager;
    }

    /**
     * 全系統唯一的 Primary 攔截器。
     * 負責整合 L1 (Caffeine) 與探測到的 L2 (Redis)。
     */
    @Bean(name = "multiCacheInterceptor") // 使用自定義名稱避免與 Spring 內建衝突
    @Primary
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public static CacheInterceptor customerCacheInterceptor(
            @Qualifier("caffeineCacheManager") CacheManager caffeineCacheManager,
            ObjectProvider<CacheManager> allManagersProvider) {

        MultiCacheInterceptor interceptor = new MultiCacheInterceptor();

        // 1. 設定註冊解析器 (使用正確的 AnnotationCacheOperationSource)
        interceptor.setCacheOperationSources(new AnnotationCacheOperationSource());

        // 2. 注入 L1 Manager 給自定義邏輯使用
        interceptor.setCaffeineCacheManager(caffeineCacheManager);

        // 3. 動態探測 L2 Manager (排除掉自己,剩下的就是 RedisCacheManager)
        CacheManager redisManager = allManagersProvider.orderedStream()
                .filter(cm -> cm != caffeineCacheManager)
                .findFirst()
                .orElse(null);

        if (redisManager != null) {
            // 重要:必須設定父類別 Manager 為 Redis,super.doGet/doPut 才會生效
            interceptor.setCacheManager(redisManager);
            log.info("┌────────────────────────────────────────────────────┐");
            log.info("│ [Cache] L1+L2 (Caffeine + Redis) 聯動模式啟動中... │");
            log.info("└────────────────────────────────────────────────────┘");
        } else {
            // 降級:僅使用 Caffeine
            interceptor.setCacheManager(caffeineCacheManager);
            log.warn("┌────────────────────────────────────────────────────┐");
            log.warn("│ [Cache] 未偵測到 L2,系統僅以 L1 (Caffeine) 模式運作 │");
            log.warn("└────────────────────────────────────────────────────┘");
        }

        return interceptor;
    }

    private static CaffeineCache createCaffeineCache(String name, int ttlMinutes) {
        return new CaffeineCache(name, Caffeine.newBuilder()
                .expireAfterWrite(ttlMinutes, TimeUnit.MINUTES)
                .maximumSize(1000)
                .recordStats()
                .build());
    }
}

4. RedisConfiguration.java

-位置: package tw.lewishome.webapp.base.cache.redis;

package tw.lewishome.webapp.base.cache.redis;

import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.CachePolicy;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
import tw.lewishome.webapp.base.utility.common.TypeConvert;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.AnnotationCacheOperationSource;
import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Role;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * Redis 二級快取配置 (L2)。
 * 整合已存在的 CaffeineCacheManager (L1) 並註冊 MultiCacheInterceptor。
 */
@Configuration
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnProperty(name = "redis.l2.cache.enabled", havingValue = "true")
public class RedisConfiguration {

    @Autowired
    private SystemEnvReader systemEnvReader;

    /**
     * 【核心聯動】註冊自定義的 MultiCacheInterceptor。
     * 透過 @Primary 確保標註了 @Cacheable 的方法優先使用此攔截器。
     * 
     * @param redisCacheManager    由下方 Bean 產生的 L2 管理器
     * @param caffeineCacheManager 由 CaffeineConfiguration 產生的 L1 管理器
     */
    @Bean(name = "multiCacheInterceptor") // 使用具體名稱避免與內建 Bean 混淆
    @Primary
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheInterceptor cacheInterceptor(
            @Qualifier("redisCacheManager") CacheManager redisCacheManager,
            @Qualifier("caffeineCacheManager") CacheManager caffeineCacheManager) {

        log.info("┌────────────────────────────────────────────────────┐");
        log.info("│ [Cache] L1 (Caffeine) + L2 (Redis) 聯動模式啟動    │");
        log.info("└────────────────────────────────────────────────────┘");

        MultiCacheInterceptor interceptor = new MultiCacheInterceptor();

        // 1. 注入 L1 管理器:供 MultiCacheInterceptor.doGet 優先從記憶體讀取
        interceptor.setCaffeineCacheManager(caffeineCacheManager);

        // 2. 注入 L2 管理器:供父類別處理 Redis 存取。
        // 解決「Redis 讀不到資料」的關鍵:必須讓父類別知道主要 CacheManager 是誰
        interceptor.setCacheManager(redisCacheManager);

        // 3. 設定註冊解析器:讀取方法上的 @Cacheable 註解參數
        interceptor.setCacheOperationSources(new AnnotationCacheOperationSource());

        return interceptor;
    }

    @Bean(name = "redisCacheManager")
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        String redisTimeToLife = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_LIFE_HOUR", "1") : "1";
        int defaultHours = TypeConvert.toInteger(redisTimeToLife);

        // 預設配置:包含 JSON 序列化與預設過期時間
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(defaultHours))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultConfig)
                .withInitialCacheConfigurations(getL2Configurations(defaultConfig))
                .build();
    }

    private Map<String, RedisCacheConfiguration> getL2Configurations(RedisCacheConfiguration defaultConfig) {
        Map<String, RedisCacheConfiguration> configurations = new HashMap<>();
        // 根據 CachePolicy 設定不同區塊的 TTL
        CachePolicy.getL2CustomPolicies().forEach((name, ttl) -> {
            configurations.put(name, defaultConfig.entryTtl(ttl));
        });
        return configurations;
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public LettuceConnectionFactory redisConnectionFactory() {
        try {
            RedisStandaloneConfiguration config = getRedisStandaloneConfiguration();
            return new LettuceConnectionFactory(config);
        } catch (Exception ex) {
            log.error("CRITICAL: Failed to create RedisConnectionFactory: {}", ex.getMessage());
            return null;
        }
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    private RedisStandaloneConfiguration getRedisStandaloneConfiguration() {
        String redisHostName = systemEnvReader != null
                ? systemEnvReader.getProperty("REDIS_HOST", "redis.lewishome.tw")
                : "redis.lewishome.tw";
        // 確保 Port 為數字字串,避免轉換出錯
        String redisPort = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_PORT", "6379") : "6379";
        String redisHostPassword = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_PASSWORD", "XXXXXXX")
                : "XXXXXXXX";

        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHostName,
                TypeConvert.toInteger(redisPort));
        config.setPassword(RedisPassword.of(redisHostPassword));
        return config;
    }
}

5. MultiCacheInterceptor.java
-位置:package tw.lewishome.webapp.base.cache.redis;

寫入/刪除快取時,同步更新 L1 和 L2 的資料

package tw.lewishome.webapp.base.cache.redis;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheInterceptor;

/**
 * MultiCacheInterceptor 最終聯動版
 * 
 * 修正重點:
 * 1. 透過 doGet 實現 L1 -> L2 -> 回填 L1 的完整讀取鏈。
 * 2. 透過 doPut 確保 @Cacheable 執行完後,資料同步存入 Caffeine 與 Redis。
 * 3. 補強 L2 操作的異常捕捉,確保 Redis 離線時系統不崩潰。
 */
@Slf4j
public class MultiCacheInterceptor extends CacheInterceptor {

    /** Caffeine CacheManager (L1) */
    private CacheManager caffeineCacheManager;

    /**
     * 注入 L1 CacheManager
     * 
     * @param caffeineCacheManager Caffeine CacheManager 實例
     */
    public void setCaffeineCacheManager(CacheManager caffeineCacheManager) {
        this.caffeineCacheManager = caffeineCacheManager;
    }

    /**
     * 讀取快取攔截 (L1 -> L2)
     * 覆寫自 CacheAspectSupport
     */
    @Override
    protected Cache.ValueWrapper doGet(Cache cache, Object key) {
        // 1. 優先從本地 L1 (Caffeine) 檢索
        if (caffeineCacheManager != null) {
            Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
            if (caffeineCache != null) {
                Cache.ValueWrapper l1Result = caffeineCache.get(key);
                if (l1Result != null) {
                    log.debug("L1 (Caffeine) Hit! [{} : {}]", cache.getName(), key);
                    return l1Result;
                }
            }
        }

        // 2. L1 未命中,嘗試從 L2 (Redis) 讀取
        Cache.ValueWrapper result = null;
        try {
            // 此處 super.doGet 會使用 RedisConfiguration 中設定的 redisCacheManager
            result = super.doGet(cache, key);
        } catch (Exception e) {
            log.warn("L2 Cache 讀取失敗(Redis 可能未啟動或逾時),系統自動降級: {}", e.getMessage());
        }

        // 3. 若 L2 命中,將結果回填至 L1,確保下次存取變快
        if (result != null && caffeineCacheManager != null) {
            Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
            if (caffeineCache != null) {
                log.debug("L2 Hit. Populating L1... [{} : {}]", cache.getName(), key);
                caffeineCache.put(key, result.get());
            }
        }
        return result;
    }

    /**
     * 寫入快取攔截 (同步更新 L1 與 L2)
     * 覆寫自 CacheAspectSupport
     */
    @SuppressWarnings("null")
    @Override
    protected void doPut(Cache cache, Object key, Object value) {
        // 1. 同步寫入 L1 (Caffeine)
        if (caffeineCacheManager != null) {
            Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
            if (caffeineCache != null) {
                caffeineCache.put(key, value);
                log.debug("L1 (Caffeine) Updated. [{} : {}]", cache.getName(), key);
            }
        }

        // 2. 呼叫父類別寫入 L2 (Redis)
        try {
            super.doPut(cache, key, value);
        } catch (Exception e) {
            // 寫入 Redis 失敗時僅記錄 Log,不中斷業務邏輯,實現韌性設計
            log.warn("L2 Cache 寫入失敗: {}", e.getMessage());
        }
    }
}

6. 調整SystemEnvReader.java

-位置:package tw.lewishome.webapp.base.utility.common;

-因為系統開機時有以下警告訊息:

trationDelegate$BeanPostProcessorChecker : Bean 'systemEnvReader' of type [tw.lewishome.webapp.base.utility.common.SystemEnvReader] is not eligible for getting processed by all BeanPostProcessors 
(for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [meterRegistryPostProcessor]? 
Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.

所以依建議,將SystemEnvReader.java宣告為ROLE_INFRASTRUCTURE

@Component
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SystemEnvReader {
	--省略程式邏輯 --
}

7. 使用Catchable方式

  • 於method前宣告 @Cacheable,並指定cacheNames以及 key,系統自動依設定 使用 L1 或 L1 + L2 Cache)
  @Cacheable(cacheNames = CachePolicy.USER_AUTH_SEQ, key = "#userId")
    public List<String> getSysUserLastAuth(String userId) {
	 -- 省略程式邏輯 --
	}

圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言