SpringBoot快取管理(二) 整合Redis快取實現

blayn發表於2021-07-12

SpringBoot支援的快取元件

在SpringBoot中,資料的快取管理儲存依賴於Spring框架中cache相關的org.springframework.cache.Cache和org.springframework.cache.CacheManager快取管理器介面。

如果程式中沒有定義型別為CacheManager的Bean元件或者是名為cacheResolver的CacheResolver快取解析器,SpringBoot將嘗試選擇啟用以下快取元件(按照指定的順序):

(1)Generic

(2)JCache (JSR-107) (EhCache 3、Hazelcast、Infinispan等)

(3)EhCache 2.x

(4)Hazelcast

(5)Infinispan

(6)Couchbase

(7)Redis

(8)Caffeine

(9)Simple

上面按照SpringBoot快取元件的載入順序,列舉了SpringBoot支援的9種快取元件,在專案中新增某個快取管理元件(例如Redis)後,SpringBoot專案會選擇並啟用對應的快取管理器。如果在專案中同時新增了多個快取元件,且沒有指定快取管理器或者快取解析器(CacheManager或者cacheResolver),那麼SpringBoot會按照上述順序在新增的多個快取元件中優先啟用排在前面的某個快取元件進行快取管理(例如,同時新增了Couchbase和Redis這兩個快取元件,那麼優先啟用Couchbase元件)。

在上一篇文章 SpringBoot快取管理(一) 預設快取管理 介紹的預設快取管理中,我們搭建的專案沒有新增任何快取管理元件,但是依舊實現了快取管理。這是因為開啟快取管理後,SpringBoot會按照上述快取元件順序查詢有效的快取元件進行快取管理,如果沒有任何快取元件,會預設使用最後一個Simple快取元件進行管理。Simple快取元件是SpringBoot預設的快取管理元件,它預設使用記憶體中的ConcurrentMap進行快取儲存,所以在沒有新增任何第三方快取元件的情況下,依舊可以實現記憶體中的快取管理,但是不推薦這種快取管理方式

基於註解的Redis快取實現

在 SpringBoot快取管理(一) 預設快取管理 搭建的專案基礎上引入Redis快取元件,使用基於註解的方式講解SpringBoot整合Redis快取的具體實現。

(1)新增Spring Data Redis依賴啟動器

在pom.xml檔案中新增Spring Data Redis依賴啟動器:

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

當我們新增Redis相關的依賴啟動器後,SpringBoot會使用RedisCacheConfigratioin作為自動配置類進行快取相關的自動裝配類(之前為預設的SimpleCacheConfiguration),容器中使用的快取管理器變為了RedisCacheManager(之前為預設為cacheManager),這個快取管理器建立的Cache為RedisCache,進而操控Redis進行資料的快取。

(2)Redis伺服器連線配置

在專案的全域性配置檔案application.properties中新增Redis資料庫的連線配置,示例程式碼如下:

# Redis伺服器地址
spring.redis.host=127.0.0.1
# Redis伺服器連線埠
spring.redis.port=6379
# Redis伺服器連線密碼(預設為空)
spring.redis.password=

(3)對CommentService類中的方法進行修改

使用@Cacheable、@CachePut、@CacheEvict三個註解進行快取管理,分別進行快取儲存、快取更新及快取刪除等操作:

package com.hardy.springbootdatacache.service;

import com.hardy.springbootdatacache.entity.Comment;
import com.hardy.springbootdatacache.repository.CommentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Optional;

/**
 * @Author: HardyYao
 * @Date: 2021/6/19
 */
@Service
public class CommentService {

    @Autowired
    private CommentRepository commentRepository;

    /**
     * 根據評論id查詢評論
     * @Cacheable:將該方法的查詢結果comment存放在SpringBoot預設快取中
     * cacheNames:起一個快取名稱空間,對應快取唯一標識
     * @param id
     * @return
     */
    @Cacheable(cacheNames = "comment", unless = "#result==null")
    public Comment findCommentById(Integer id){
        Optional<Comment> comment = commentRepository.findById(id);
        if(comment.isPresent()){
            Comment comment1 = comment.get();
            return comment1;
        }
        return null;
    }

    /**
     * 更新評論
     * @param comment
     * @return
     */
    @CachePut(cacheNames = "comment",key = "#result.id")
    public Comment updateComment(Comment comment) {
        commentRepository.updateComment(comment.getAuthor(), comment.getaId());
        return comment;
    }

