Spring Cache 整合 Redis 做快取使用~ 快速上手~

語言: CN / TW / HK

highlight: atom-one-dark theme: awesome-green


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第6天,點選檢視活動詳情

前三篇文章說了那麼那麼多,但是我們在使用快取的場景中,大都數還是會採用了類似 Spring Cache 的快取管理器來做,說原因其實也沒啥,因為專案中並不是所有的業務對資料有那麼強的資料一致性。

前三篇:

聊一聊快取和資料庫不一致性問題的產生及主流解決方案以及擴充套件的思考

用萬字長文來講講本地鎖至分散式鎖的演進和Redis實現,擴充套件:Redlock 紅鎖

週四埋下的坑,週五來惡補!! Redisson 加鎖、鎖自動續期、解鎖原始碼分析

Spring Cache 正好可以幫我們減輕開發負擔,一個註解就搞定,不用自己去程式設計式操作。

Spring Cache 介紹

看到Spring就知道這是Spring生態中的東西,其實快取資料的技術並不少,Spring 官方此舉是引入 Spring Cache 來幫我們管理快取,使用註解,簡化很多操作。

當然使用 Spring Cache 也有優缺點的.

優點

  • 使用註解,簡化操作
  • 快取管理器,方便多種實現切換快取源,如Redis,Guava Cache等
  • 支援事務, 即事物回滾時,快取同時自動回滾

缺點

  • 不支援TTL,不能為每個 key 設定單獨過期時間 expires time
  • 針對多執行緒沒有專門的處理,所以當多執行緒時,是會產生資料不一致性的。(同樣,一般有高併發操作的快取資料,都會特殊處理,而不太使用這種方式)

Spring Cache 快速上手

不想那麼多,先快速上個手,再接著詳細說一說。

SpringBoot 常規步驟:

  • 匯入依賴
  • 修改配置檔案(這一步也可以直接寫在第三步)
  • 編寫xxxxConfig 或者是Enablexxxx

前期準備

這也一樣的,另外我這裡使用的是 Spring Cache 整合 Redis 做快取。

<dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-cache</artifactId>  </dependency>  <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-data-redis</artifactId>  </dependency>

一般看到是spring-boot-starter開頭的依賴,都可以大膽猜測他們是有一個xxxProperties配置類與之對應的。

修改配置檔案:

spring:   redis:     host: xxxxx     password: xxxx    #指定快取型別   cache:     type: redis    #指定存活時間(ms)     redis.time-to-live: 86400000    #是否快取空值,可以防止快取穿透     redis.cache-null-values: true

與之對應的配置類,大夥可以自己去看看,能配置些啥

image.png

另外,在這裡進行配置的,在我們的編寫xxxConfig類的時候,也同樣可以在那裡配置。

因為也要配置Redis的配置,就把之前文章裡面的東西都貼上過來了~

