精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

使用 Spring Cache 實現緩存,這種方式才叫優雅!

開發 前端
別再用 HashMap 當緩存,也別再手動寫 RedisTemplate 操作緩存了,趕緊把 Spring Cache 用起來。剛開始可能會覺得配置有點麻煩,但一旦上手,你會發現, 這玩意兒是真的香!?

兄弟們,咱們做 Java 開發的,誰沒踩過性能的坑啊?前陣子我朋友小楊就跟我吐槽,說他寫的用戶查詢接口,本地測試的時候唰唰快,一上生產環境,用戶量稍微一上來,數據庫直接被干到 CPU 100%,日志里全是紅色的慢查詢警告。老板在旁邊盯著,他手忙腳亂地改代碼,最后沒辦法,手動寫了個 HashMap 當緩存應急 。 結果嘛,你懂的,多實例部署的時候緩存不一致,又加班到半夜。

其實這種事兒真不用這么狼狽,Spring 早就給咱們準備好了解決方案 ——Spring Cache。這玩意兒就像個 “緩存管家”,你不用手動寫代碼操作 Redis、Ehcache 這些中間件,只要給方法貼個注解,它就幫你把 “查緩存→有就返回→沒有查庫→存緩存” 這一套流程全搞定。今天咱們就好好聊聊,怎么用 Spring Cache 實現優雅的緩存,讓你從此告別 “手動緩存火葬場”。

一、先搞明白:Spring Cache 到底是個啥?

可能有人會說,“緩存不就是存數據嘛,我自己寫個工具類調用 Redis 也能搞定”。這話沒毛病,但你想想,要是每個查詢方法都寫一遍 “查緩存、存緩存” 的邏輯,代碼得有多冗余?而且萬一以后要換緩存中間件,從 Redis 換成 Memcached,不得每個方法都改一遍?

Spring Cache 的核心思路就是 “解耦”—— 把緩存邏輯和業務邏輯分開。它基于 AOP(面向切面編程)實現,當你調用一個加了緩存注解的方法時,Spring 會先攔截這個調用,幫你處理緩存相關的操作,業務代碼里完全不用管緩存的事兒。

打個比方,你就像餐廳里的廚師(負責業務邏輯),Spring Cache 就是服務員(負責緩存)。客人要一份宮保雞丁(調用方法),服務員會先去備餐區看看有沒有做好的(查緩存),有就直接端給客人;沒有就告訴廚師做一份(執行業務邏輯),做好后再把一份放進備餐區(存緩存),下次客人再要就不用麻煩廚師了。你看,廚師全程不用管備餐區的事兒,專心做菜就行 —— 這就是優雅!

而且 Spring Cache 是 “抽象層”,它不關心底層用的是 Redis 還是 Ehcache,你只要配置好對應的 “緩存管理器”,就能無縫切換。比如開發環境用內存緩存(ConcurrentMapCache)方便測試,生產環境換成 Redis,業務代碼一行都不用改 —— 這波操作誰看了不夸一句?

二、快速上手:3 步搞定 Spring Cache 基礎使用

光說不練假把式,咱們先從最基礎的例子開始,用 Spring Boot+Spring Cache+Redis 實現一個用戶查詢的緩存功能。別擔心,步驟很簡單,跟著做就行。

第一步:搭環境(引入依賴)

首先得有個 Spring Boot 項目,然后在 pom.xml 里加兩個關鍵依賴:Spring Cache 的起步依賴,還有 Redis 的起步依賴(畢竟生產環境大多用 Redis)。

<!-- Spring Cache 起步依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis 起步依賴(底層用 lettuce 客戶端) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 要是用Jackson序列化,再加個這個(后面會講為啥需要) -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

如果是 Gradle 項目,對應的依賴也差不多,這里就不贅述了。記住,Spring Boot 2.x 和 3.x 的依賴坐標基本一致,不用糾結版本問題(除非你用的是特別老的 2.0 之前的版本,那得升級了兄弟)。

第二步:開開關(加注解啟用緩存)

在 Spring Boot 的啟動類上,加個@EnableCaching注解,告訴 Spring “我要啟用緩存功能啦”。就像你開空調之前要按一下電源鍵一樣簡單。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // 關鍵:啟用Spring Cache
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

第三步:寫業務(給方法貼緩存注解)

接下來就是核心了 —— 給業務方法加緩存注解。咱們先定義一個 User 實體類,再寫個 UserService,里面有個根據 id 查詢用戶的方法,給這個方法加@Cacheable注解。

先看 User 實體類(注意要實現 Serializable,后面講序列化會用到):

import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data // lombok注解,省掉getter、setter
public class User implements Serializable { // 實現Serializable,Redis序列化需要
    private Long id;
    private String username;
    private String phone;
    private LocalDateTime createTime;
}

然后是 UserService,這里咱們模擬數據庫查詢(用 Thread.sleep 模擬慢查詢,突出緩存的作用):

import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
    // 模擬數據庫查詢:根據id查用戶
    // @Cacheable:表示這個方法的結果要被緩存
    // value:緩存的“命名空間”,相當于給緩存分個組,避免key沖突
    // key:緩存的key,這里用SpEL表達式,取方法參數id的值
    @Cacheable(value = "userCache", key = "#id")
    public User getUserById(Long id) {
        // 模擬數據庫查詢的耗時操作(比如查MySQL)
        try {
            TimeUnit.SECONDS.sleep(2); // 睡2秒,模擬慢查詢
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 模擬從數據庫查出來的數據
        User user = new User();
        user.setId(id);
        user.setUsername("用戶" + id);
        user.setPhone("1380013800" + (id % 10));
        user.setCreateTime(LocalDateTime.now());
        return user;
    }
}

最后寫個 Controller 測試一下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        long start = System.currentTimeMillis();
        User user = userService.getUserById(id);
        long end = System.currentTimeMillis();
        System.out.println("查詢耗時:" + (end - start) + "毫秒");
        return user;
    }
}

現在啟動項目,用 Postman 或者瀏覽器訪問http://localhost:8080/user/1:

第一次訪問:控制臺會打印 “查詢耗時:約 2000 毫秒”,因為要走數據庫查詢,然后把結果存到 Redis。

第二次訪問:控制臺直接打印 “查詢耗時:約 10 毫秒”,因為直接從 Redis 拿緩存了!

你看,就加了個@Cacheable注解,緩存就生效了,業務代碼里完全沒寫任何 Redis 相關的邏輯 —— 這難道不優雅嗎?比你手動寫redisTemplate.opsForValue().get()、redisTemplate.opsForValue().set()清爽多了吧?

三、核心注解:這 5 個注解搞定 90% 的緩存場景

剛才用了@Cacheable,但 Spring Cache 的本事可不止這一個。它總共提供了 5 個核心注解,覆蓋了 “查、增、改、刪” 所有緩存操作。咱們一個個講,結合實際業務場景,保證你一看就懂。

1. @Cacheable:查緩存,有則用,無則存

