當你要儲存的資料含有兩個以上的欄位時,使用純字串會迫使你做出一個尷尬的選擇:將整個物件序列化成 JSON,並在每次變更時都重新寫入;或是將各欄位分散到許多獨立的鍵。雜湊(Hash)正是適合儲存物件的資料結構。它把欄位-值配對存放在單一鍵下,讓你讀取或更新個別欄位,並將相關資料分組存放,這正是你在使用者記錄、工作階段或快取資料列時所需要的。

這是 Redis 大師班系列的第 4 篇,接續前一篇字串與計數器

基本操作

雜湊是一個在單一鍵下存放欄位對應值的對應表:

HSET user:1 name "Aman" plan "pro" logins "42"
HGET user:1 plan          # "pro"
HGETALL user:1            # 所有欄位與值
HMGET user:1 name plan    # 一次讀取多個欄位
HDEL user:1 logins        # 刪除一個欄位

Enter fullscreen mode Exit fullscreen mode

相較於序列化的 JSON 字串,最大的優勢在於能直接存取個別欄位。讀取單一欄位只需 HGET,而不用取得並解析整個物件;更新單一欄位只需 HSET,而不用讀取-修改-寫回整個資料區塊。對於大型物件來說,這不僅更快,也在並行環境下更安全,因為兩個客戶端同時更新不同欄位時,不會互相覆蓋彼此的變更。

為什麼不直接把 JSON 存成字串?

這是開發者常面臨的抉擇,因此值得釐清。把物件存成 JSON 字串既簡單又適用,只要你總是整筆讀取或寫入,例如需要整筆替換的快取項目即可。但在以下情況下,雜湊會是更好的選擇:

  • 經常讀取或更新單一欄位時。使用 HGET user:1 plan 比抓取整個 JSON 再解析單一欄位更快。
  • 多個客戶端同時更新不同欄位時。使用 JSON 時,兩個寫入者各自讀取-修改-寫回,整個物件可能遺失其中一方的變更;使用雜湊時,HSET 不同欄位彼此獨立且安全。
  • 需要對數值欄位做遞增時。使用 HINCRBY 可對單一欄位進行原子遞增,而 JSON 字串無法避免讀取-修改-寫回的競態。

原則是:若只需整筆物件存取,可使用 JSON 字串;若需欄位層級存取或並行欄位更新,請使用雜湊。

原子欄位操作

與字串相同,雜湊也提供原子數值操作,但範圍限定在單一欄位:

HINCRBY user:1 logins 1          # 對 logins 欄位原子 +1
HINCRBY cart:99 item:42 2        # 將 item:42 的數量 +2
HINCRBYFLOAT account:1 balance 9.99

Enter fullscreen mode Exit fullscreen mode

這對於內含計數器的物件非常實用。將購物車存成雜湊(每項商品一個欄位,值為數量),即可用 HINCRBY 原子地調整單一商品數量,而不影響其他欄位。每位使用者的統計物件也可獨立且安全地遞增 loginsposts。雜湊同時提供物件分組與原子欄位計數器的能力。

記憶體效率

雜湊在規模化時有一項優點:對於小型雜湊,Redis 使用緊湊的記憶體編碼(listpack),比把每個欄位存成獨立頂層鍵更省空間。因此,將物件的欄位分組存入單一雜湊,除了邏輯上更整齊,也比相同資料分散成多個鍵更節省記憶體。

這也是為何常見建議是:儲存大量小型物件時,應使用雜湊而非每個欄位各用一個鍵。一百萬筆使用者記錄以雜湊儲存,會比把相同資料分散成一百萬個獨立字串鍵更省記憶體,且更容易管理(每位使用者只需一個鍵,使用單一 DEL 即可移除整位使用者)。

過期:唯一的限制

有一點需要注意。歷史上,Redis 的過期(TTL)是針對整個鍵,而非個別雜湊欄位。你可以讓整個 user:1 雜湊過期,但無法只讓其 logins 欄位過期。這表示過去需要其他方式或不同結構來實現欄位層級過期。

Redis 7.4 及更新版本新增 HEXPIRE,可對個別雜湊欄位設定 TTL,移除此歷史限制:

HEXPIRE user:1 3600 FIELDS 1 session_token   # 讓單一欄位在 1 小時後過期

Enter fullscreen mode Exit fullscreen mode

如果你使用較舊版本,請記得 TTL 僅作用於鍵層級:若需欄位層級過期,需將這些欄位存成獨立鍵,或升級至支援 HEXPIRE 的版本。對於大多數物件儲存用途,通常會讓整個雜湊過期(例如整個工作階段或快取記錄),因此此限制較少影響。

何時應使用雜湊

當你儲存的物件需要獨立存取或更新其欄位時,請使用雜湊:

  • 使用者記錄與個人檔案:欄位會逐一讀取與更新。
  • 工作階段:具多個屬性的工作階段物件,以整筆方式過期。
  • 快取資料庫資料列:將資料列的各欄位作為雜湊欄位,可個別更新。
  • 內含計數器的物件:購物車、實體統計資料等需要原子遞增的數值欄位。

若你只會整筆讀取與寫入物件,且單一序列化資料區塊的簡潔性優於欄位層級存取,則應使用 JSON 字串。

雜湊是 Redis 在不犧牲欄位層級存取或原子欄位更新的情況下,儲存結構化物件的方式。它將相關資料分組存放在單一鍵下、為小型物件節省記憶體,並允許並行寫入者安全地修改不同欄位。當你的值是一個物件而非單一資料時,雜湊幾乎永遠是正確的選擇。

下一篇將介紹列表:支援佇列與近期項目摘要的有序序列,以及將其轉換為簡單工作佇列的阻塞操作。

重點摘要

  • 雜湊在單一鍵下存放欄位-值配對,提供對個別欄位的直接存取,而非整筆序列化資料。
  • 當你需要個別存取欄位,或多個客戶端同時更新不同欄位時,請優先使用雜湊而非 JSON 字串。
  • HINCRBY 可原子地遞增單一欄位,非常適合內含計數器的物件,例如購物車與實體統計。
  • 小型雜湊使用緊湊的記憶體編碼,因此將欄位分組存放比使用多個獨立鍵更節省空間。
  • TTL 歷史上僅作用於鍵層級;Redis 7.4+ 新增 HEXPIRE 支援欄位層級過期,但大多數情況仍會讓整個雜湊過期。

常見問題

何時應使用 Redis 雜湊而非 JSON 字串?

當你經常讀取或更新個別欄位,或多個客戶端同時更新不同欄位時,請使用雜湊,因為雜湊允許獨立的欄位存取與更新。若你總是整筆讀取與寫入物件,則使用 JSON 字串。

我可以在 Redis 雜湊中遞增單一欄位嗎?

可以,使用 HINCRBY(或 HINCRBYFLOAT),它會原子地對單一欄位的數值進行加總,而不影響其他欄位。這非常適合內含計數器的物件,例如購物車中各商品的數量。

雜湊比獨立鍵更省記憶體嗎?

對於許多小型物件來說,是的。Redis 會將小型雜湊以緊湊方式編碼,因此將物件的欄位分組存入單一雜湊,會比把每個欄位存成獨立頂層鍵更省記憶體,且資料在邏輯上也更整齊。

我可以對單一雜湊欄位設定過期嗎?

歷史上不行,TTL 僅作用於整個鍵。Redis 7.4 及更新版本新增 HEXPIRE 支援欄位層級 TTL。若使用較舊版本,需讓整個雜湊過期,或將需要過期的欄位存成獨立鍵。

如何在 Redis 中儲存使用者物件?

使用鍵如 user:1 的雜湊,並以各屬性(nameplanlogins)作為欄位。你可以直接讀取或更新任何欄位,並原子地遞增數值欄位,且只需單一 DEL 即可刪除整位使用者。

延伸閱讀


本文最初發表於 amanksingh.com/blog/redis-hashes

關於作者

Aman Kumar Singh 是印度諾伊達的團隊負責人兼高級軟體工程師,專長於使用 TypeScript、Next.js、NestJS、PostgreSQL 與 Redis 撰寫全端工程、系統設計與生產環境 SaaS。