java  /**   * @description:   * @author: Ning Zaichun   * @date: 2022年10月22日 23:21   */  @EnableConfigurationProperties(CacheProperties.class)  @EnableCaching  @Configuration  public class MyRedisConfig {  ​      private final Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer(Object.class);       {          ObjectMapper objectMapper = new ObjectMapper();          objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);          objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);          serializer.setObjectMapper(objectMapper);     }      /**       * 1.原來的配置類形式       * @ConfigurationProperties(prefix = "spring.cache")       * public class CacheProperties {       * 因為這個並沒有放到容器中,所以要讓他生效 @EnableConfigurationProperties(CacheProperties.class)       * 因為這個和配置檔案已經繫結生效了       * @return       */      @Bean      RedisCacheConfiguration redisCacheConfiguration(CacheProperties CacheProperties) {          RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();          //因為key的序列化預設就是 StringRedisSerializer  //       config = config.serializeKeysWith(RedisSerializationContext  //               .SerializationPair  //               .fromSerializer(new StringRedisSerializer()));  ​          config = config.serializeValuesWith(RedisSerializationContext                 .SerializationPair                 .fromSerializer(serializer));  ​          CacheProperties.Redis redisProperties = CacheProperties.getRedis();          if (redisProperties.getTimeToLive() != null) {              config = config.entryTtl(redisProperties.getTimeToLive());         }          if (!redisProperties.isCacheNullValues()) {              config = config.disableCachingNullValues();         }          if (!redisProperties.isUseKeyPrefix()) {              config = config.disableKeyPrefix();         }          return config;     }  ​      @Bean      public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {          RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();  ​          //設定value 值的序列化          redisTemplate.setValueSerializer(serializer);          //key的序列化          redisTemplate.setKeySerializer(new StringRedisSerializer());  ​          // set hash hashkey 值的序列化          redisTemplate.setHashKeySerializer(new StringRedisSerializer());          // set hash value 值的序列化          redisTemplate.setHashValueSerializer(serializer);  ​          redisTemplate.setConnectionFactory(redisConnectionFactory);          redisTemplate.afterPropertiesSet();          return redisTemplate;     }  ​      @Bean      public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {          return new StringRedisTemplate(redisConnectionFactory);     }  }

前期準備結束,直接上手使用~

開始使用

controller-- service--mapper 一路到底,我這裡是連線了資料庫,只是測試的話,直接在service 的返回結果中存一串字串即可。

java  /**   * @description:   * @author: Ning Zaichun   * @date: 2022年09月06日 22:16   */  @RestController  @RequestMapping("/cache")  @RequiredArgsConstructor  public class CacheController {  ​      private final IUseSpringCache useSpringCache;  ​      @GetMapping("/test")      public String getTest() {          return useSpringCache.getTest();     }  ​  ​      @GetMapping("/test2")      public String getTest2() {          return useSpringCache.getTest2();     }  ​  ​      @GetMapping("/test/clear")      public String clearTest() {          useSpringCache.clearTest();          return "clearTest";     }  ​      @GetMapping      public List<MenuEntity> getMenuList() {          return useSpringCache.getMenuList();     }  ​      @GetMapping("/clear")      public String updateMenu() {          MenuEntity menuEntity = new MenuEntity();          menuEntity.setCatId(33L);          menuEntity.setName("其他測試資料");          useSpringCache.updateMenuById(menuEntity);          return "成功清空快取";     }  }

java  /**   * @description:   * @author: Ning Zaichun   * @date: 2022年09月21日 20:30   */  public interface IUseSpringCache {  ​      String getTest();  ​      String getTest2();  ​      void clearTest();  ​      List<MenuEntity> getMenuList();  ​      void updateMenuById(MenuEntity menuEntity);  }

java  /**   * @description:   * @author: Ning Zaichun   * @date: 2022年09月21日 20:30   */  @Service  @RequiredArgsConstructor  public class UseSpringCacheImpl implements IUseSpringCache {  ​      private final MenuMapper menuMapper;  ​      @Cacheable(value = {"menu"}, key = "'getMenuList'")      @Override      public List<MenuEntity> getMenuList() {          System.out.println("查詢資料庫======");          List<MenuEntity> menuEntityList = menuMapper.selectList(new QueryWrapper<>());          return menuEntityList;     }  ​      /**       * 級聯更新所有關聯的資料       *       * @param menuEntity       * @CacheEvict:失效模式       * @CachePut:雙寫模式,需要有返回值 1、同時進行多種快取操作:@Caching       * 2、指定刪除某個分割槽下的所有資料 @CacheEvict(value = "menu",allEntries = true)       * 3、儲存同一型別的資料,都可以指定為同一分割槽       */      // @Caching(evict = {      //         @CacheEvict(value = "category",key = "'getLevel1Categorys'"),      //         @CacheEvict(value = "category",key = "'getCatalogJson'")      // })      @CacheEvict(value = "menu", allEntries = true)       //刪除某個分割槽下的所有資料      @Transactional(rollbackFor = Exception.class)      @Override      public void updateMenuById(MenuEntity menuEntity) {          System.out.println("清空快取======");          menuMapper.updateById(menuEntity);     }  ​      @Cacheable(value = {"test"}, key = "#root.methodName")      @Override      public String getTest() {          System.out.println("測試查詢了資料庫");          return "我是測試快取資料";     }  ​      @Cacheable(value = {"test"}, key = "'getTest2'")      @Override      public String getTest2() {          System.out.println("測試查詢了資料庫2");          return "我是測試快取資料2";     }  ​       @Caching(evict = {               @CacheEvict(value = "test",key = "'getTest'")       })      @Override      public void clearTest() {           System.out.println("清空了test快取");     }  }

測試

上面就是簡單的使用,上面的註解啥的,馬上就開說哈

先講講案例中的兩個刪除快取的註解

java  @CacheEvict(value = "menu", allEntries = true)    @Caching(evict = {           @CacheEvict(value = "test",key = "'getTest'")   })

兩種方式,allEntries = true表示直接清空掉整個分割槽,

而第二種方式,只會清掉getTest的分割槽。

Redis的快取,它的格式是這樣的。

image.png

採用第二種方式時,只會清理掉getTest的分割槽。

變成下面這樣:

image.png

上面的案例,我只是使用最簡單的方式使用了一下 Spring Cache

但其實註解上遠不止這麼一點東西,接下來慢慢說一說👇

大家也不用刻意記,就大致知道Spring cache可以解決什麼問題即可。

Spring Cache 註解

只有使用public定義的方法才可以被快取,而private方法、protected 方法或者使用default 修飾符的方法都不能被快取。 當在一個類上使用註解時,該類中每個公共方法的返回值都將被快取到指定的快取項中或者從中移除。

  • @Cacheable
  • @CachePut
  • @CacheEvict
  • @Caching
  • @CacheConfig

@Cacheable

| 屬性名 | 作用與描述 | | ---------------- | ------------------------------------------------------------------------------------------------------------------ | | cacheNames/value | 指定快取的名字,快取使用CacheManager管理多個快取Cache,這些Cache就是根據該屬性進行區分。對快取的真正增刪改查操作在Cache中定義,每個快取Cache都有自己唯一的名字。 | | key | 快取資料時的key的值,預設是使用方法所有入參的值。1、可以使用SpEL表示式表示key的值。2、可以使用字串,3、可以使用方法名 | | keyGenerator | 快取的生成策略(鍵生成器),和key二選一,作用是生成鍵值key,keyGenerator可自定義。 | | cacheManager | 指定快取管理器(例如ConcurrentHashMap、Redis等)。 | | cacheResolver | 和cacheManager作用一樣,使用時二選一。 | | condition | 指定快取的條件(對引數判斷,滿足什麼條件時才快取),可用SpEL表示式,例如:方法入參為物件user則表示式可以寫為condition = "#user.age>18",表示當入參物件user的屬性age大於18才進行快取。 | | unless | 否定快取的條件(對結果判斷,滿足什麼條件時不快取),即滿足unless指定的條件時,對呼叫方法獲取的結果不進行快取,例如:unless = "result==null",表示如果結果為null時不快取。 | | sync | 是否使用非同步模式進行快取,預設false。 |

@Cacheable指定了被註解方法的返回值是可被快取的。其工作原理是就是AOP機制,實際上,Spring 首先查詢的是快取,快取中沒有再查詢的資料庫。

接下來就說說幾種用法:

java  @Cacheable(value = "users")  //Spring 從4.0開始新增了value別名cacheNames比value更達意,推薦使用  @Cacheable(cacheNames = "users")  ​  //綜合使用  @Cacheable(cacheNames = {"test"}, key = "'getTest3'",condition = "#number>12",unless = "#number<12")

測試~

image.png

當我不傳值,因為不滿足條件,Redis 中是不會快取的

image.png

只有滿足number>12 時才會進行快取

image.png

下面的註解中含有的condition和unless屬性的都是同樣的用法。

@CachePut

@CachePut的註解屬性就比@Cacheable 少了一個sync,其餘都一樣。

@CachePut註解你就直接理解為執行後更新快取就好。

就比如我一個方法是快取某個學生或者是某個使用者資訊。

然後我修改了我的個人資訊什麼之類的,這個時候就可以直接用上@CachePut註解了。

比如:

java  /**   * studentCache   * 快取鍵值key未指定預設為userNumber+userName組合字串   */  @Cacheable(cacheNames = "studentCache")  @Override  public Student getStudentById(String id) {      // 方法內部實現不考慮快取邏輯,直接實現業務      return getFromDB(id);  }  ​  /**   * 註解@CachePut:確保方法體內方法一定執行,執行完之後更新快取;   * 相同的快取userCache和key(快取鍵值使用spEl表示式指定為userId字串)以實現對該快取更新;   * @param student   * @return 返回   */  @CachePut(cacheNames = "studentCache", key = "(#student.id)")  @Override  public Student updateStudent(Student student) {      return updateData(student);  }  ​  private Student updateData(Student student) {      System.out.println("real updating db..." + student.getId());      return student;  }  ​  private Student getFromDB(String id) {      System.out.println("querying id from db..." + id);      return new Student(id,"寧在春","社會",19);  }

結果:

image.png

更新之後

image.png

@CacheEvict

@CacheEvict註解是@Cachable註解的反向操作,它負責從給定的快取中移除一個值。大多數快取框架都提供了快取資料的有效期,使用該註解可以顯式地從快取中刪除失效的快取資料。

cacheNames/value、key、keyGenerator、cacheManager、cacheResolver、condition這些和上面一樣的屬性就不說了

它還有兩個其他的屬性:

allEntries:allEntries是布林型別的,用來表示是否需要清除這個快取分割槽中的的所有元素。預設值為false,表示不需要。

beforeInvocation: 清除操作預設是在對應方法執行成功後觸發的(beforeInvocation = false),即方法如果因為丟擲異常而未能成功返回時則不會觸發清除操作。使用beforeInvocation屬性可以改變觸發清除操作的時間。當指定該屬性值為true時,Spring會在呼叫該方法之前清除快取中的指定元素。

之前也簡單的使用過了,就不多測試啦,讓我偷個懶~

大夥想要啥騷操作的話,就得多去嘗試~

@Caching

@Caching註解屬性一覽:

| 屬性名 | 作用與描述 | | -----| ------------------------------------------------- | | cacheable | 取值為基於@Cacheable註解的陣列,定義對方法返回結果進行快取的多個快取。 | | put | 取值為基於@CachePut註解的陣列,定義執行方法後,對返回方的方法結果進行更新的多個快取。 | | evict | 取值為基於@CacheEvict註解的陣列。定義多個移除快取。 |

總結來說,@Caching是一個組註解,可以為一個方法定義提供基於@Cacheable@CacheEvict或者@CachePut註解的陣列。

就比如:

你如果使用@CacheEvict(value = "test",key = "'getTest'")這條註解,只能清理某一個分割槽的快取,test下的getTest所快取的資料,你沒辦法再清理其他分割槽的快取。

使用了@Caching就可以一次清理多個。

@Caching(evict = {      @CacheEvict(value = "test",key = "'getTest'"),      @CacheEvict(value = "test",key = "'getTest2'"),      @CacheEvict(value = "test",key = "'getTest3'"),  })

其他的也類似。

@CacheConfig

@CacheConfig註解屬性一覽:cacheNames/value、keyGenerator、cacheManager、cacheResolver.

一個類中可能會有多個快取操作,而這些快取操作可能是重複的。這個時候可以使用 @CacheConfig是一個類級別的註解.

簡單舉個例子吧:

image.png

我們發現在同個service類下,對不同方法新增的註解都要指定同一個快取元件我們可以在類頭上統一抽取快取元件,或者是快取名稱之類的~

大夥私下多試一試,就可以啦,很簡單的~

其實還有一些知識的,但是說難也不難,就沒有再說啦,大夥慢慢發掘吧~

注意事項

1)不建議快取分頁查詢的結果。

2)基於 proxyspring aop 帶來的內部呼叫問題

這個問題不僅僅是出現在這裡,其實只要牽扯到Spring AOP 切面的問題,都有這個問題,就像@Transactional(rollbackFor = Exception.class)註解一樣。

假設物件的方法是內部呼叫(即 this 引用)而不是外部引用,則會導致 proxy 失效,那麼切面就失效,也就是說 @Cacheable、@CachePut 和 @CacheEvict 都會失效。

解決方法:

  • 啟動類新增@EnableAspectJAutoProxy(exposeProxy = true),方法內使用AopContext.currentProxy()獲得代理類,使用事務。
  • @Autowired  private ApplicationContext applicationContext;  ​  // 在方法中手動獲取bean,再呼叫  applicationContext.getBean(xxxxServiceImpl.class);

3)@Cache註解的方法必須為 public

4)預設情況下,@CacheEvict標註的方法執行期間丟擲異常,則不會清空快取。

後記

不知道這篇文章有沒有幫助到你,希望看完的你,開開心心~


今天這個文章就寫到了這裡啦,我是 寧在春,一個寧願永遠活在有你的春天裡的那個人

如果你覺得有所收穫,就給我點點贊,點點關注吧~ 哈哈,希望收到來自你的正向反饋,下一篇文章再見。

yijiansanlian.webp

寫於 2022 年 10 月 23 日,作者:寧在春