這是最常用的注解,作用是 “查詢緩存”:調用方法前先查緩存,如果緩存存在,直接返回緩存的值,不執行方法;如果緩存不存在,執行方法,把結果存到緩存里。

剛才的例子已經用過了,這里再補充幾個關鍵屬性,都是實戰中必用的:

屬性

作用

例子

value

緩存命名空間(必填),可以理解為緩存的 “文件夾”,避免 key 沖突

value = "userCache"

key

緩存的 key(可選),用 SpEL 表達式,默認是所有參數的組合

key = "#id"(取參數 id)、key = "#user.id"(取對象參數的 id)

condition

緩存的 “前置條件”(可選),滿足條件才緩存,SpEL 表達式返回 boolean

condition = "#id > 100"(只有 id>100 才緩存)

unless

緩存的 “排除條件”(可選),方法執行后判斷,滿足則不緩存

unless = "#result == null"(結果為 null 不緩存)

cacheManager

指定用哪個緩存管理器(可選),比如有的方法用 Redis,有的用 Ehcache

cacheManager = "redisCacheManager"

舉個帶條件的例子,比如 “只緩存 id 大于 100 的用戶,并且結果不為 null”:

@Cacheable(
    value = "userCache",
    key = "#id",
    condition = "#id > 100", // 前置條件:id>100才查緩存/存緩存
    unless = "#result == null" // 排除條件:結果為null不存緩存
)
public User getUserById(Long id) {
    // 業務邏輯不變...
}

這里要注意condition和unless的區別:condition是在方法執行前判斷的,如果不滿足,連緩存都不查,直接執行方法;unless是在方法執行后判斷的,不管怎么樣都會執行方法,只是結果不存緩存。別搞混了哈!

2. @CachePut:更新緩存,先執行方法再存緩存

比如你更新用戶信息的時候,得把緩存里的舊數據更成新的吧?這時候就用@CachePut。它的邏輯是:先執行方法(不管緩存有沒有),然后把方法的結果存到緩存里(覆蓋舊的緩存,如果有的話)。

舉個更新用戶的例子:

// @CachePut:更新緩存,執行方法后把結果存到緩存
// key和查詢方法保持一致,都是#user.id,這樣才能覆蓋舊緩存
@CachePut(value = "userCache", key = "#user.id")
public User updateUser(User user) {
    // 模擬更新數據庫(這里省略JDBC/MyBatis代碼)
    try {
        TimeUnit.SECONDS.sleep(1); // 模擬耗時
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 模擬更新后的用戶數據(實際應該從數據庫查最新的)
    user.setCreateTime(LocalDateTime.now()); // 更新時間
    return user;
}

比如你先查了 user.id=1(緩存里存了舊數據),然后調用 updateUser 更新 user.id=1 的信息,@CachePut會先執行更新邏輯,再把新的 user 對象存到緩存里,覆蓋原來的舊緩存。下次再查 user.id=1,拿到的就是最新的數據了 —— 完美解決緩存和數據庫不一致的問題。

3. @CacheEvict:刪除緩存,執行方法后清緩存

當你刪除用戶的時候,緩存里的舊數據也得刪掉吧?不然別人還能查到已經刪除的用戶,這就出 bug 了。@CacheEvict就是干這個的,它的邏輯是:執行方法(比如刪除數據庫記錄),然后刪除對應的緩存。

舉個刪除用戶的例子:

// @CacheEvict:刪除緩存,執行方法后刪除指定緩存
@CacheEvict(value = "userCache", key = "#id")
public void deleteUser(Long id) {
    // 模擬刪除數據庫記錄
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("用戶" + id + "已從數據庫刪除");
}

還有個常用的屬性allEntries,比如你更新了用戶列表,想把 “userCache” 這個命名空間下的所有緩存都刪掉(避免列表緩存和數據庫不一致),就可以設allEntries = true:

// allEntries = true:刪除value="userCache"下的所有緩存
@CacheEvict(value = "userCache", allEntries = true)
public void batchUpdateUser(List<User> userList) {
    // 批量更新數據庫邏輯...
}

另外還有個beforeInvocation屬性,默認是false(方法執行后刪緩存),如果設為true,會在方法執行前刪緩存。什么時候用呢?比如方法執行可能會拋異常,你又希望不管成功失敗都刪緩存,就可以用這個。不過一般情況下用默認的false就行,避免方法執行失敗了,緩存卻被刪了,導致下次查詢穿透到數據庫。

4. @Caching:組合注解,一次搞定多個緩存操作

有時候一個方法需要同時做多個緩存操作,比如 “更新用戶信息后,既要更新用戶的緩存,又要刪除用戶列表的緩存”,這時候單個注解就不夠用了,得用@Caching來組合。

@Caching里可以包含cacheable、put、evict三個屬性,每個屬性都是一個數組,可以放多個對應的注解。

舉個實戰例子:更新用戶信息后,更新用戶緩存(@CachePut),同時刪除用戶列表的緩存(@CacheEvict):

// @Caching:組合多個緩存操作
@Caching(
    put = {
        // 更新用戶緩存(key是用戶id)
        @CachePut(value = "userCache", key = "#user.id")
    },
    evict = {
        // 刪除用戶列表緩存(假設列表緩存的key是"userList")
        @CacheEvict(value = "userListCache", key = "'userList'")
    }
)
public User updateUserAndClearListCache(User user) {
    // 更新數據庫邏輯...
    user.setCreateTime(LocalDateTime.now());
    returnuser;
}

這樣一來,調用這個方法的時候,Spring 會同時執行@CachePut和@CacheEvict兩個操作,既更新了單個用戶的緩存,又清空了列表緩存 —— 不用寫兩個方法,也不用手動操作緩存,太方便了!

5. @CacheConfig:類級注解,統一配置緩存屬性

如果一個 Service 里的所有方法都用同一個value(緩存命名空間)或者cacheManager,那每個方法都寫一遍豈不是很麻煩?@CacheConfig就是用來解決這個問題的,它是類級別的注解,可以統一配置當前類所有緩存方法的公共屬性。

比如 UserService 里的所有方法都用value = "userCache",就可以這么寫:

import org.springframework.cache.annotation.CacheConfig;
importorg.springframework.cache.annotation.Cacheable;
importorg.springframework.stereotype.Service;

@Service
// @CacheConfig:統一配置當前類的緩存屬性
@CacheConfig(value = "userCache") 
publicclassUserService {

    // 不用再寫value="userCache"了,繼承類上的配置
    @Cacheable(key = "#id")
    public User getUserById(Long id) {
        // 邏輯...
    }

    // 同樣不用寫value,key還是要寫(因為每個方法的key可能不一樣)
    @CachePut(key = "#user.id")
    public User updateUser(User user) {
        // 邏輯...
    }

    // 也不用寫value
    @CacheEvict(key = "#id")
    public void deleteUser(Long id) {
        // 邏輯...
    }
}

注意哈,@CacheConfig只能配置公共屬性,像key這種每個方法可能不一樣的屬性,還是得在方法上單獨寫。而且方法上的配置會覆蓋類上的配置,比如你在方法上寫了value = "otherCache",就會覆蓋@CacheConfig里的value = "userCache"—— 這個優先級要記清楚。

四、進階配置:從 “能用” 到 “好用”,這些配置不能少

剛才的例子用的是默認配置,但生產環境里肯定不夠用。比如默認的 Redis 緩存序列化方式是 JDK 序列化,存到 Redis 里的是一堆亂碼;默認沒有緩存過期時間,數據會一直存在 Redis 里,占內存;還有不同的業務可能需要不同的緩存策略 —— 這些都得靠自定義配置來解決。

咱們一步步來,把 Spring Cache 配置得 “好用” 起來。

1. 解決 Redis 緩存亂碼:自定義序列化方式

先看個坑:剛才的例子里,你用 Redis 客戶端(比如 Redis Desktop Manager)查看緩存,會發現 key 是userCache::1,但 value 是一堆亂碼,根本看不懂。這是因為 Spring Cache 默認用的是JdkSerializationRedisSerializer,這種序列化方式雖然能用,但可讀性太差,而且占空間。

解決辦法很簡單:把序列化方式換成Jackson2JsonRedisSerializer,這樣存到 Redis 里的是 JSON 格式,又好懂又省空間。

寫個 Redis 緩存配置類:

import org.springframework.cache.CacheManager;
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.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching// 這里加也行,啟動類加也行,只要加一次
publicclass RedisCacheConfig {

    // 自定義Redis緩存管理器
    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 1. 配置序列化方式
        // Key的序列化:用StringRedisSerializer,key是字符串
        RedisSerializer<String> keySerializer = new StringRedisSerializer();
        // Value的序列化:用Jackson2JsonRedisSerializer,轉成JSON
        Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        
        // 配置Jackson,解決LocalDateTime等Java 8時間類型序列化問題
        com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
        // 開啟類型信息,反序列化時能知道對象的類型(避免List等集合反序列化出錯)
        objectMapper.activateDefaultTyping(
                com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL,
                com.fasterxml.jackson.databind.jsontype.TypeSerializer.DefaultImpl.NON_FINAL
        );
        // 支持Java 8時間類型(LocalDateTime、LocalDate等)
        objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
        valueSerializer.setObjectMapper(objectMapper);

        // 2. 配置默認的緩存規則(比如默認過期時間30分鐘)
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30)) // 默認過期時間30分鐘
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer)) // 序列化key
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer)) // 序列化value
                .disableCachingNullValues(); // 禁止緩存null值(可選,看業務需求)

        // 3. 配置不同緩存命名空間的個性化規則(比如userCache過期1小時,userListCache過期10分鐘)
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("userCache", defaultCacheConfig.entryTtl(Duration.ofHours(1))); // userCache過期1小時
        cacheConfigurations.put("userListCache", defaultCacheConfig.entryTtl(Duration.ofMinutes(10))); // userListCache過期10分鐘

        // 4. 創建緩存管理器
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultCacheConfig) // 默認規則
                .withInitialCacheConfigurations(cacheConfigurations) // 個性化規則
                .build();
    }
}

