iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
自我挑戰組

從購物車到秒殺:30 天 Redis 高併發自學筆記系列 第 5

Day 5|購物車:String 還是 Hash?

  • 分享至 

  • xImage
  •  

購物車是存什麼

一個使用者對到多個「商品 + 數量」,最常做的動作是「某一項 +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(可以一次設定多組)
    https://ithelp.ithome.com.tw/upload/images/20260917/2018420934wmMHy6I2.png

  • HGET key field
    https://ithelp.ithome.com.tw/upload/images/20260917/20184209oQvrli9MT5.png

  • HGETALL key(整包撈出來)
    https://ithelp.ithome.com.tw/upload/images/20260917/20184209ELlwHgdfD1.png

  • HINCRBY key field num(指定 field 加減數值,原子)
    https://ithelp.ithome.com.tw/upload/images/20260917/20184209bjSlsssSxx.png

  • HDEL key field(1 刪除成功,0 field 不存在)
    https://ithelp.ithome.com.tw/upload/images/20260917/20184209BpTy8umk5G.png

  • HLEN key(有幾個 field)
    https://ithelp.ithome.com.tw/upload/images/20260917/20184209iEtiDRLYpT.png

  • HEXISTS key field(1 存在,0 不存在)
    https://ithelp.ithome.com.tw/upload/images/20260917/20184209Znh43n03aS.png

Hash 原本不能對單一 field 設過期時間,只能整個 key 一起過期。Redis 7.4 之後多了 HEXPIRE 可以指定 field,但版本比 7.4 舊就沒有這個指令。

換成 Hash

開頭的五個步驟,可以變成一行:

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 次會得到:
https://ithelp.ithome.com.tw/upload/images/20260917/20184209DM9xe2d7Di.png
Hash 準準的 5000,JSON 只剩 381。

差距比 Day 4 大這麼多,是因為整包讀完到寫回中間多了一趟網路來回。這段時間裡另外 49 個人也在讀同一包 JSON,大家都拿到舊的數字,寫回去的時候互相蓋掉。

那 String 什麼時候比較好

整包讀、整包寫、很少單獨改其中一個欄位的時候,像 Day 4 的商品詳情就很適合。反過來只要會「只改其中一項」,就換 Hash。

相關範例程式碼可以參考 https://github.com/gary880306/redis-30days/tree/dev

明天

Hash 解決了「只改其中一項」,但它不管順序。明天的「最近瀏覽的 10 件商品」要照時間排,而且只留 10 筆,繼續來看 List 怎麼做/images/emoticon/emoticon31.gif


上一篇
Day 4|String
系列文
從購物車到秒殺:30 天 Redis 高併發自學筆記5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言