    /**
     * 刪除評論
     * @param comment_id
     */
    @CacheEvict(cacheNames = "comment")
    public void deleteComment(int comment_id) {
        commentRepository.deleteById(comment_id);
    }

}

在上述程式碼中,使用了@Cacheable、@CachePut、@CacheEvict註解在資料查詢、資料更新及資料刪除方法上進行了快取管理。

其中,查詢快取@Cacheable註解中沒有標記key值,將會使用預設引數值comment_id作為key進行資料儲存,在進行快取更新時必須使用同樣的的key;同樣,在使用查詢快取@Cacheable註解中,定義了 unless= "#result==null" 表示查詢結果為空則不進行快取。

(4)在CommentController類中新增兩個介面

新增更新和刪除的介面:

package com.hardy.springbootdatacache.controller;

import com.hardy.springbootdatacache.entity.Comment;
import com.hardy.springbootdatacache.service.CommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: HardyYao
 * @Date: 2021/6/19
 */
@RestController
public class CommentController {

    @Autowired
    private CommentService commentService;

    @RequestMapping(value = "/findCommentById")
    public Comment findCommentById(Integer id){
        Comment comment = commentService.findCommentById(id);
        return comment;
    }

    @RequestMapping(value = "/updateComment")
    public Comment updateComment(Comment comment){
        Comment oldComment = commentService.findCommentById(comment.getId());
        oldComment.setAuthor(comment.getAuthor());
        Comment comment1 = commentService.updateComment(oldComment);
        return comment1;
    }

    @RequestMapping(value = "/deleteComment")
    public void deleteComment(Integer id){
        commentService.deleteComment(id);
    }

}

(5)基於註解的Redis查詢快取測試

在瀏覽器中輸入:http://localhost:8080/findCommentById?id=1 進行訪問:

頁面報錯了,檢視控制檯資訊:

根據報錯資訊可知:查詢使用者評論資訊Comment時執行了相應的SQL語句,但是在進行快取儲存時出現了IllegalArgumentException非法引數異常,提示資訊要求對應的Comment實體類必須實現序列化(DefaultSerializer requires a Serializable payload but received an object of type [com.hardy.springbootdatacache.entity.Comment])。

(6)將快取物件實現序列化

(7)重啟專案測試查詢快取

在瀏覽器中輸入:http://localhost:8080/findCommentById?id=1 進行訪問(連續訪問三次):

開啟Redis客戶端視覺化工具Redis Desktop Manager,連線本地啟用的Redis服務,檢視具體的資料快取效果:

執行findById()方法查詢出的使用者評論資訊Comment正確儲存到了Redis快取庫中名為comment的名稱空間下。

其中快取資料的唯一標識key值是以“名稱空間comment::+引數值(comment::1)”的字串形式體現的,而value值則是經過JDK預設序列格式化後的HEX格式儲存。這種JDK預設序列格式化後的資料顯然不方便快取資料的視覺化檢視和管理,所以在實際開發中,通常會自定義資料的序列化格式,這方面的內容在後面會介紹。

(8)基於註解的Redis快取更新測試

先通過瀏覽器訪問:http://localhost:8080/updateComment?id=1&author=hardy;

接著在訪問:http://localhost:8080/findCommentById?id=1,檢視瀏覽器返回資訊及控制檯列印資訊:

可以看到,執行updateComment()更新id為1的資料時執行了一條更新的SQL語句,後續呼叫findById()方法查詢id為1的使用者評論資訊時沒有再次執行查詢的SQL語句,且瀏覽器返回了更新後的正確結果,這說明@CachePut快取更新配置成功。

(9)基於註解的Redis快取刪除測試

通過瀏覽器訪問:http://localhost:8080/deleteComment?id=1 和 http://localhost:8080/findCommentById?id=1

執行deleteComment()方法刪除id為1的資料後查詢結果為空,檢視Redis快取資料庫:

可以看到之前儲存的comment相關資料被刪除掉了,這表明@CacheEvict註解快取刪除成功實現。

 

通過上面的案例可以看出:使用基於註解的Redis快取實現只需要新增Redis依賴、並使用幾個註解在對應的方法上,就可以實現對資料的快取管理。

