DB 數百萬筆的龐大資料如何提供給 Client & 儲存進 Redis cache 層的設計案例分享
前言
我遇到一個需求,需要將某張 Table 的所有資料提供 API 給客戶
but!!那張 Table 的資料量高達 100 萬筆,考量到資料庫查詢效能,不能對 Table 直接 Select All,並且考量到 API 的 response time 不能太久 & response size 不能太龐大, API 的 Client 端也無法一次接收,我必須研究其他可行的方式來傳遞這些資料
分批存取 Database
由於不能對 Table 直接 Select 100 萬筆,所以勢必要分批提供,這裡我將 Timestamp 的欄位轉 BIGINT 來供 filter 使用,並限制他每次 Select 的數量
每次用 Timestamp 的 int 數值,進 DB 搜尋後面 1,000 筆的資料
DB Select example:
SELECT TOP (1000)
teamId,
teamName,
teamTimestamp,
CONVERT(BIGINT, teamTimestamp) AS intTimestamp,
FROM team WITH (NOLOCK)
WHERE teamTimestamp >= CAST (@VersionKey AS timestamp)
ORDER BY timestamp
Timestamp 的資料型態是二進位的格式,當該項資料後續一旦有任何一個內容有異動,Timestamp 就會更新
如果有新增的項目,也會往後遞增 Timestamp
所以當我用上一次 Select 的最後一筆 Timestamp + 1 去往後搜,即可取得 DB 後續有 add / update 的項目
需要為 Database 墊 cache 層
現在,資料請求的設計是:
請求端呼叫多次請求,每次取 1,000 筆,直到 count 為 0 停止請求
至此,就已經可以做到分批提供 100 萬筆資料給 client 了
but …
由於資料是透過 API 提供出去,為了避免 Client 端頻繁發送大量請求會對 DB 造成負擔,我需要在 Service 和 DB 之間加一層 Redis Cache,以確保查詢效率與系統穩定性。(不然會被 DBA 罵 😭)
這意味著我必須將這百萬筆資料存入 Redis,並且同時支援 Client 端使用 versionKey 進行分批查詢。
研究 Redis 資料儲存方式
對於以往只在 Redis 存過簡單 string 資料的我來說,如何高效地儲存、查詢與維護這批大規模數據是一個很大的挑戰。😟
為此我跑去鑽研 Redis 的各種數據結構類型:
String |
|
Set |
|
List |
|
Hash |
|
Sorted Set |
|
Json |
|
Json 在基本版 Redis 不可用,必須安裝 Redis Stack 或 Redis Enterprise,才可用 Redis Module 的 RedisJSON
了解各數據結構的功能後,發現只有 Sorted Set 才有辦法讓我做到 (1) 排序 (2) range 查詢,故決定使用 Sorted Set 來當作這次需求的 [篩選用] 儲存結構。
再來是資料 body 本身的其他欄位內容,我比較了 String / Hash / Json 三種數據儲存結構
Json 結構可以解決 String 無法存取部分 field 的問題,也可以達到 Hash 做不到的多層結構,在我這少量 & 固定 field 數量的情境下很適合使用,並且在考量到需求後續仍有可能調整內容的現有工作條件下,為了保留資料異動上的彈性,我最終選擇使用 Json 作為我的 [Detail內容用] 儲存結構。
在少量的 field 下,Json 與 Hash 的存取性能相近,若是扁平一層的 Json 資料,使用 Json 或 Hash 都可以,不過 RedisJSON 會隨著 field 數量 / Json 嵌套層級越多而性能越差,使用上要多注意~
上面提到的 “資料異動上的彈性”,是指像是需要更動 field name 的話,Json 就可以在 set key 的時候整個 Json 直接覆蓋掉,Hash 就必須要先刪掉 old field 再新增 new field,比較麻煩就是了~~
Job by Version key 取 Database 資料存進 Redis
這個同步的動作我放到 job 來定期處理,將 Database 中的新資料 (or 新異動的舊資料) 取出,改存放到 Redis 中
由於需要記住我上次最後一筆是同步到哪一筆資料,會在 Redis 存一個 lastVersionKey (也就是前面提到的轉成 BIGINT 的 Timestamp)
每次 Job 處理時去 Redis 取得 lastVersionKey,去 DB 往後取 1,000 筆 Data 後存到 Redis
Sorted Set:
Member | Score |
teamId | BIGINT 的 Timestamp |
Json:
Key | Value |
teamId | 其他 field |
API by Version key 取 range 資料
當 client 發送 request 帶 versionkey 進來時,就會改到 Redis 中的 sorted set 取 range (551939612 往後取 1,000 筆)
從 sorted set 中取得 1,000 筆項目後,再根據存放在 Member 中的 teamId
,去各別 by Key Get 1,000 筆 Json 項目,回傳給 client,收工~~~
以上是我在此次需求情境中的應用方式,給大家參考,如果有人有其他不同做法也歡迎交流交流~~~ 😄