https://www.pexels.com/zh-tw/photo/46274/
安裝 Redis 後,可透過命令列工具(Command Line Interface,CLI)直接操作。然而實際進行後端開發時,會使用專門的函式庫(library)來存取它。
本文會以 Spring Boot 後端專案為基礎,在程式中使用 RedisTemplate 存取 Redis。文末也會附上完成後的程式專案供讀者參考。
本文的練習用專案:
https://github.com/ntub46010/SpringBootUsingRedis/tree/master
一、範例專案介紹
本文的範例是以「取得文章資料」為情境。透過 REST API 取得一篇文章(post)時,後端除了回傳文章本身的資料,還會包含「建立者」與「按讚者」的人名。
但筆者設計成文章的建立者人名與按讚者人名,在資料庫(DB)中並沒有和文章儲存在同一個結構(schema)。因此後端回傳文章時,還得從其他的資料表(table)取得需要的資料。
本文想做到的是建立一套快取機制,避免每次都需要在如 MySQL 之類的 DB 查詢人名。
(一)資料結構
以下的 PostPO 類別代表資料庫中的文章資料,有包含建立者的 id。提供 3 筆測試資料。而 PostVO 類別,則代表後端要回傳的 response body。
UserPO 類別代表使用者資料,包含 id 與人名。提供 3 筆測試資料。
LikePO 類別代表按讚資料,包含按讚者與文章的 id。提供 2 筆測試資料。
(二)REST API
以下設計了 controller,提供取得或刪除文章、新增與刪除讚的 API。
二、導入 Redis
現在讓我們把 Redis 導入到 Spring Boot 的專案中。
(一)匯入函式庫
請在 pom 檔透過添加依賴的方式,匯入 Spring 提供的 Redis 的 library。並且再匯入 apache 提供的用來處理連線池的 library,否則會出現「ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig」的錯誤。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.11.1</version> </dependency>
附帶一提,Redis 常見的 Java client 有 Jedis 與 Lettuce 等。前面匯入的「spring-boot-starter-data-redis」採用的是後者。
(二)配置連線設定
接下來可參考「【Redis】基本介紹與在 Windows Docker 上安裝」,在電腦上啟動 Redis 的服務。另外微軟也有提供 Windows 的安裝檔(版本 3.0.504),就不必透過 Docker。讀者亦可考慮選用。
回到 Spring Boot 專案,請在 application.properties 檔中,提供連線 Redis 服務的參數。
其中「spring.redis.database」的用途相當於 SELECT 指令,是用來切換資料庫的。此處選擇索引為 0 的資料庫。
(三)配置 Redis Template
在程式中存取 Redis,會透過 RedisTemplate 類別的物件來執行。請新增一個 Configuration 類別,並在裡面配置該元件。
RedisTemplate 接受兩個泛型類別,分別是 key 和 value 的型態。若兩者都是 String,我們還可直接選用 StringRedisTemplate。
此處建立一個 StringRedisSerializer,它是 RedisSerializer 介面的物件。將該介面的物件賦予給 RedisTemplate,是為了指定 key 或 value 存到 Redis 的序列化方式。雖然不指定也行,但進入 Redis 的 CLI 查看,我們就會看到一些可讀性差的亂碼,不利於 debug。
最後在 Spring 的啟動類別冠上 @EnableCaching 的標記,啟用快取功能,Redis 的配置就完成了。
三、使用 Redis Template
RedisTemplate 提供許多方法來讀寫資料,其用法與原生語法相當接近。在本節,筆者設計了一個叫做 RedisService 的元件,用來封裝對 RedisTemplate 的操作。
這個 RedisService 會讓其他業務邏輯呼叫,用來實現快取機制。也就是先確認 Redis 中是否有想要的資料,有的話則直接取用。否則就用預設的方式來查詢,並將結果儲存。
所謂「預設的方式」,是指將原本取得資料的程式碼包裝成 Supplier 物件,在呼叫 RedisService 的方法時一併傳入。以下是假想的虛擬碼:
public String getUserName(String userId) { var cacheKey = "userName_" + userId; Supplier<String> supplier = () -> userRepository.findOneById(id).getName(); return redisService.getString(cacheKey, supplier); }
(一)操作字串
以下是取得字串的實作。它接收了兩個參數,分別是 Redis 資料的 key,和預設取得資料的方式。
若要進行字串的操作,需先呼叫 opsForValue 方法,得到 ValueOperations 物件,接著再呼叫操作方法。例如取得字串時會使用 get 方法,儲存則使用 set 方法。分別對應到原生語法中的 GET 與 SET 指令。
讀者仔細閱讀範例程式便可看出,會優先採用 Redis 的資料。若沒有,才執行 Supplier 物件中所包含的程式碼來取得。
(二)操作 List 結構
以下是取得字串 List 的實作。若想取得指定索引範圍的元素,會使用 range 方法,並傳入起始與結尾的索引,對應到 LRANGE 指令。另外要補充的是,參數「-1」代表最後一個元素。而快取資料不存在時,會回傳空 List,而不是 null。。
以下是從 List 移除元素的實作。會使用 remove 方法,並傳入想移除的值與數量。數量的正負分別代表從 List 的左邊或右邊移除,而「0」代表全部。對應到 LREM 指令。
以下是將一個元素放入 List 的實作。以直覺來說,會覺得使用 rightPush 方法,從 List 的右邊放入就好。其對應到 RPUSH 指令。但新增元素時,需留意資料不同步的問題。
我們先假定某個查詢,若經由 DB 會取得 A、B、C 三筆資料,而 Redis 一開始還沒有對應的 List。接著後端收到一個會導致新增 List 元素的請求,且在 Redis 存入 D。此時執行同樣的查詢想取得 List,在「優先採用快取資料」的前提下,RedisService 經由 Redis 只會取得 D,使得回傳值意外地漏掉 A、B、C。
可能的解法是,在 Redis 還不存在這個 List 時,就不做放入元素的動作。因此筆者選 用 rightPushIfPresent,它對應到 RPUSHX 指令。直到有其他情境需要取得 List,那就在 DB 查詢結束後另外存入 Redis,後續便能安心地放入新元素。
(三)刪除快取
以下是刪除整筆快取資料的實作,會使用 delete 方法,它對應到 DEL 指令。該方法也支援傳入 Collection,方便一次刪除多筆。
四、在業務邏輯中使用快取
在上一節完成 RedisTemplate 邏輯的封裝後,本節會將 RedisService 注入到業務邏輯的 service 中。由於本文範例專案並沒有串接其他資料庫,作為代替,下面會看到 service 用全域的 Map 或 Set 來儲存測試資料。
(一)Post Service
以下的 PostService 提供兩個方法,分別是取得與刪除文章。
取得文章時,會從 postDB 這個 Map 取得文章資料。接著再從 UserService 與 LikeService 分別獲取建立者人名與按讚者人名。這兩項資料都有引進快取。
刪除文章時,除了從 postDB 刪除,也透過 LikeService 刪除按讚資料,包含按讚者人名清單的快取。
(二)User Service
以下的 UserService 僅提供一個方法,用來取得使用者人名。
原定的做法是從 userDB 這個 Map 中拿到資料,再取得人名。此處將這段過程包裝成 Supplier 物件。而為了優先採用快取資料,會連同 Redis 資料的 key 一同傳入 RedisService。
下圖是在取得 id 為 P1 的文章後,在 Redis 中找到建立者(U1)與按讚者(U2)的人名,以及 P1 的按讚者清單。
(三)Like Service
以下的 LikeService 提供四個方法。分別是建立與刪除按讚資料、取得某文章的按讚者人名清單,以及刪除某文章的按讚資料。
建立與刪除按讚資料時,是對 Redis 的 List 結構進行新增與移除元素,其值為按讚者的人名。
取得所有按讚者人名時,會先從 likeDB 這個 Set 中收集按讚者 id,再透過前面的 UserService 取得人名。
接續前面,下圖是使用者 U1 與 U3 對 文章 P1 按讚後,在 Redis 中找到 U3 的人名和 P1 的按讚者清單。
刪除指定文章的按讚資料時,除了從 likeDB 刪除,也從 Redis 刪除快取。
本文的完成專案:
https://github.com/ntub46010/SpringBootUsingRedis/tree/part1-implement-cache-by-redis-template
留言
張貼留言