Go-redis:執行 Lua 腳本

go-redis (github.com/redis/go-redis) 支持 Lua 腳本 redis.Script,本文在這裏簡單展示其在秒殺場景中使用的代碼片段。

秒殺場景

在秒殺場景中,一個商品的庫存對應了兩個信息,分別是總庫存量和已秒殺量。可以使用一個 Hash 類型的鍵值對來保存庫存的這兩個信息,如下所示:

key: product_id
value: {total: N, ordered: M}

其中,product_id 是商品的編號,total 是總庫存量,ordered 是已秒殺量。

因爲庫存查驗庫存扣減這兩個操作要保證一起執行,一個直接的方法就是使用 Redis 的原子操作。

原子操作可以是 Redis 自身提供的原子命令,也可以是 Lua 腳本。因爲庫存查驗和庫存扣減是兩個操作,無法用一條命令來完成,所以需要使用 Lua 腳本原子性地執行這兩個操作

package main

import (
 "context"
 "fmt"
 "github.com/redis/go-redis/v9"
)

var stockHIncrBy = redis.NewScript(`
  local key = KEYS[1]
  local change = ARGV[1]

  local values = redis.call("HMGET", key, "total", "ordered")

  local total = tonumber(values[1])
  local ordered = tonumber(values[2])

  if( ordered + change <= total ) then
   redis.call("HINCRBY", key, "ordered", change)
   return change
  end
  return 0
 `)

func main() {
 rdb := redis.NewClient(&redis.Options{
  Addr:     "localhost:6379",
  Password: "",
  DB:       0,
 })

 key := "product_id:10001"                                    // id 爲 10001 的商品 key
 value := map[string]interface{}{"total": 100, "ordered": 10} // 插入數據 total 總庫存量 ordered 已秒殺量

 ctx := context.Background()
 rdb.HMSet(ctx, key, value) // 設置初始庫存

 change := 20 // 庫存扣減數量

 keys := []string{key}
 values := []interface{}{change}

 for i := 0; i < 5; i++ { // 嘗試扣減庫存 5 次
  result, err := stockHIncrBy.Run(ctx, rdb, keys, values...).Int()
  if err != nil {
   // ...
  }
  fmt.Println(result) // 如果返回值是當前請求的庫存量,就是成功;如果是 0,就是失敗。
 }
}

Lua 腳本的邏輯如下:

  1. 獲取商品的 total(總庫存量) 和 ordered(已秒殺量) 值。

  2. 檢查 ordered(已秒殺量) + change(庫存扣減量) 是否小於等於 total(總庫存量)。

如果當前請求的庫存扣減量加上已被秒殺的庫存量仍然小於總庫存量,就可以更新已秒殺的庫存量。根據腳本的返回值,來確定秒殺是成功還是失敗了。如果返回值是當前請求的庫存量,就是成功了;如果是 0,就是失敗。


References
https://redis.uptrace.dev/zh/guide/lua-scripting.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/hzQShIAUFHblc1UWnRcfRw