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 脚本原子性地执行这两个操作。
go
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 脚本的逻辑如下:
- 获取商品的 total(总库存量) 和 ordered(已秒杀量) 值。
- 检查 ordered(已秒杀量) + change(库存扣减量) 是否小于等于 total(总库存量)。
- 是:执行 HINCRBY 更新 ordered,返回成功的数量。
- 否:返回 0,表示库存不足,秒杀失败。
如果当前请求的库存扣减量加上已被秒杀的库存量仍然小于总库存量,就可以更新已秒杀的库存量。根据脚本的返回值,来确定秒杀是成功还是失败了。如果返回值是当前请求的库存量,就是成功了;如果是 0,就是失败。
References
redis.uptrace.dev/zh/guide/lu...
推荐阅读