SpringBoot自定義Redis代碼實(shí)踐指南
RedisCacheConfiguration 的全局定制
在 Spring Boot 整合 Redis 的過程中,默認(rèn)的緩存配置(RedisCacheConfiguration)雖然開箱即用,但在生產(chǎn)環(huán)境中往往存在以下痛點(diǎn):
- 數(shù)據(jù)不可讀:默認(rèn)使用 JDK 序列化,Redis 中存儲(chǔ)的是二進(jìn)制亂碼。
- Key 命名冗余:默認(rèn)使用雙冒號(hào)
::分隔,導(dǎo)致在 Redis 可視化工具中出現(xiàn)空文件夾層級。 - 缺乏過期限制:默認(rèn)緩存永不過期,可能導(dǎo)致 Redis 內(nèi)存堆積。
本章將詳細(xì)解析如何通過自定義 RedisCacheConfiguration Bean,一次性解決上述問題,實(shí)現(xiàn)規(guī)范化的緩存存儲(chǔ)策略。
一、 RedisCacheConfiguration 的核心職責(zé)
RedisCacheConfiguration 是 Spring Data Redis 提供的一個(gè)不可變(Immutable)配置類。它的核心職責(zé)是定義緩存數(shù)據(jù)的屬性和格式。
由于它是不可變的,任何修改方法(如 entryTtl)都不會(huì)修改當(dāng)前對象,而是返回一個(gè)新的配置實(shí)例。我們主要通過以下四個(gè)維度進(jìn)行定制:
- 序列化策略 (Serialization):Key 和 Value 的二進(jìn)制轉(zhuǎn)換規(guī)則。
- 前綴策略 (Prefixing):Key 的命名空間生成規(guī)則。
- 生存時(shí)間 (TTL):全局默認(rèn)的過期時(shí)間。
- 空值處理 (Null Handling):防止緩存穿透的策略。
二、 核心定制點(diǎn)詳解
1. 序列化定制:從 JDK 二進(jìn)制到 JSON
Spring 默認(rèn)使用 JdkSerializationRedisSerializer。
- 缺點(diǎn):生成的 Value 是
\xac\xed\x00\x05...這樣的二進(jìn)制流,無法人工閱讀,且 Java 強(qiáng)依賴,這就限制了其他語言(如 Python/Go)讀取該緩存。 - 解決:通常替換為
GenericJackson2JsonRedisSerializer,將對象存儲(chǔ)為標(biāo)準(zhǔn)的 JSON 字符串。需要注意處理LocalDateTime等 Java 8 時(shí)間類型的序列化問題。
2. 前綴策略定制:去除雙冒號(hào)
Spring 默認(rèn)生成的 Key 格式為 CacheName::Key。
- 缺點(diǎn):雙冒號(hào)
::在 Redis Desktop Manager 等工具中會(huì)被解析為兩層目錄,其中一層是空的,視覺效果差。 - 解決:通過
computePrefixWith方法,將分隔符修改為單冒號(hào):,例如CacheName:Key。
3. 過期時(shí)間定制
默認(rèn)配置是 Duration.ZERO(永不過期)。建議在全局配置中設(shè)置一個(gè)合理的默認(rèn)值(如 1 小時(shí)),作為兜底策略。
三、 完整代碼實(shí)踐
下面是一個(gè)生產(chǎn)級可用的配置示例。將此代碼放入你的 Spring Boot 項(xiàng)目的配置包下即可生效。
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
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;
@Configuration
@EnableCaching // 開啟緩存注解支持
public class RedisConfig {
/**
* 自定義 RedisCacheConfiguration
* 作用:定制 Key/Value 的序列化方式、過期時(shí)間、Key 前綴格式
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
// 1. 構(gòu)造 JSON 序列化器
// 使用 GenericJackson2JsonRedisSerializer 替換默認(rèn)序列化
GenericJackson2JsonRedisSerializer jsonSerializer = buildJsonSerializer();
// 2. 構(gòu)建配置
return RedisCacheConfiguration.defaultCacheConfig()
// 【核心 1】設(shè)置默認(rèn)過期時(shí)間:1 小時(shí)
.entryTtl(Duration.ofHours(1))
// 【核心 2】定制 Key 的序列化:使用 String (默認(rèn)就是 String,顯式指定更清晰)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 【核心 3】定制 Value 的序列化:使用 JSON
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer))
// 【核心 4】定制 Key 前綴策略:CacheName + ":" + Key
// 默認(rèn)是 "::",這里改為 ":",避免 RDM 工具出現(xiàn)空目錄
.computePrefixWith(name -> name + ":")
// 【可選】禁止緩存 Null 值 (根據(jù)業(yè)務(wù)需求決定是否開啟)
// .disableCachingNullValues()
;
}
/**
* 輔助方法:構(gòu)建支持 Java8 時(shí)間和類型信息的 JSON 序列化器
*/
private GenericJackson2JsonRedisSerializer buildJsonSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
// 1. 注冊 JavaTimeModule,解決 LocalDateTime 等時(shí)間類序列化問題
objectMapper.registerModule(new JavaTimeModule());
// 2. 啟用多態(tài)類型驗(yàn)證,將類信息 (@class) 寫入 JSON
// 這樣從 Redis 讀出來時(shí),才能自動(dòng)轉(zhuǎn)回具體的 Java 對象,而不僅僅是 Map
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
}四、 配置前后的效果對比
假設(shè)我們有一個(gè) @Cacheable(value = "users", key = "1001") 的緩存操作,存儲(chǔ)一個(gè) User 對象。
1. 修改前(默認(rèn)配置)
- Redis Key:
users::1001(雙冒號(hào)) - Redis Value:
\xac\xed\x00\x05sr\x00\x1bcom.example.User...(二進(jìn)制亂碼) - TTL: -1 (永不過期)
2. 修改后(自定義配置)
- Redis Key:
users:1001(單冒號(hào),目錄結(jié)構(gòu)清晰) - Redis Value:
{
"@class": "com.example.User",
"id": 1001,
"username": "admin",
"createTime": [2023, 10, 1, 12, 0, 0]
}(標(biāo)準(zhǔn) JSON,可讀性強(qiáng),其他語言可讀)
- TTL: 3600 (1小時(shí)后自動(dòng)刪除)
進(jìn)階定制 RedisCacheManager實(shí)現(xiàn)動(dòng)態(tài)過期時(shí)間
在上一章中,我們通過 RedisCacheConfiguration 解決了序列化和 Key 命名格式的問題。然而,在實(shí)際生產(chǎn)環(huán)境中,我們面臨一個(gè)新的挑戰(zhàn):緩存的生命周期管理。
默認(rèn)的 RedisCacheManager 雖然支持設(shè)置全局過期時(shí)間(例如統(tǒng)一 1 小時(shí)),但業(yè)務(wù)場景往往更加復(fù)雜:
- 驗(yàn)證碼:需要 5 分鐘過期。
- 用戶會(huì)話:需要 30 分鐘過期。
- 首頁廣告:需要 24 小時(shí)過期。
- 系統(tǒng)參數(shù):可能需要永不過期。
如果使用 Spring 原生方式,我們需要在配置類中構(gòu)建一個(gè)巨大的 Map<String, RedisCacheConfiguration> 來一一指定,這既繁瑣又難以維護(hù)。
本章將介紹如何通過繼承并擴(kuò)展 RedisCacheManager,實(shí)現(xiàn)基于注解后綴的動(dòng)態(tài)過期時(shí)間控制(例如:@Cacheable(value = "users#30m"))。
一、 核心思路:重寫工廠方法
RedisCacheManager 的本質(zhì)是一個(gè)工廠(Factory),負(fù)責(zé)生產(chǎn) RedisCache 對象。其核心方法是 createRedisCache(String name, RedisCacheConfiguration cacheConfig)。
我們的改造思路遵循 “約定優(yōu)于配置” 的原則:
- 攔截:在創(chuàng)建緩存實(shí)例前,攔截傳入的
name(緩存名稱)。 - 解析:判斷
name中是否包含特定的分隔符(如#),提取時(shí)間信息。 - 定制:如果包含時(shí)間信息,則基于全局配置衍生出一個(gè)新的配置對象,并修改其 TTL。
- 還原:將
name還原為干凈的業(yè)務(wù)名稱(去除時(shí)間后綴),創(chuàng)建緩存實(shí)例。
二、 代碼實(shí)現(xiàn)
下面是一個(gè)生產(chǎn)級的 TimeoutRedisCacheManager 實(shí)現(xiàn)。它支持 d (天), h (小時(shí)), m (分鐘), s (秒) 等時(shí)間單位。
1. 自定義 Manager 類
import cn.hutool.core.util.StrUtil; // 示例使用了 Hutool 工具類,也可使用原生 String 方法
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.time.Duration;
public class TimeoutRedisCacheManager extends RedisCacheManager {
private static final String SPLIT_FLAG = "#";
public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
/**
* 核心重寫方法:在創(chuàng)建 Cache 對象時(shí)介入
*/
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
// 1. 如果名稱為空或不包含分隔符,直接使用父類邏輯(應(yīng)用全局默認(rèn)配置)
if (StrUtil.isEmpty(name) || !name.contains(SPLIT_FLAG)) {
return super.createRedisCache(name, cacheConfig);
}
// 2. 切割字符串
// 格式約定:cacheName#ttl,例如 "system_users#30m"
String[] parts = name.split(SPLIT_FLAG);
if (parts.length != 2) {
return super.createRedisCache(name, cacheConfig);
}
// 3. 提取真實(shí)緩存名和時(shí)間字符串
String realName = parts[0]; // "system_users"
String ttlStr = parts[1]; // "30m"
// 4. 解析時(shí)間,并修改配置
// 注意:RedisCacheConfiguration 是不可變的,entryTtl 會(huì)返回新實(shí)例
Duration duration = parseDuration(ttlStr);
if (cacheConfig != null) {
cacheConfig = cacheConfig.entryTtl(duration);
}
// 5. 使用真實(shí)的名稱和定制后的配置創(chuàng)建緩存
// 這樣 Redis 中的 Key 就是 "prefix:system_users:key",不會(huì)帶上 "#30m"
return super.createRedisCache(realName, cacheConfig);
}
/**
* 輔助方法:解析時(shí)間字符串
* 支持格式:1d, 4h, 30m, 60s
*/
private Duration parseDuration(String ttlStr) {
String timeUnit = StrUtil.subSuf(ttlStr, -1).toLowerCase(); // 獲取最后一位單位
long timeValue = Long.parseLong(StrUtil.sub(ttlStr, 0, ttlStr.length() - 1)); // 獲取數(shù)值
switch (timeUnit) {
case "d":
return Duration.ofDays(timeValue);
case "h":
return Duration.ofHours(timeValue);
case "m":
return Duration.ofMinutes(timeValue);
case "s":
return Duration.ofSeconds(timeValue);
default:
// 默認(rèn)單位為秒,或者根據(jù)業(yè)務(wù)需求拋出異常
return Duration.ofSeconds(Long.parseLong(ttlStr));
}
}
}2. 注冊 Bean
將自定義的 Manager 注冊到 Spring 容器中,替換默認(rèn)的 CacheManager。
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate<String, Object> redisTemplate,
RedisCacheConfiguration redisCacheConfiguration) {
// 1. 獲取連接工廠
RedisConnectionFactory factory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
// 2. 創(chuàng)建無鎖的 CacheWriter
// BatchStrategies.scan(1000) 是為了在使用 evict(allEntries=true) 時(shí)
// 使用 SCAN 命令代替 KEYS 命令,防止阻塞 Redis 主線程
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(factory,
BatchStrategies.scan(1000));
// 3. 返回自定義的 TimeoutRedisCacheManager
return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
}三、 使用效果演示
完成上述配置后,開發(fā)人員在編寫業(yè)務(wù)代碼時(shí),可以極其靈活地控制緩存時(shí)間:
場景 A:使用默認(rèn)時(shí)間
// 不帶后綴,使用 RedisCacheConfiguration 配置的全局默認(rèn)時(shí)間(如 1 小時(shí))
@Cacheable(value = "system_dict", key = "#type")
public List<Dict> getDictByType(String type) { ... }- Redis Key:
system_dict:sex - TTL: 1 小時(shí)
場景 B:自定義短緩存
// 后綴 #60s,指定 60 秒過期
@Cacheable(value = "sms_code#60s", key = "#mobile")
public String getVerifyCode(String mobile) { ... }- Redis Key:
sms_code:13800138000(注意:后綴被去除了) - TTL: 60 秒
場景 C:自定義長緩存
// 后綴 #1d,指定 1 天過期
@Cacheable(value = "daily_ranking#1d", key = "#date")
public Ranking getDailyRanking(String date) { ... }- Redis Key:
daily_ranking:20231001 - TTL: 24 小時(shí)
深度定制 RedisCache 實(shí)現(xiàn)多級緩存與監(jiān)控
在掌握了 RedisCacheConfiguration(定制屬性)和 RedisCacheManager(定制策略)之后,我們來到了 Spring Cache 定制的“深水區(qū)”——自定義 RedisCache。
前兩者的定制局限于“配置”層面(數(shù)據(jù)存什么格式、存多久),而自定義 RedisCache 則是直接介入緩存的運(yùn)行時(shí)行為(Runtime Behavior)。
當(dāng)你遇到以下需求時(shí),必須重寫 RedisCache:
- 多級緩存:在訪問 Redis 前,先訪問本地堆內(nèi)存(Caffeine/HashMap),以減少網(wǎng)絡(luò) IO。
- 讀寫分離/雙寫:寫入緩存時(shí)需要同步寫入其他存儲(chǔ)介質(zhì)。
- 埋點(diǎn)監(jiān)控:需要精確統(tǒng)計(jì)緩存的命中率、讀取耗時(shí),并上報(bào)監(jiān)控系統(tǒng)(如 Prometheus)。
- 容錯(cuò)降級:當(dāng) Redis 宕機(jī)時(shí),不拋出異常,而是記錄日志并回源查詢數(shù)據(jù)庫。
本章將通過實(shí)現(xiàn)一個(gè)簡單的 “內(nèi)存 + Redis 二級緩存” 組件,展示如何重寫 RedisCache 的核心生命周期。
一、 核心原理:RedisCache 的職責(zé)
org.springframework.data.redis.cache.RedisCache 是 Spring 官方提供的核心實(shí)現(xiàn)類。它繼承自 AbstractValueAdaptingCache。
我們主要關(guān)注以下三個(gè)方法的重寫:
lookup(Object key):對應(yīng)@Cacheable的查詢操作。底層核心。put(Object key, Object value):對應(yīng)@Cacheable的寫入和@CachePut。evict(Object key):對應(yīng)@CacheEvict。
二、 代碼實(shí)現(xiàn):構(gòu)建二級緩存 (Level2RedisCache)
我們將構(gòu)建一個(gè) Level2RedisCache。它的邏輯是:
- 讀:先查 L1(本地內(nèi)存),有則返回;無則查 L2(Redis);如果 Redis 有,則回填 L1。
- 寫:同時(shí)寫入 L1 和 L2。
- 刪:同時(shí)刪除 L1 和 L2。
1. 自定義 Cache 類
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheWriter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定義二級緩存實(shí)現(xiàn)
* L1: JVM 本地內(nèi)存 (使用 ConcurrentHashMap 模擬,生產(chǎn)建議使用 Caffeine)
* L2: Redis (使用父類邏輯)
*/
public class Level2RedisCache extends RedisCache {
// 一級緩存容器
private final Map<Object, Object> localCache = new ConcurrentHashMap<>();
// 統(tǒng)計(jì)指標(biāo)(可選)
private final String name;
public Level2RedisCache(String name, RedisCacheWriter cacheWriter, RedisCacheConfiguration cacheConfig) {
super(name, cacheWriter, cacheConfig);
this.name = name;
}
/**
* 重寫查詢邏輯 (核心)
* 對應(yīng) @Cacheable 注解的讀取操作
*/
@Override
protected Object lookup(Object key) {
// 1. 【L1】先檢查本地內(nèi)存
Object localValue = localCache.get(key);
if (localValue != null) {
// log.debug("L1 緩存命中: key={}", key);
return localValue;
}
// 2. 【L2】本地沒有,調(diào)用父類方法查 Redis
// super.lookup() 內(nèi)部會(huì)處理序列化和反序列化
Object redisValue = super.lookup(key);
// 3. 【回填】如果 Redis 查到了,回寫到本地內(nèi)存
if (redisValue != null) {
// log.debug("L2 緩存命中并回填 L1: key={}", key);
localCache.put(key, redisValue);
}
return redisValue;
}
/**
* 重寫寫入邏輯
* 對應(yīng) @Cacheable 的寫入和 @CachePut
*/
@Override
public void put(Object key, Object value) {
// 1. 【L2】先寫 Redis (保證遠(yuǎn)程數(shù)據(jù)的強(qiáng)一致性)
super.put(key, value);
// 2. 【L1】再寫本地內(nèi)存
// 注意:如果是集群環(huán)境,這里只更新了當(dāng)前節(jié)點(diǎn)的 L1。
// 生產(chǎn)環(huán)境通常需要結(jié)合 Redis Pub/Sub 通知其他節(jié)點(diǎn)失效 L1。
if (value != null) {
localCache.put(key, value);
}
}
/**
* 重寫刪除邏輯
* 對應(yīng) @CacheEvict
*/
@Override
public void evict(Object key) {
// 1. 【L2】刪 Redis
super.evict(key);
// 2. 【L1】刪本地
localCache.remove(key);
}
/**
* 重寫清空邏輯
* 對應(yīng) @CacheEvict(allEntries = true)
*/
@Override
public void clear() {
super.clear();
localCache.clear();
}
}2. 在 Manager 中啟用自定義 Cache
有了自定義的“工人”(Cache),我們需要在“工廠”(Manager)中雇傭它。
我們需要再次回到 RedisCacheManager 的 createRedisCache 方法,將返回值修改為我們的 Level2RedisCache。
public class MultiLevelCacheManager extends RedisCacheManager {
public MultiLevelCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
// 這里可以結(jié)合上一章的邏輯,先處理 cacheConfig 的 TTL
// ... (省略 TTL 解析邏輯) ...
// 【關(guān)鍵】返回自定義的 Level2RedisCache 實(shí)例
// 這里的 super.createRedisCache 只是為了復(fù)用父類的一些初始化檢查,
// 但我們直接 new 自己的對象是最直接的。
return new Level2RedisCache(name, getCacheWriter(), cacheConfig);
}
}三、 進(jìn)階擴(kuò)展:添加監(jiān)控埋點(diǎn)
除了做多級緩存,自定義 RedisCache 也是做應(yīng)用層監(jiān)控的最佳切入點(diǎn)。我們可以在 lookup 方法中加入耗時(shí)統(tǒng)計(jì)。
@Override
protected Object lookup(Object key) {
long start = System.nanoTime();
try {
Object value = super.lookup(key);
recordMetrics(key, value != null, System.nanoTime() - start);
return value;
} catch (Exception e) {
// 容錯(cuò)降級:如果 Redis 掛了,記錄日志,返回 null 讓業(yè)務(wù)查庫
// log.error("Redis 異常", e);
return null;
}
}
private void recordMetrics(Object key, boolean hit, long durationNs) {
// 偽代碼:上報(bào)給 Prometheus 或 Micrometer
// Metrics.counter("cache.requests", "name", this.name, "hit", String.valueOf(hit)).increment();
// Metrics.timer("cache.latency", "name", this.name).record(durationNs, TimeUnit.NANOSECONDS);
}除了上面討論的 RedisCacheConfiguration (配置屬性)、RedisCacheManager (管理策略) 和 RedisCache (運(yùn)行時(shí)行為) 這“三駕馬車”之外,Spring Data Redis 體系非常龐大,還存在其他幾個(gè)關(guān)鍵層面的定制。
如果把之前的三個(gè)層面比作“裝修房子”,那么剩下的層面涉及 “地基加固”、“進(jìn)門鎖匙” 和 “應(yīng)急預(yù)案”。
以下是另外 4 個(gè) 常見且重要的高級定制層面:
第四層:AOP 輔助層定制 (KeyGenerator & ErrorHandler)
這層定制發(fā)生在緩存邏輯執(zhí)行的 “前” 和 “后”。
1. Custom KeyGenerator (定制鍵生成器)
- 場景:你厭倦了在每個(gè)
@Cacheable里寫key = "#user.id"?;蛘吣愕娜?yún)⒑軓?fù)雜(比如是一個(gè)大對象),你想用一種通用的算法(如 SHA256)生成 Key。 - 默認(rèn)行為:
SimpleKeyGenerator(使用參數(shù)的hashCode)。 - 如何定制:實(shí)現(xiàn)
KeyGenerator接口。
@Bean("md5KeyGenerator")
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
// 自定義邏輯:拼接類名+方法名+參數(shù)MD5
return method.getName() + "_" + Arrays.toString(params);
};
}
// 使用:@Cacheable(keyGenerator = "md5KeyGenerator")2. CacheErrorHandler (定制異常處理器)
- 場景:這是生產(chǎn)環(huán)境的保命符。默認(rèn)情況下,如果 Redis 掛了(連接超時(shí)),
@Cacheable會(huì)拋出異常,導(dǎo)致整個(gè)接口報(bào)錯(cuò)(500 Error)。大多數(shù)時(shí)候,我們希望 “緩存掛了降級查庫,不要影響主流程”。 - 如何定制:實(shí)現(xiàn)
CacheErrorHandler。
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
// 僅打印日志,不拋出異常 -> 視為未命中,自動(dòng)穿透到數(shù)據(jù)庫
log.error("Redis get failed, key: {}", key, exception);
}
// ... handlePutError, handleEvictError, handleClearError
};
}
}第五層:客戶端驅(qū)動(dòng)層定制 (LettuceClientConfiguration)
這層定制屬于 “基礎(chǔ)設(shè)施” 級別,涉及網(wǎng)絡(luò)連接、線程模型和讀寫分離。它不在 Spring Cache 的邏輯里,而是在連接工廠(RedisConnectionFactory)的底層。
1. 讀寫分離策略 (ReadFrom)
- 場景:你搭建了 Redis Sentinel(哨兵)或 Cluster(集群),主節(jié)點(diǎn)壓力大,希望 “寫主讀從”。
- 如何定制:通過
LettuceClientConfiguration。
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
// 優(yōu)先從從節(jié)點(diǎn)讀取,如果沒有從節(jié)點(diǎn)則讀主節(jié)點(diǎn)
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(), clientConfig);
}2. Netty 線程池調(diào)優(yōu)
- 場景:Redis 吞吐量極高,默認(rèn)的 Lettuce 線程數(shù)(CPU 核心數(shù))成為瓶頸,或者發(fā)生了 Netty 堆外內(nèi)存溢出。
- 如何定制:自定義
ClientResources。
第六層:操作模版層定制 (RedisTemplate)
雖然我們主要討論 @Cacheable(基于注解),但很多業(yè)務(wù)場景需要直接操作 Redis(基于代碼)。這時(shí)就需要定制 RedisTemplate。
1. 腳本執(zhí)行 (Scripting)
- 場景:需要執(zhí)行復(fù)雜的原子操作(如“扣減庫存”或“限流算法”),Java 代碼分步執(zhí)行有并發(fā)問題。
- 定制點(diǎn):封裝
DefaultRedisScript<T>,加載 Lua 腳本。
2. 流水線 (Pipeline)
- 場景:需要一次性寫入 10000 條數(shù)據(jù),循環(huán)調(diào)用
put會(huì)產(chǎn)生 10000 次網(wǎng)絡(luò) RTT(往返時(shí)延)。 - 定制點(diǎn):使用
redisTemplate.executePipelined(...),將命令打包發(fā)送,批量執(zhí)行。
第七層:事件監(jiān)聽層定制 (MessageListener)
Redis 不僅僅是存儲(chǔ),還是一個(gè)消息總線。
1. 鍵過期監(jiān)聽 (Keyspace Notifications)
- 場景:當(dāng)訂單支付超時(shí)(30分鐘未支付),Redis Key 過期刪除,你需要監(jiān)聽到這個(gè)事件來自動(dòng)取消訂單。
- 如何定制:配置
RedisMessageListenerContainer并開啟 Redis 的notify-keyspace-events配置。
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
// 然后監(jiān)聽 __keyevent@0__:expired 頻道Spring Data Redis 全景定制圖
為了讓你一眼看清整個(gè)架構(gòu),我總結(jié)了這 7 個(gè)層面:
| 層面 | 核心類/接口 | 作用域 | 典型應(yīng)用場景 |
|---|---|---|---|
| 1. 全局配置層 | RedisCacheConfiguration | 屬性規(guī)范 | 改 JSON 序列化、統(tǒng)一 Key 前綴 |
| 2. 管理策略層 | RedisCacheManager | 調(diào)度策略 | 動(dòng)態(tài)過期時(shí)間 (#30m) |
| 3. 運(yùn)行時(shí)行為層 | RedisCache | 執(zhí)行邏輯 | 多級緩存、埋點(diǎn)監(jiān)控 |
| 4. AOP 輔助層 | CacheErrorHandler | 容錯(cuò)/切面 | Redis 宕機(jī)自動(dòng)降級、自定義 Key 生成 |
| 5. 驅(qū)動(dòng)配置層 | LettuceClientConfiguration | 網(wǎng)絡(luò)/連接 | 讀寫分離、連接池參數(shù)、Netty 調(diào)優(yōu) |
| 6. 操作模版層 | RedisTemplate | 直接操作 | Lua 腳本、Pipeline 批量操作 |
| 7. 事件監(jiān)聽層 | KeyExpirationEvent | 異步回調(diào) | 監(jiān)聽 Key 過期事件處理業(yè)務(wù)邏輯 |
本文主要重點(diǎn)在于前三層定制,也是最常見的三層。
在面對需求時(shí),你應(yīng)該自上而下進(jìn)行考慮:能用上一層解決的,絕不輕易下沉到下一層,以保持系統(tǒng)的簡潔性和可維護(hù)性。
核心決策圖譜
第一層:RedisCacheConfiguration (配置層)
這是最頂層,也是改動(dòng)成本最低的一層。它控制的是數(shù)據(jù)的靜態(tài)屬性。
- 核心定義:定義緩存的 格式 (Format) 和 默認(rèn)規(guī)則 (Defaults)。
- 適用場景:
- 修改序列化方式:不想用 JDK 的亂碼二進(jìn)制,想用 JSON 格式(通用需求)。
- 規(guī)范 Key 的命名:把 Spring 默認(rèn)的雙冒號(hào)
::改成單冒號(hào):,或者統(tǒng)一添加全局前綴(如app_name:)。 - 設(shè)置兜底 TTL:為了防止 Redis 內(nèi)存爆炸,設(shè)置一個(gè)全局默認(rèn)過期時(shí)間(如 1 小時(shí))。
- 空值防御:全局禁止緩存
null值。
一句話判別:如果你想改變存入 Redis 的數(shù)據(jù) “長什么樣”,或者設(shè)置 “默認(rèn)存多久”,選它。
第二層:RedisCacheManager (策略層)
這是中間層,它控制的是配置的分發(fā)策略。
- 核心定義:根據(jù) 緩存名稱 (CacheName) 的不同,動(dòng)態(tài)應(yīng)用不同的 Configuration。
- 適用場景:
- 差異化過期時(shí)間:這是最經(jīng)典的需求。需要支持
@Cacheable("code#5m")和@Cacheable("data#1d")共存。 - 差異化序列化:極少數(shù)情況下,某些緩存想存 JSON,某些想存純 String(
#raw),需要 Manager 識(shí)別后綴并切換配置。 - 多租戶隔離:根據(jù)緩存名動(dòng)態(tài)添加租戶 ID 前綴。
一句話判別:如果你的需求是 “看菜下碟”(即不同的業(yè)務(wù)緩存需要不同的過期時(shí)間或配置),選它。
第三層:RedisCache (行為層)
這是最底層,也是“深水區(qū)”。它控制的是**“數(shù)據(jù)的讀寫邏輯”**。
- 核心定義:介入
get、put、evict的 運(yùn)行時(shí)行為 (Runtime Behavior)。 - 適用場景:
- 多級緩存:這是最硬核的理由。引入 Caffeine 做 L1 緩存,Redis 做 L2 緩存。
- 埋點(diǎn)監(jiān)控:需要精確統(tǒng)計(jì)每次 Redis IO 的耗時(shí),并上報(bào)給 Prometheus/SkyWalking。
- 容錯(cuò)降級:當(dāng) Redis 連接超時(shí)或宕機(jī)時(shí),捕獲異常并打印日志,而不是讓整個(gè)接口報(bào)錯(cuò)(雖然
CacheErrorHandler也能做,但這里更靈活)。 - 讀寫分離/雙寫:寫入緩存時(shí),需要同步寫入另一個(gè)存儲(chǔ)介質(zhì)。
一句話判別:如果你需要 “改變流程”(比如讀 Redis 前先讀內(nèi)存,或者讀完 Redis 后發(fā)個(gè)消息),或者需要 “強(qiáng)行介入” 讀寫過程,選它。
決策速查表 (Cheat Sheet)
在實(shí)際開發(fā)中,對照下表進(jìn)行選型:
| 你的需求 | 推薦方案 | 復(fù)雜度 |
|---|---|---|
| “我想讓 Redis 里存的是 JSON,不是亂碼” | RedisCacheConfiguration | ? (低) |
| “我想把 Key 中間的雙冒號(hào) :: 去掉” | RedisCacheConfiguration | ? (低) |
| “我想讓所有緩存默認(rèn) 1 小時(shí)過期” | RedisCacheConfiguration | ? (低) |
| “我的驗(yàn)證碼要 5分鐘過期,排行榜要 1天過期” | RedisCacheManager (解析 #ttl) | ?? (中) |
| “我想根據(jù)緩存名決定是否存 Null 值” | RedisCacheManager (解析 #nonull) | ?? (中) |
| “Redis 太慢了,我想加一層本地內(nèi)存緩存” | 自定義 RedisCache | ??? (高) |
| “我想知道 Redis 緩存的命中率和平均耗時(shí)” | 自定義 RedisCache | ??? (高) |
| “Redis 掛了不能報(bào)錯(cuò),要自動(dòng)去查數(shù)據(jù)庫” | 自定義 RedisCache (或 ErrorHandler) | ??? (高) |
最佳實(shí)踐建議:
對于絕大多數(shù)(90%)的 Spring Boot 業(yè)務(wù)系統(tǒng),“JSON 序列化配置 (Layer 1) + 動(dòng)態(tài)過期時(shí)間支持 (Layer 2)” 的組合是最完美的黃金搭檔。除非你有明確的性能瓶頸需要多級緩存,否則盡量不要觸碰 Layer 3,以降低系統(tǒng)的維護(hù)成本。
到此這篇關(guān)于SpringBoot自定義Redis代碼實(shí)踐指南的文章就介紹到這了,更多相關(guān)SpringBoot自定義Redis內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot使用redisson?+?自定義注解實(shí)現(xiàn)消息的發(fā)布訂閱(解決方案)
- SpringBoot+Redisson自定義注解一次解決重復(fù)提交問題
- SpringBoot2.0整合Redis自定義注入bean組件配置的實(shí)戰(zhàn)教程
- SpringBoot 如何實(shí)現(xiàn)自定義Redis序列化
- SpringBoot如何監(jiān)控Redis中某個(gè)Key的變化(自定義監(jiān)聽器)
- SpringBoot配置Redis自定義過期時(shí)間操作
- springboot自定義redis-starter的實(shí)現(xiàn)
- SpringBoot2.3整合redis緩存自定義序列化的實(shí)現(xiàn)
相關(guān)文章
Spring整合mybatis實(shí)現(xiàn)過程詳解
這篇文章主要介紹了Spring整合mybatis實(shí)現(xiàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
SpringBoot項(xiàng)目中分頁插件PageHelper無效的問題及解決方法
這篇文章主要介紹了解決SpringBoot項(xiàng)目中分頁插件PageHelper無效的問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06
Java?IO流與NIO技術(shù)綜合應(yīng)用詳細(xì)實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于Java?IO流與NIO技術(shù)綜合應(yīng)用的相關(guān)資料,文中包括了字節(jié)流和字符流,以及它們的高級特性如緩沖區(qū)、序列化和反序列化,同時(shí)還介紹了NIO中的通道和緩沖區(qū),以及選擇器的使用,需要的朋友可以參考下2024-12-12
Java集合框架數(shù)據(jù)存儲(chǔ)與操作的神器
Java集合框架提供了多種數(shù)據(jù)結(jié)構(gòu)和操作方法,包括Collection接口、Map接口以及各種實(shí)現(xiàn)類,本文詳細(xì)介紹了集合框架的層次結(jié)構(gòu)、常見接口和實(shí)現(xiàn)類的特點(diǎn)、使用場景以及最佳實(shí)踐,幫助開發(fā)者高效地進(jìn)行數(shù)據(jù)存儲(chǔ)和操作,感興趣的朋友跟隨小編一起看看吧2025-12-12

