精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

污视频在线看网站| 亚洲综合五月天婷婷丁香| 粉嫩av一区二区| 欧美性xxxxx极品娇小| 欧洲在线视频一区| 国产伦子伦对白视频| 日韩午夜av在线| 正在播放欧美一区| 911亚洲精选| 欧美一级二级视频| 亚洲综合激情网| 日韩av不卡播放| 国产模特av私拍大尺度| 国产一区91| 久久视频国产精品免费视频在线| 久久福利小视频| 国内欧美日韩| 精品露脸国产偷人在视频| 亚洲制服欧美久久| 午夜在线观看视频18| 黑人精品欧美一区二区蜜桃| 7777精品视频| 91制片厂在线| 综合国产视频| 精品伦理精品一区| 在线观看免费黄网站| 欧美男男激情videos| 一区二区三区四区乱视频| 视频三区二区一区| 性xxxx18| 不卡一卡二卡三乱码免费网站| 国产在线精品自拍| 免费视频网站在线观看入口| 亚洲高清av| 色偷偷噜噜噜亚洲男人的天堂| 韩国无码一区二区三区精品| 一本色道69色精品综合久久| 91麻豆精品国产91久久久久久| 91淫黄看大片| 625成人欧美午夜电影| 亚洲一区二区视频在线观看| 9l视频自拍9l视频自拍| 日本三级视频在线播放| 中文字幕va一区二区三区| 欧美日韩最好看的视频| 五月激情婷婷综合| youjizz久久| 国产精品久久亚洲7777| www国产一区| 精品无码三级在线观看视频| 国产精品丝袜视频| 99成人精品视频| 日韩精品一卡二卡三卡四卡无卡| 51精品国产黑色丝袜高跟鞋| 国产精品一区二区6| 一区视频在线看| 欧美—级高清免费播放| 久久无码精品丰满人妻| 在线成人激情| 欧美极品少妇全裸体| 青青草手机在线观看| 欧美日韩影院| 久久久久亚洲精品成人网小说| 久久久久香蕉视频| 99精品视频免费| 日本欧美国产在线| 日韩国产亚洲欧美| 免费观看30秒视频久久| 国产在线98福利播放视频| 一级片在线免费观看视频| 激情成人综合网| 91沈先生播放一区二区| 亚洲精品一区二区三区区别| 成人黄色a**站在线观看| 精品日本一区二区三区在线观看| 手机福利小视频在线播放| 国产夜色精品一区二区av| 日韩高清av电影| 毛片网站在线免费观看| 亚洲激情成人在线| 少妇高潮喷水在线观看| 欧美亚洲韩国| 欧美福利一区二区| 四虎精品一区二区| 国产亚洲电影| 精品国内产的精品视频在线观看| 国产精品九九九九九九| 一区二区激情| 国产精品麻豆va在线播放| 国产一区二区三区中文字幕| 国产999精品久久| 欧美日韩大片一区二区三区| 免费av网站在线观看| 午夜视频在线观看一区二区 | 91精品啪在线观看国产18| 久久色在线播放| 国产精品第9页| 久久精品国产亚洲高清剧情介绍| 国产传媒欧美日韩| 国产香蕉在线| 亚洲综合无码一区二区| 另类小说第一页| 91大神精品| 日韩中文字幕第一页| 日韩av一区二区在线播放| 免费观看在线综合色| 国产精品伊人日日| 免费黄网站在线| 欧美日韩亚洲系列| 成年人性生活视频| 人人狠狠综合久久亚洲婷| 极品束缚调教一区二区网站| 欧美一三区三区四区免费在线看| 丰满岳乱妇一区二区| 日韩欧美国产精品综合嫩v| 久久久人成影片一区二区三区| 国产一区二区视频免费| 风间由美性色一区二区三区| 亚洲图片在线观看| 悠悠资源网亚洲青| 日韩欧美在线综合网| 久久久视频6r| 亚洲欧洲午夜| 91亚洲精品久久久| 国产毛片在线| 欧美日韩国产一中文字不卡| 久久久久亚洲av片无码v| 成人情趣视频网站| 欧亚精品中文字幕| 高清毛片aaaaaaaaa片| 18欧美亚洲精品| 999精品网站| 牲欧美videos精品| 久久久久久久久久久人体| 国产精品一区二区黑人巨大| 国产精品视频在线看| 日本精品一区二区三区四区 | 国语精品免费视频| 大片免费在线观看| 欧美美女一区二区三区| 麻豆视频免费在线播放| 日韩av中文字幕一区二区| 久久一区免费| 牛牛精品一区二区| 亚洲国产精彩中文乱码av在线播放| 欧美日韩三级在线观看| 国产美女在线观看一区| 影音先锋男人的网站| 国产aa精品| 美女性感视频久久久| 99久久婷婷国产一区二区三区| 国产精品伦一区二区三级视频| 日日噜噜噜噜久久久精品毛片| 综合干狼人综合首页| 日本免费久久高清视频| 国产youjizz在线| 欧美中文字幕一区| 中文字幕黄色网址| 麻豆精品视频在线| 中文字幕中文字幕在线中一区高清| а√天堂资源国产精品| 日韩在线视频观看| 国产精品久久久久精| 亚洲欧洲中文日韩久久av乱码| 亚洲视频在线不卡| 欧美日韩免费观看一区=区三区| 国产98在线|日韩| 白白色在线观看| 日韩激情片免费| 丰满人妻一区二区三区四区| 中文一区二区在线观看| 日本网站在线看| 国产精品啊啊啊| 免费日韩av电影| 黄页免费欧美| 欧美大片免费观看| 免费在线一级视频| 欧美日韩国产高清一区| 久久久99精品| 2014亚洲片线观看视频免费| 自拍偷拍一区二区三区四区| 你懂的国产精品永久在线| 国产欧美一区二区三区另类精品| 黄色在线免费观看网站| 在线精品91av| 亚洲国产精品久久人人爱潘金莲 | 91一区二区在线| 日本新janpanese乱熟| 欧美淫片网站| 欧美久久在线| 国产美女视频一区二区| 97福利一区二区| 永久免费在线观看视频| 欧美精品一区二区三区很污很色的 | 亚洲人挤奶视频| 成人久久一区二区三区| av手机在线观看| 日韩网站免费观看| 天堂在线资源网| 欧美高清激情brazzers| 黄色在线观看国产| 亚洲精品成人a在线观看| 亚洲天堂久久新| 国产精品中文字幕欧美| 国产综合免费视频| 国产一区亚洲| 亚洲a∨一区二区三区| 粉嫩久久久久久久极品| 成人免费淫片视频软件| 成人直播视频| 久久久久国产精品www| 天堂аⅴ在线地址8| 亚洲国产天堂网精品网站| 国产视频手机在线观看| 色久优优欧美色久优优| 欧美一区二区三区爽爽爽| 国产亚洲精品7777| 99re久久精品国产| 国产成人精品1024| 91插插插影院| 日韩av二区在线播放| 日本www在线视频| 欧美精品三级| 正在播放国产精品| 精品国产一区一区二区三亚瑟| 国产精品一区二区三区在线| 精品午夜av| 成人在线播放av| 成人免费在线观看视频| 日本精品性网站在线观看| а_天堂中文在线| 欧美日韩高清在线观看| 顶级网黄在线播放| 久久久精品免费视频| av在线天堂播放| 亚洲午夜久久久影院| 天堂91在线| 日韩av中文字幕在线播放| 亚洲第一天堂网| 日韩欧美国产综合一区| 国产三级视频在线播放| 欧美一区二区三区在线看| 91成人在线免费| 欧美日韩不卡一区| 在线观看毛片网站| 欧美绝品在线观看成人午夜影视| 国产精品无码一区| 欧美日韩卡一卡二| 一级黄色大片免费| 69成人精品免费视频| 国产欧美一区二区三区视频在线观看| 欧美乱妇15p| 国产精品一区二区人人爽| 69久久99精品久久久久婷婷| 国产人妻精品一区二区三| 欧美一二三四在线| 囯产精品久久久久久| 精品电影一区二区三区| 姝姝窝人体www聚色窝| 精品视频在线观看日韩| 理论在线观看| 中文字幕综合在线| 国产黄色在线观看| 久久久久久91| 黑人巨大精品| 国产精品丝袜高跟| 日韩中文字幕在线一区| 国产精品露出视频| 亚洲性视频大全| 一级做a爰片久久| 亚洲精品一区二区妖精| 中文字幕人妻熟女人妻洋洋| 亚洲精品三级| 日韩精品无码一区二区三区免费 | 国产精品美女视频网站| 天天综合在线观看| wwwxx欧美| 色综合综合网| 久久观看最新视频| 亚洲综合社区| 五月婷婷六月丁香激情| 国产精品99久| 97人妻精品一区二区免费| 中文字幕一区免费在线观看| 国语对白一区二区| 日本福利一区二区| a天堂中文在线观看| 精品性高朝久久久久久久| 美女国产在线| 亲子乱一区二区三区电影| 成人一级视频| 国产亚洲欧美一区二区| 欧美一级淫片| 久草视频国产在线| 蜜桃av一区二区| 黄色av电影网站| 欧美国产欧美亚州国产日韩mv天天看完整| 日韩一区二区不卡视频| 欧美色欧美亚洲高清在线视频| 国产精品久久777777换脸| 日韩国产欧美区| a视频在线免费看| 国产成人精品999| 成人高潮a毛片免费观看网站| 亚洲国产精品123| 一区二区毛片| 国产黑丝在线视频| 国产日韩欧美麻豆| 国产无遮挡又黄又爽又色| 欧美日韩激情一区| 五月婷婷六月丁香综合| 九九精品视频在线观看| 成人mm视频在线观看| 国产一区二区在线观看免费播放| 羞羞色午夜精品一区二区三区| 亚洲乱码中文字幕久久孕妇黑人| 国产剧情一区在线| 国产传媒在线看| 日韩欧美在线免费| 国产小视频一区| 久久久精品国产亚洲| 欧美日一区二区三区| 久久综合精品一区| 亚洲午夜激情在线| 国模大尺度视频| 国产精品美女久久久久久2018| 男人的天堂一区二区| 日韩精品一区国产麻豆| 九色porny在线| 国产精品一区二区三区久久久| 久久不见久久见免费视频7| 久久99久久久久久| 国产精品一品视频| 中文字幕电影av| 欧美日韩国产首页在线观看| 成人高清免费观看mv| 国产mv久久久| 免费国产自久久久久三四区久久| 久久精品国产sm调教网站演员| 国产激情视频一区二区三区欧美| 国产老头老太做爰视频| 欧美美女激情18p| 麻豆影院在线| 91久久国产精品| 国产精品久久天天影视| 五月激情五月婷婷| 亚洲欧洲另类国产综合| 97人妻精品一区二区三区视频 | 国产第一页浮力| 555夜色666亚洲国产免| 精品51国产黑色丝袜高跟鞋| 91视频-88av| 午夜激情一区| 日批免费观看视频| 午夜精品成人在线视频| 五月婷婷在线播放| 日韩免费观看网站| 欧美日韩在线网站| 色天使在线观看| 亚洲欧美日韩国产成人精品影院| 国产精品久久久久久久免费| 免费99精品国产自在在线| 欧美视频二区欧美影视| 免费网站在线观看视频| 不卡的看片网站| 国产午夜无码视频在线观看| 在线视频精品一| 国产精品久久久久久av公交车| 日韩美女爱爱视频| 91捆绑美女网站| 中文字幕人妻一区二区在线视频 | www深夜成人a√在线| 99久久影视| 亚洲人精品午夜在线观看| 李丽珍裸体午夜理伦片| 性欧美lx╳lx╳| 色婷婷av一区二区三区久久| 欧美a级片免费看| 欧美破处大片在线视频| 69久久夜色精品国产69乱青草| 亚洲天堂男人av| 开心九九激情九九欧美日韩精美视频电影 | 荫蒂被男人添免费视频| 午夜精品aaa| 国产福利在线| 1卡2卡3卡精品视频| 在线亚洲自拍| 特黄一区二区三区| 精品成人a区在线观看| 中文另类视频| 日韩亚洲欧美一区二区| 久久亚洲二区三区| 99re只有精品| 欧美做受高潮电影o| 欧美大片一区| www.99热| 亚洲精品久久久久国产| 99久久久国产| 国产精品97在线| 一区二区在线免费观看|