再在 application.yml 里配置 Redis 的連接信息(不然連不上 Redis):

spring:
  redis:
    host: localhost # Redis地址,生產環境填實際地址
    port: 6379 # Redis端口
    password: # Redis密碼,沒有就空著
    database: 0 # Redis數據庫索引(默認0)
    lettuce: # Spring Boot 2.x默認用lettuce客戶端,比jedis好
      pool:
        max-active: 8 # 最大連接數
        max-idle: 8 # 最大空閑連接數
        min-idle: 2 # 最小空閑連接數
        max-wait: 1000ms # 連接池最大阻塞等待時間

現在再啟動項目,調用 getUserById (1),去 Redis 里看:

  • key 還是userCache::1,沒變。
  • value 變成了 JSON 格式:{"@class":"com.example.demo.entity.User","id":1,"username":"用戶1","phone":"13800138001","createTime":["java.time.LocalDateTime",["2024-05-20T15:30:45.123"]]}—— 雖然多了點類型信息,但至少能看懂了,而且 Java 8 的 LocalDateTime 也能正常序列化 / 反序列化了。

這個配置解決了兩個大問題:緩存亂碼和時間類型序列化失敗,生產環境必備!

2. 自定義 Key 生成策略:不用再手動寫 key

剛才的例子里,每個方法都要寫key = "#id"、key = "#user.id",要是方法參數多了,key 寫起來很麻煩,還容易出錯。Spring Cache 允許我們自定義 Key 生成策略,以后不用再手動寫 key 了。

比如我們定義一個 “Key 生成器”:key 由 “方法名 + 參數值” 組成,這樣能保證唯一性。

寫個自定義 KeyGenerator:

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;

@Configuration
publicclass CustomKeyGeneratorConfig {

    // 自定義Key生成器,Bean名稱是customKeyGenerator
    @Bean("customKeyGenerator")
    public KeyGenerator customKeyGenerator() {
        returnnew KeyGenerator() {
            @Override
            publicObject generate(Object target, Method method, Object... params) {
                // 生成規則:方法名 + 參數列表(比如 getUserById[1])
                String key = method.getName() + "[" + Arrays.toString(params) + "]";
                System.out.println("生成的緩存key:" + key);
                return key;
            }
        };
    }
}

然后在方法上用keyGenerator屬性指定用這個生成器,不用再寫key了:

@Service
@CacheConfig(value = "userCache", keyGenerator = "customKeyGenerator") // 類級配置,指定Key生成器
public class UserService {

    // 不用寫key了,Key生成器會自動生成:getUserById[1]
    @Cacheable
    public User getUserById(Long id) {
        // 邏輯...
    }

    // 自動生成key:updateUser[User(id=1, username=xxx, ...)]
    @CachePut
    public User updateUser(User user) {
        // 邏輯...
    }
}

這樣一來,不管方法有多少個參數,Key 生成器都會自動生成唯一的 key,再也不用手動寫復雜的 SpEL 表達式了。當然,你也可以根據自己的業務需求修改生成規則,比如加上類名(避免不同 Service 的方法名重復導致 key 沖突),靈活得很。不過要注意:key和keyGenerator不能同時用,用了一個就不能用另一個,不然會報錯 —— 這個坑別踩。

3. 多緩存管理器:Redis 和 Ehcache 按需切換

有時候項目里可能需要用多種緩存,比如本地緩存用 Ehcache(快),分布式緩存用 Redis(多實例共享)。Spring Cache 支持配置多個緩存管理器,然后在方法上指定用哪個。

比如我們再配置一個 Ehcache 的緩存管理器:

首先加 Ehcache 的依賴:

<!-- Ehcache 依賴 -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

