一個使用者對到多個「商品 + 數量」,最常做的動作是「某一項 +1」。
按照 Day 4 的做法,整包存成 JSON:
SET cart:01 '{"product:01":1,"product:02":3}'
這樣要把某個商品數量 +1,得做五件事:GET 整包 → 反序列化 → 改 → 序列化 → SET 回去。
只是想改一個數字,卻要把整包讀出來再寫回去。而且 GET 完到 SET 回去中間,別人也在改同一個 key。
Hash 是 key 底下再掛一層 field-value,剛好對上購物車的結構。
HSET key field value(可以一次設定多組)
HGET key field
HGETALL key(整包撈出來)
HINCRBY key field num(指定 field 加減數值,原子)
HDEL key field(1 刪除成功,0 field 不存在)
HLEN key(有幾個 field)
HEXISTS key field(1 存在,0 不存在)
Hash 原本不能對單一 field 設過期時間,只能整個 key 一起過期。Redis 7.4 之後多了 HEXPIRE 可以指定 field,但版本比 7.4 舊就沒有這個指令。
開頭的五個步驟,可以變成一行:
HINCRBY cart:01 product:01 1
Java 是用 HashOperations:
private final HashOperations<String, String, String> hash;
public CartController(StringRedisTemplate redis) {
this.hash = redis.opsForHash(); // 從 StringRedisTemplate 拿就好
}
/** 加入商品:已經在車上就累加數量 */
@PostMapping("/{userId}/items")
public Long add(@PathVariable Long userId, @RequestParam String productId, @RequestParam long qty) {
return hash.increment("cart:" + userId, productId, qty);
}
/** 直接指定數量(HSET 是覆蓋,不是累加) */
@PostMapping("/{userId}/items/{productId}")
public Map<String, Object> update(@PathVariable Long userId, @PathVariable String productId, @RequestParam long qty) {
hash.put("cart:" + userId, productId, String.valueOf(qty));
return Map.of("ok", true);
}
@DeleteMapping("/{userId}/items/{productId}")
public Map<String, Object> remove(@PathVariable Long userId, @PathVariable String productId) {
return Map.of("removed", hash.delete("cart:" + userId, productId));
}
/** 整包撈出來,順便回傳件數 */
@GetMapping("/{userId}")
public Map<String, Object> all(@PathVariable Long userId) {
return Map.of("items", hash.entries("cart:" + userId), "size", hash.size("cart:" + userId));
}
Day 4 的計數器少加了 47 次,這裡的差距更誇張。
兩台購物車同時放同一件商品,一邊走整包 JSON、一邊走 HINCRBY:
// String 版:讀整包 → 改 → 寫回
String json = redis.opsForValue().get(JSON_KEY);
Map<String, Integer> cart = mapper.readValue(json, CART);
cart.merge(ITEM, 1, Integer::sum);
redis.opsForValue().set(JSON_KEY, mapper.writeValueAsString(cart));
// Hash 版
hash.increment(HASH_KEY, ITEM, 1);
k6 一樣開 50 個虛擬使用者,兩邊各加 5000 次會得到:
Hash 準準的 5000,JSON 只剩 381。
差距比 Day 4 大這麼多,是因為整包讀完到寫回中間多了一趟網路來回。這段時間裡另外 49 個人也在讀同一包 JSON,大家都拿到舊的數字,寫回去的時候互相蓋掉。
整包讀、整包寫、很少單獨改其中一個欄位的時候,像 Day 4 的商品詳情就很適合。反過來只要會「只改其中一項」,就換 Hash。
相關範例程式碼可以參考 https://github.com/gary880306/redis-30days/tree/dev
Hash 解決了「只改其中一項」,但它不管順序。明天的「最近瀏覽的 10 件商品」要照時間排,而且只留 10 筆,繼續來看 List 怎麼做![]()