另外,還可以在SpringBoot全域性配置檔案中配置Redis有效期,示例程式碼如下:

# 對基於註解的Redis快取資料統一設定有效期為1分鐘,單位毫秒
spring.cache.redis.time-to-live=60000

上述程式碼中,在SpringBoot全域性配置檔案中新增了“spring.cache.redis.time-to-live”屬性統一設定Redis資料的有效期(單位為毫秒),但這種方式不夠靈活,因此一般不用。

基於API的Redis快取實現

在SpringBoot整合Redis快取實現中,除了基於註解形式的Redis快取形式外,還有一種開發中更常用的方式——基於API的Redis快取實現。這種基於API的Redis快取實現,需要在某種業務需求下通過Redis提供的API呼叫相關方法實現資料快取管理。同時,這種方法還可以手動管理快取的有效期。

下面,通過Redis API的方式講解SpringBoot整合Redis快取的具體實現。

(1)使用Redis API進行業務資料快取管理

在 com.hardy.springbootdatacache.service 包下新建一個 ApiCommentService:

package com.hardy.springbootdatacache.service;

import com.hardy.springbootdatacache.entity.Comment;
import com.hardy.springbootdatacache.repository.CommentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * @Author: HardyYao
 * @Date: 2021/6/19
 */
@Service
public class ApiCommentService {

    @Autowired
    private CommentRepository commentRepository;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根據評論id查詢評論
     * @param id
     * @return
     */
    public Comment findCommentById(Integer id){
        // 先查Redis快取
        Object o = redisTemplate.opsForValue().get("comment_" + id);
        if (o != null) {
            return (Comment) o;
        } else {
            // 如果快取中沒有,則從資料庫查詢
            Optional<Comment> dbComment = commentRepository.findById(id);
            if (dbComment.isPresent()) {
                Comment redisComment = dbComment.get();
                // 將查詢結果儲存到快取中,並設定有效期為1天
                redisTemplate.opsForValue().set("comment_"+id, redisComment,1, TimeUnit.DAYS);
                return redisComment;
            } else {
                return null;
            }
        }

    }

    /**
     * 更新評論
     * @param comment
     * @return
     */
    public Comment updateComment(Comment comment) {
        commentRepository.updateComment(comment.getAuthor(), comment.getId());
        // 更新資料庫資料後進行快取更新
        redisTemplate.opsForValue().set("comment_" + comment.getId(), comment);
        return comment;
    }

    /**
     * 刪除評論
     * @param comment_id
     */
    public void deleteComment(int comment_id) {
        commentRepository.deleteById(comment_id);
        // 刪除資料庫資料後進行快取刪除
        redisTemplate.delete("comment_" + comment_id);
    }

}

(2)編寫Web訪問層ApiCommentController

package com.hardy.springbootdatacache.controller;

import com.hardy.springbootdatacache.entity.Comment;
import com.hardy.springbootdatacache.service.ApiCommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: HardyYao
 * @Date: 2021/6/19
 */
@RestController
@RequestMapping("api")  // 改變請求路徑
public class ApiCommentController {

    @Autowired
    private ApiCommentService apiCommentService;

    @RequestMapping(value = "/findCommentById")
    public Comment findCommentById(Integer id){
        Comment comment = apiCommentService.findCommentById(id);
        return comment;
    }

    @RequestMapping(value = "/updateComment")
    public Comment updateComment(Comment comment){
        Comment oldComment = apiCommentService.findCommentById(comment.getId());
        oldComment.setAuthor(comment.getAuthor());
        Comment comment1 = apiCommentService.updateComment(oldComment);
        return comment1;
    }

    @RequestMapping(value = "/deleteComment")
    public void deleteComment(Integer id){
        apiCommentService.deleteComment(id);
    }

}

(3)測試基於API的Redis快取實現

輸入:http://localhost:8080/api/findCommentById?id=2(連續輸入三次)、http://localhost:8080/api/updateComment?id=2&author=hardy、http://localhost:8080/deleteComment?id=2進行訪問:

檢視控制檯訊息及Redis資料庫:

 

基於API的Redis快取實現的相關配置:基於API的Redis快取實現不需要@EnableCaching註解開啟基於註解的快取支援,所以這裡可以選擇將新增在專案啟動類上的@EnableCaching註解進行刪除或者註釋,不會影響專案的功能實現。

相關文章