然后寫 Ehcache 的配置文件(src/main/resources/ehcache.xml):

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="http://www.ehcache.org/schema/ehcache-core.xsd"
        updateCheck="false">

    <!-- 本地緩存:默認配置 -->
    <defaultCache
            maxEntriesLocalHeap="1000" <!-- 堆內存最大緩存條目 -->
            eternal="false" <!-- 是否永久有效 -->
            timeToIdleSeconds="60" <!-- 空閑時間(秒),超過這個時間沒人用就過期 -->
            timeToLiveSeconds="60" <!-- 存活時間(秒),不管用不用都過期 -->
            memoryStoreEvictionPolicy="LRU"> <!-- 淘汰策略:LRU(最近最少使用) -->
    </defaultCache>

    <!-- 自定義緩存:localCache(本地緩存) -->
    <cache name="localCache"
           maxEntriesLocalHeap="500"
           eternal="false"
           timeToIdleSeconds="30"
           timeToLiveSeconds="30"
           memoryStoreEvictionPolicy="LRU">
    </cache>
</config>

再在配置類里加 Ehcache 的緩存管理器:

import org.springframework.cache.CacheManager;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.ConfigurationFactory;

@Configuration
publicclass MultiCacheManagerConfig {

    // 1. Redis緩存管理器(之前寫過,這里省略,注意Bean名稱是redisCacheManager)

    // 2. Ehcache緩存管理器(Bean名稱是ehcacheCacheManager)
    @Bean("ehcacheCacheManager")
    public CacheManager ehcacheCacheManager() {
        // 加載Ehcache配置文件
        net.sf.ehcache.CacheManager ehcacheManager = CacheManager.create(ConfigurationFactory.parseConfiguration(getClass().getResourceAsStream("/ehcache.xml")));
        returnnew EhCacheCacheManager(ehcacheManager);
    }
}

然后在方法上用cacheManager屬性指定用哪個緩存管理器:

@Service
public class UserService {

    // 用Redis緩存管理器(分布式緩存)
    @Cacheable(value = "userCache", cacheManager = "redisCacheManager")
    public User getUserById(Long id) {
        // 邏輯...
    }

    // 用Ehcache緩存管理器(本地緩存,適合不共享的臨時數據)
    @Cacheable(value = "localCache", cacheManager = "ehcacheCacheManager")
    public List<String> getLocalTempData() {
        // 模擬獲取本地臨時數據(比如配置信息,不用共享)
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        returnArrays.asList("temp1", "temp2", "temp3");
    }
}

這樣一來,getUserById用 Redis 緩存(多實例部署時能共享),getLocalTempData用 Ehcache 本地緩存(速度快,不占 Redis 資源)—— 按需選擇,靈活高效。

五、踩坑指南:這些問題不注意,上線準出幺蛾子

Spring Cache 雖然好用,但要是不注意這些細節,上線了肯定出問題。我整理了幾個實戰中最容易踩的坑,幫你避坑。

1. 緩存穿透:查不存在的數據,一直打數據庫

問題描述:比如有人故意查user.id=-1(數據庫里根本沒有這個用戶),@Cacheable會執行方法,返回 null,默認情況下 null 不會被緩存(除非你配置了enableCachingNullValues())。這樣一來,每次查id=-1都會穿透到數據庫,要是有人惡意刷這個接口,數據庫直接就崩了。

解決方案:

  • 方案一:緩存 null 值。在 Redis 緩存配置里把disableCachingNullValues()改成enableCachingNullValues(),這樣查不到的數據也會緩存(value 是 null),下次再查就直接返回 null,不打數據庫了。

但要注意:緩存 null 值會占空間,所以要給這類緩存設置較短的過期時間(比如 5 分鐘),避免浪費內存。

  • 方案二:參數校驗。在 Controller 或 Service 里先判斷參數是否合法,比如id <=0直接返回錯誤,根本不執行查詢邏輯。

舉個例子:

@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
    // 參數校驗:id<=0直接返回錯誤
    if (id <= 0) {
        return Result.fail("用戶id不合法");
    }
    User user = userService.getUserById(id);
    return Result.success(user);
}

這種方式最徹底,從源頭阻止無效請求。

2. 緩存擊穿:熱點數據過期,大量請求打數據庫

問題描述:比如某個用戶是熱點數據(比如網紅用戶,id=10086),緩存過期的瞬間,有 1000 個請求同時查這個用戶,這時候緩存里沒有,所有請求都會穿透到數據庫,把數據庫打垮 —— 這就是緩存擊穿。

解決方案:

  • 方案一:熱點數據永不過期。對于特別熱點的數據,比如首頁 Banner、網紅用戶信息,設置緩存永不過期(entryTtl(Duration.ofDays(365*10))),然后通過定時任務(比如 Quartz)定期更新緩存,這樣就不會有過期的瞬間。
  • 方案二:互斥鎖。在查詢方法里加鎖,只有一個請求能去數據庫查,查到后更新緩存,其他請求等待鎖釋放后再查緩存。

舉個用 Redis 分布式鎖實現的例子(用 Redisson 客戶端,比自己寫鎖簡單):

首先加 Redisson 依賴:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version> <!-- 選和Spring Boot兼容的版本 -->
</dependency>

然后修改 Service 方法:

import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
publicclass UserService {

    @Autowired
    private RedissonClient redissonClient;

    @Cacheable(value = "userCache", key = "#id")
    public User getUserById(Long id) {
        // 1. 判斷是不是熱點數據(這里假設id=10086是熱點數據)
        if (id == 10086) {
            // 2. 獲取分布式鎖(鎖的key:userLock:10086)
            String lockKey = "userLock:" + id;
            RLock lock = redissonClient.getLock(lockKey);
            try {
                // 3. 加鎖(等待10秒,持有鎖30秒,防止死鎖)
                boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
                if (locked) {
                    try {
                        // 4. 加鎖成功,查數據庫(這里模擬)
                        return queryUserFromDb(id);
                    } finally {
                        // 5. 釋放鎖
                        lock.unlock();
                    }
                } else {
                    // 6. 加鎖失敗,等待100毫秒后重試(遞歸調用自己)
                    TimeUnit.MILLISECONDS.sleep(100);
                    return getUserById(id);
                }
            } catch (InterruptedException e) {
                thrownew RuntimeException("獲取鎖失敗");
            }
        } else {
            // 非熱點數據,直接查數據庫
            return queryUserFromDb(id);
        }
    }

    // 模擬從數據庫查數據
    private User queryUserFromDb(Long id) {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        User user = new User();
        user.setId(id);
        user.setUsername("用戶" + id);
        user.setPhone("1380013800" + (id % 10));
        user.setCreateTime(LocalDateTime.now());
        return user;
    }
}

這樣一來,即使熱點數據緩存過期,也只有一個請求能去查數據庫,其他請求都等鎖釋放后查緩存,不會打垮數據庫。

3. 緩存雪崩:大量緩存同時過期,數據庫被壓垮

問題描述:比如你給所有緩存都設置了同一個過期時間(比如凌晨 3 點),到了 3 點,大量緩存同時過期,這時候正好有大量用戶訪問,所有請求都穿透到數據庫,數據庫直接扛不住 —— 這就是緩存雪崩。

解決方案:

  • 方案一:過期時間加隨機值。給每個緩存的過期時間加個隨機數,比如默認 30 分鐘,再加上 0-10 分鐘的隨機值,這樣緩存就不會同時過期了。

在 Redis 緩存配置里修改:

import java.util.Random;

// 個性化緩存規則時加隨機值
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
Random random = new Random();
// userCache過期時間:1小時 ± 10分鐘
long userCacheTtl = 3600 + random.nextInt(600); 
cacheConfigurations.put("userCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userCacheTtl)));
// userListCache過期時間:10分鐘 ± 2分鐘
long userListCacheTtl = 600 + random.nextInt(120);
cacheConfigurations.put("userListCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userListCacheTtl)));
  • 方案二:分批次過期。把緩存分成多個批次,比如按用戶 id 的尾號分 10 批,每批的過期時間差 10 分鐘,這樣即使一批過期,也只有 10% 的請求會打數據庫,壓力小很多。
  • 方案三:Redis 集群。用 Redis 集群(主從 + 哨兵)保證 Redis 的高可用,即使某個 Redis 節點掛了,其他節點還能提供緩存服務,避免 Redis 掛了導致所有請求打數據庫。

4. 緩存一致性:數據庫改了,緩存沒更

問題描述:比如你更新了數據庫里的用戶信息,但緩存里的還是舊數據,這時候用戶查到的就是舊數據 —— 這就是緩存一致性問題。這個問題在分布式系統里很常見,比如多實例部署時,實例 A 更新了數據庫,但實例 B 的緩存還是舊的。

解決方案:

  • 方案一:更新數據庫后立即更新 / 刪除緩存。就是咱們之前用的@CachePut(更新緩存)和@CacheEvict(刪除緩存),這是最常用的方式,適合大部分場景。

注意:要先更數據庫,再更緩存。如果先更緩存,再更數據庫,中間有其他請求查緩存,拿到的是新緩存,但數據庫還是舊的,會導致不一致。

  • 方案二:延遲雙刪。如果更新操作比較頻繁,或者多實例部署時緩存同步有延遲,可以用 “延遲雙刪”:先刪緩存,再更數據庫,過一會兒再刪一次緩存。

舉個例子:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;

@Service
publicclass UserService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 延遲雙刪:更新用戶信息
    public User updateUserWithDelayDelete(User user) {
        String cacheKey = "userCache::" + user.getId();

        // 第一步:先刪除緩存
        stringRedisTemplate.delete(cacheKey);

        // 第二步:更新數據庫
        User updatedUser = updateUserInDb(user);

        // 第三步:延遲1秒后再刪除一次緩存(用異步線程,不阻塞主流程)
        delayDeleteCache(cacheKey, 1);

        return updatedUser;
    }

    // 異步延遲刪除緩存
    @Async// 要在啟動類加@EnableAsync啟用異步
    public void delayDeleteCache(String key, long delaySeconds) {
        try {
            TimeUnit.SECONDS.sleep(delaySeconds);
            stringRedisTemplate.delete(key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 模擬更新數據庫
    private User updateUserInDb(User user) {
        // 數據庫更新邏輯...
        return user;
    }
}

為什么要刪兩次?因為第一步刪緩存后,可能有其他實例已經查了數據庫(舊數據),正在往緩存里存,這時候第二步更新數據庫后,緩存里還是舊數據。延遲 1 秒再刪一次,就能把這個舊緩存刪掉,保證一致性。

  • 方案三:用 Canal 監聽數據庫 binlog。如果業務對緩存一致性要求特別高,可以用 Canal 監聽 MySQL 的 binlog 日志,當數據庫數據變化時,Canal 會觸發事件,自動更新 / 刪除 Redis 緩存。這種方式比較復雜,但一致性最好,適合大型項目。

六、實戰案例:完整的用戶服務緩存實現

講了這么多理論,咱們來個完整的實戰案例,把前面的知識點串起來。實現一個用戶服務,包含 “查單個用戶、查用戶列表、更新用戶、刪除用戶” 四個接口,用 Spring Cache+Redis 實現緩存,解決常見問題。

1. 項目結構

com.example.demo
├── config
│ ├── RedisCacheConfig.java// Redis緩存配置
│ └── CustomKeyGeneratorConfig.java// 自定義Key生成器
├── entity
│ └── User.java// 用戶實體類
├── service
│ ├── UserService.java// 用戶服務接口
│ └── impl
│ └── UserServiceImpl.java// 用戶服務實現(帶緩存)
├── controller
│ └── UserController.java// 控制層
└── DemoApplication.java  // 啟動類

2. 關鍵代碼實現

(1)User.java(實體類)

import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

@Data
public class User implements Serializable {
    private Long id;
    private String username;
    private String phone;
    private Integer age;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

(2)RedisCacheConfig.java(緩存配置)

import org.springframework.cache.CacheManager;
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.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@Configuration
publicclass RedisCacheConfig {

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 1. 配置序列化
        RedisSerializer<String> keySerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper();
        objectMapper.activateDefaultTyping(
                com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL,
                com.fasterxml.jackson.databind.jsontype.TypeSerializer.DefaultImpl.NON_FINAL
        );
        objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
        valueSerializer.setObjectMapper(objectMapper);

        // 2. 默認緩存配置(過期時間30分鐘,加隨機值避免雪崩)
        Random random = new Random();
        long defaultTtl = 1800 + random.nextInt(300); // 30分鐘 ± 5分鐘
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(defaultTtl))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer))
                .enableCachingNullValues(); // 緩存null值,解決穿透

        // 3. 個性化緩存配置
        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
        // userCache:1小時 ± 10分鐘
        long userCacheTtl = 3600 + random.nextInt(600);
        cacheConfigs.put("userCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userCacheTtl)));
        // userListCache:10分鐘 ± 2分鐘
        long userListCacheTtl = 600 + random.nextInt(120);
        cacheConfigs.put("userListCache", defaultCacheConfig.entryTtl(Duration.ofSeconds(userListCacheTtl)));

        // 4. 創建緩存管理器
        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultCacheConfig)
                .withInitialCacheConfigurations(cacheConfigs)
                .build();
    }
}

(3)CustomKeyGeneratorConfig.java(Key 生成器)

import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;

@Configuration
publicclass CustomKeyGeneratorConfig {

    @Bean("customKeyGenerator")
    public KeyGenerator customKeyGenerator() {
        return (target, method, params) -> {
            // 生成規則:類名.方法名[參數列表](避免不同類方法名重復)
            String className = target.getClass().getSimpleName();
            String methodName = method.getName();
            String paramStr = Arrays.toString(params);
            return className + "." + methodName + "[" + paramStr + "]";
        };
    }
}

(4)UserService.java(接口)

import com.example.demo.entity.User;
import java.util.List;

publicinterface UserService {
    // 查單個用戶
    User getUserById(Long id);

    // 查用戶列表
    List<User> getUserList(Integer pageNum, Integer pageSize);

    // 更新用戶
    User updateUser(User user);

    // 刪除用戶
    void deleteUser(Long id);
}

(5)UserServiceImpl.java(服務實現,帶緩存)

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
// 統一配置:緩存命名空間、Key生成器
@CacheConfig(value = "userCache", keyGenerator = "customKeyGenerator")
publicclass UserServiceImpl implements UserService {

    // 模擬數據庫(實際項目中是MyBatis/JPA)
    private List<User> mockDb() {
        return Arrays.asList(
                createUser(1L, "張三", "13800138001", 20),
                createUser(2L, "李四", "13800138002", 25),
                createUser(3L, "王五", "13800138003", 30)
        );
    }

    private User createUser(Long id, String username, String phone, Integer age) {
        User user = new User();
        user.setId(id);
        user.setUsername(username);
        user.setPhone(phone);
        user.setAge(age);
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        return user;
    }

    /**
     * 查單個用戶:用緩存,解決穿透(緩存null)、擊穿(熱點數據加鎖)
     */
    @Override
    @Cacheable(unless = "#result == null") // 結果為null也緩存(配置里已enableCachingNullValues)
    public User getUserById(Long id) {
        // 模擬熱點數據(id=1是熱點)
        if (id == 1) {
            // 這里可以加分布式鎖,參考前面的緩存擊穿解決方案,省略代碼
        }

        // 模擬數據庫查詢耗時
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 從模擬數據庫查詢
        return mockDb().stream()
                .filter(user -> user.getId().equals(id))
                .findFirst()
                .orElse(null); // 查不到返回null,會被緩存
    }

    /**
     * 查用戶列表:用單獨的緩存命名空間,避免和單個用戶緩存沖突
     */
    @Override
    @Cacheable(value = "userListCache") // 覆蓋類上的value,用userListCache
    public List<User> getUserList(Integer pageNum, Integer pageSize) {
        // 模擬數據庫查詢耗時
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 模擬分頁(實際項目用PageHelper)
        int start = (pageNum - 1) * pageSize;
        int end = Math.min(start + pageSize, mockDb().size());
        return mockDb().subList(start, end);
    }

    /**
     * 更新用戶:更新緩存,同時刪除列表緩存(保證一致性)
     */
    @Override
    @Caching(
        put = {
            @CachePut(key = "#user.id") // 更新單個用戶緩存
        },
        evict = {
            @CacheEvict(value = "userListCache", allEntries = true) // 刪除列表緩存
        }
    )
    public User updateUser(User user) {
        // 模擬數據庫更新耗時
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 模擬更新數據庫(實際項目會更新MySQL)
        User updatedUser = createUser(
                user.getId(),
                user.getUsername(),
                user.getPhone(),
                user.getAge()
        );
        updatedUser.setUpdateTime(LocalDateTime.now());
        return updatedUser;
    }

    /**
     * 刪除用戶:刪除緩存,同時刪除列表緩存
     */
    @Override
    @Caching(
        evict = {
            @CacheEvict(key = "#id"), // 刪除單個用戶緩存
            @CacheEvict(value = "userListCache", allEntries = true) // 刪除列表緩存
        }
    )
    public void deleteUser(Long id) {
        // 模擬數據庫刪除耗時
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 模擬刪除數據庫記錄(實際項目會刪MySQL)
        System.out.println("用戶" + id + "已從數據庫刪除");
    }
}

(6)UserController.java(控制層)

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/user")
publicclass UserController {

    @Autowired
    private UserService userService;

    // 查單個用戶
    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) {
        long start = System.currentTimeMillis();
        User user = userService.getUserById(id);
        long end = System.currentTimeMillis();
        System.out.println("查詢耗時:" + (end - start) + "ms");
        return user;
    }

    // 查用戶列表(分頁)
    @GetMapping("/list")
    public List<User> getList(
            @RequestParam(defaultValue = "1") Integer pageNum,
            @RequestParam(defaultValue = "2") Integer pageSize) {
        long start = System.currentTimeMillis();
        List<User> list = userService.getUserList(pageNum, pageSize);
        long end = System.currentTimeMillis();
        System.out.println("列表查詢耗時:" + (end - start) + "ms");
        return list;
    }

    // 更新用戶
    @PutMapping
    public User update(@RequestBody User user) {
        return userService.updateUser(user);
    }

    // 刪除用戶
    @DeleteMapping("/{id}")
    public String delete(@PathVariable Long id) {
        userService.deleteUser(id);
        return"刪除成功";
    }
}

(7)DemoApplication.java(啟動類)

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableCaching // 啟用緩存
@EnableAsync // 啟用異步(延遲雙刪用)
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

3. 測試驗證

啟動項目后,用 Postman 測試:

1)查單個用戶:GET http://localhost:8080/user/1

  • 第一次耗時約 2000ms,存緩存。
  • 第二次耗時約 10ms,讀緩存。
  • 查id=999(不存在),返回 null,緩存 null 值,下次查耗時約 10ms。

2)查用戶列表:GET http://localhost:8080/user/list?pageNum=1&pageSize=2

  • 第一次耗時約 2000ms,存緩存到userListCache。
  • 第二次耗時約 10ms,讀緩存。

3)更新用戶:PUT http://localhost:8080/user,請求體:

{
    "id": 1,
    "username": "張三更新",
    "phone": "13800138001",
    "age": 21
}
  • 執行后,userCache里 id=1 的緩存被更新,userListCache被清空。
  • 再查id=1,拿到更新后的用戶;再查列表,耗時約 2000ms(重新存緩存)。

4)刪除用戶:DELETE http://localhost:8080/user/1

  • 執行后,userCache里 id=1 的緩存被刪除,userListCache被清空。
  • 再查id=1,耗時約 2000ms(重新查庫并存緩存)。

所有功能都正常,緩存也能正確工作,常見的緩存問題也都有解決方案 —— 這就是一個生產環境可用的 Spring Cache 實現!

七、總結:為什么說 Spring Cache 是優雅的緩存方式?

看到這里,你應該明白為什么我說 Spring Cache 優雅了吧?咱們總結一下:

  1. 代碼更簡潔:不用手動寫 “查緩存、存緩存、刪緩存” 的邏輯,一個注解搞定,業務代碼更純粹。
  2. 解耦更徹底:緩存邏輯和業務邏輯完全分開,換緩存中間件(Redis→Ehcache)不用改業務代碼。
  3. 配置更靈活:支持自定義序列化、Key 生成器、過期時間,還能多緩存管理器切換,滿足各種場景。
  4. 問題有解法:針對緩存穿透、擊穿、雪崩、一致性這些常見問題,都有成熟的解決方案,踩坑成本低。

最后給大家一個建議:別再用 HashMap 當緩存,也別再手動寫 RedisTemplate 操作緩存了,趕緊把 Spring Cache 用起來。剛開始可能會覺得配置有點麻煩,但一旦上手,你會發現, 這玩意兒是真的香!


責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2023-03-23 22:46:38

Spring限流機制

2023-05-05 18:38:33

多級緩存Caffeine開發

2022-03-31 09:13:49

Cache緩存高并發

2021-06-29 19:26:29

緩存Spring CachSpring

2023-10-30 07:56:46

Spring緩存

2021-04-20 10:50:38

Spring Boot代碼Java

2022-05-18 12:04:19

Mybatis數據源Spring

2023-02-13 08:10:40

Gateway網關Spring

2014-11-04 10:34:27

JavaCache

2020-10-25 19:58:04

Pythonic代碼語言

2023-08-01 08:54:02

接口冪等網絡

2009-09-22 10:50:04

Hibernate c

2022-01-26 10:09:25

安全漏洞掃描工具緩存投毒漏洞

2018-05-28 08:54:45

SparkRDD Cache緩存

2012-08-27 09:36:51

程序員創業讀書

2025-03-11 00:55:00

Spring停機安全

2023-11-09 08:01:41

Spring緩存注解

2025-09-22 09:31:34

2024-01-05 16:43:30

數據庫線程

2018-07-14 21:59:57

緩存數據庫數據
點贊
收藏

51CTO技術棧公眾號

91精品论坛| 97精品久久人人爽人人爽| 国产人妖ts一区二区| 97精品一区| 亚洲图片欧美色图| 精品91免费| www.久久网| 欧美日本一区二区高清播放视频| 亚洲黄色在线看| 爱情岛论坛亚洲首页入口章节| 国内外激情在线| heyzo一本久久综合| 国产精品视频不卡| 黄色小视频在线免费看| 日韩欧美综合| 亚洲国产另类 国产精品国产免费| 久久久久久三级| 俄罗斯一级**毛片在线播放 | 五月天国产精品| 中国人体摄影一区二区三区| 天堂中文在线视频| 欧美精品1区| 亚洲欧美精品中文字幕在线| 少妇愉情理伦片bd| 亚洲精品一区三区三区在线观看| 亚洲综合丁香婷婷六月香| 日韩欧美一区二区三区四区五区 | 亚洲国产成人爱av在线播放| 91制片厂毛片| 一级毛片久久久| 亚洲亚洲人成综合网络| 亚洲一区三区在线观看| 偷拍自拍在线| youjizz久久| 亚洲综合视频1区| 中文字幕一区二区免费| 成人精品中文字幕| 亚洲精品国精品久久99热| 久久久福利影院| 国产成人77亚洲精品www| 欧美日韩综合视频| 精品无码国模私拍视频| 日本精品600av| 亚洲视频网在线直播| 视频一区视频二区视频三区高| 五月婷婷激情在线| 北条麻妃一区二区三区| 成人av男人的天堂| 国产99久一区二区三区a片 | 国产精品大片| 欧美成人午夜影院| www.色就是色.com| 韩日精品一区| 欧美中文字幕不卡| 国产wwwxx| 日本精品网站| 欧美色图在线观看| 波多野结衣xxxx| 成人日韩av| 欧美日本在线播放| 91精产国品一二三产区别沈先生| av在线国产精品| 欧美一区二区三区免费视频| 香蕉视频xxxx| av成人男女| 日韩视频中午一区| zjzjzjzjzj亚洲女人| 国产乱人伦丫前精品视频| 亚洲大胆人体在线| 四虎永久免费影院| 精品美女视频| 久久精品国产一区二区三区| 欧美高清视频一区二区三区| 欧美日韩免费| 26uuu另类亚洲欧美日本老年| 99热国产在线观看| 天堂影院一区二区| 国产中文字幕日韩| www香蕉视频| 91美女精品福利| 天堂√在线观看一区二区| 欧美jizz18性欧美| 亚洲精品国产一区二区精华液 | 成人精品视频一区| 开心色怡人综合网站| 91涩漫在线观看| 夜夜嗨av一区二区三区网页| 日韩avxxx| 亚洲久草在线| 亚洲国产精品va在看黑人| 免费人成又黄又爽又色| 日韩精品一区二区三区中文在线| 日韩久久免费av| 白丝女仆被免费网站| 999精品视频| 国内精品久久久| 欧美激情一区二区三区免费观看| 国产一区二区电影| 欧美日韩一区二区三区在线视频 | 国产精品久久久毛片| 国产日韩在线观看视频| 亚洲第一男人天堂| 欧美性猛交xxxx乱大交少妇| 在线看片一区| 久久网福利资源网站| 日韩高清精品免费观看| 美日韩一区二区三区| 欧洲美女免费图片一区| 国产又大又粗又硬| 91一区二区在线观看| 黄色免费高清视频| 在线黄色的网站| 欧美mv和日韩mv的网站| 欧美视频国产视频| 香蕉久久99| 欧美激情精品久久久久久大尺度| 黄色网址中文字幕| 成人爽a毛片一区二区免费| 亚洲国产午夜伦理片大全在线观看网站| 男女在线视频| 午夜欧美视频在线观看| 最新天堂中文在线| 久久成人高清| 午夜免费在线观看精品视频| 日韩成人一区二区三区| 国内成+人亚洲+欧美+综合在线| 欧美另类高清视频在线| 136福利第一导航国产在线| 欧美丰满少妇xxxxx高潮对白| 美女伦理水蜜桃4| 亚洲激情久久| 成人h视频在线观看播放| 国产人成在线视频| 色中色一区二区| 中文字幕在线播放一区| 国产精品porn| 91视频九色网站| 免费网站成人| 欧美精品九九99久久| 91社区视频在线观看| 久久国产电影| 国产精品久在线观看| 欧美色综合一区二区三区| 图片区日韩欧美亚洲| 娇妻高潮浓精白浆xxⅹ| 激情成人亚洲| 国产欧美一区二区三区不卡高清| 好看的中文字幕在线播放| 日韩欧美国产系列| 青娱乐国产在线| 亚洲每日更新| 日本精品视频网站| 日本中文字幕一区二区有码在线| 亚洲a一区二区| 无码一区二区精品| 亚洲欧美日韩一区在线观看| 国产精品视频午夜| 3p在线观看| 欧美高清视频不卡网| 国产成人av免费在线观看| 亚洲电影成人| 精品国产一区二区三区麻豆免费观看完整版 | 日韩免费精品视频| 国产福利在线看| 欧美另类z0zxhd电影| 亚洲综合视频网站| 国产成人免费网站| 青娱乐自拍偷拍| 国产成人一区| 国产精品一区二区三区毛片淫片 | 97香蕉超级碰碰久久免费的优势| 亚洲人成色777777精品音频| 91国产精品成人| 熟女少妇a性色生活片毛片| 国产一区二区三区在线观看免费视频| 今天免费高清在线观看国语| 免费福利视频一区| 国产精品久久久久久av福利| а√天堂在线官网| 亚洲精品久久久一区二区三区| 无码人妻精品一区二区蜜桃色欲 | 国产小视频91| 国产丝袜在线视频| 91丨porny丨蝌蚪视频| 女人另类性混交zo| 一个色综合网| 鲁丝一区二区三区免费| 日韩专区视频| 欧美性在线视频| 免费大片黄在线| 亚洲国产小视频在线观看| 毛片在线免费播放| 亚洲国产综合色| 免费成人深夜天涯网站| 国产ts人妖一区二区| 99视频在线免费| 欧美日韩国产一区精品一区| 日韩欧美精品久久| a级日韩大片| 成人精品在线观看| 成人免费看黄| 久久久亚洲精选| 麻豆视频网站在线观看| 亚洲女人被黑人巨大进入| 国产激情视频在线播放| 欧洲av一区二区嗯嗯嗯啊| 国产亚洲精品久久777777| 欧美国产日产图区| 亚洲精品在线视频免费观看| 国产一区久久久| 天天爽天天爽夜夜爽| 亚洲裸体俱乐部裸体舞表演av| 一区二区三区四区视频在线| 蜜桃精品wwwmitaows| 国产精品国产精品国产专区蜜臀ah| 国产激情久久| 情事1991在线| 美女高潮在线观看| 欧美多人乱p欧美4p久久| 日韩黄色影院| 中文字幕亚洲图片| 国产青青草在线| 日韩精品在线观看视频| 蜜臀av中文字幕| 日韩亚洲欧美成人一区| 在线视频你懂得| 欧美在线视频不卡| 一级黄色在线视频| 欧美日韩亚洲高清| 国产在线一二区| 亚洲一区视频在线观看视频| 日韩欧美国产成人精品免费| 国产精品入口麻豆九色| 99精品全国免费观看| 国产亚洲欧美日韩俺去了| 在线免费观看成年人视频| av电影天堂一区二区在线观看| 国产一精品一aⅴ一免费| 国产一区二区影院| 91丨porny丨九色| 国产精品一区二区在线观看不卡| www.cao超碰| 裸体在线国模精品偷拍| 亚洲欧美久久久久| 久久99精品一区二区三区三区| 色噜噜狠狠永久免费| 免费看欧美女人艹b| jizzzz日本| 经典一区二区三区| 日本中文字幕在线不卡| 国产精品夜夜嗨| 激情av中文字幕| 99精品视频在线免费观看| 网站免费在线观看| 91老师片黄在线观看| 国产成人精品无码免费看夜聊软件| 久久久久久久久97黄色工厂| 久久视频精品在线观看| 国产精品美女视频| av激情在线观看| 国产午夜亚洲精品不卡| 亚洲一区二区三区日韩| 欧美激情在线一区二区三区| 国产免费无码一区二区| 成人午夜视频免费看| 亚洲第九十七页| 国产性天天综合网| 天天做夜夜爱爱爱| 亚洲老司机在线| 日韩久久久久久久久| 91国在线观看| 国产av无码专区亚洲a∨毛片| 精品国产sm最大网站免费看| 青青色在线视频| 日韩在线激情视频| 黑人精品视频| 国产精品久久久久福利| 视频在线一区| 欧美久久电影| 亚洲国产精品综合久久久 | 久久中文精品| 爱豆国产剧免费观看大全剧苏畅| 成人在线综合网| 国产又大又粗又爽的毛片| 亚洲美女屁股眼交| av大片免费观看| 欧美精选一区二区| 色婷婷av一区二区三区之e本道| 亚洲视频在线观看网站| 亚洲男人第一天堂| 亚洲精品视频在线观看视频| 欧美边添边摸边做边爱免费| 91福利视频网| 国产精品1区| 日本精品一区二区三区不卡无字幕| 香蕉视频官网在线观看日本一区二区| 国产精品又粗又长| 久久国产欧美日韩精品| 人体私拍套图hdxxxx| 国产精品第一页第二页第三页| 久草精品视频在线观看| 欧美三级欧美一级| 亚洲欧美日韩精品永久在线| 九色成人免费视频| 成人va天堂| 国产伦精品一区二区三区高清版| 日韩a一区二区| 成人在线免费观看av| 国产精品18久久久久久久久| 波多野结衣av在线观看| 亚洲成人1区2区| 国产偷拍一区二区| 一区二区中文字幕| 欧美日韩在线观看首页| 999热视频| 日本免费一区二区视频| 日韩久久精品一区二区三区| 亚洲精品三级| 国产在线a视频| 亚洲欧洲一区二区在线播放| 91精品国产高清一区二区三密臀| 一本色道久久综合亚洲91 | 亚洲人成伊人成综合网久久久| 免费网站在线观看人| 国产噜噜噜噜噜久久久久久久久| 亚洲另类av| 国产深夜男女无套内射| 国产成人综合网| 777777国产7777777| 欧美日韩专区在线| 韩日视频在线| 日本在线精品视频| 天堂99x99es久久精品免费| a级黄色小视频| 成人综合在线视频| 免费看一级一片| 欧美一级日韩免费不卡| 黄色在线免费网站| 亚洲精品女av网站| 欧美福利电影在线观看| 亚洲911精品成人18网站| 亚洲男人的天堂在线观看| 91精品国产乱码久久| zzjj国产精品一区二区| 国产视频网站一区二区三区| 丰满女人性猛交| 国产乱妇无码大片在线观看| 亚洲国产美女视频| 欧美一区二区福利视频| 在线观看午夜av| 国产99视频精品免费视频36| 红桃视频欧美| 人妻在线日韩免费视频| 欧美日韩亚洲激情| 极品美乳网红视频免费在线观看 | 午夜精品一区二| 国产午夜精品一区理论片飘花| 123成人网| 中文字幕乱码一区二区三区| 黄色资源网久久资源365| 老湿机69福利| 亚洲成av人片在线观看香蕉| 9999热视频在线观看| 美日韩免费视频| 蜜桃av一区二区三区电影| 成年人一级黄色片| 精品国产欧美一区二区| 亚洲人成在线网站| 亚洲不卡一卡2卡三卡4卡5卡精品| 日韩成人一区二区三区在线观看| 国产一区二区三区视频播放| 日韩欧美中文字幕公布| 黄色视屏在线免费观看| 色姑娘综合av| 国产成人自拍网| 欧美啪啪小视频| xxx欧美精品| 国产精品极品| 亚洲综合在线网站| 一区二区三区四区在线播放| 日本在线视频1区| 91色中文字幕| 亚洲一区二区伦理| 日日操免费视频| 欧美性猛交xxxx富婆弯腰| 成人在线免费观看| 2019国产精品视频| 母乳一区在线观看| chinese全程对白| 日韩成人在线视频观看| 久久久久黄色| 日韩在线综合网| 日韩理论片在线| 欧美色18zzzzxxxxx| 亚洲已满18点击进入在线看片| 亚洲综合99| 欧美日韩偷拍视频| 中文字幕日韩欧美在线 | 亚洲二区在线播放视频| 成人国产精品入口免费视频|