抢红包功能
抢红包功能是一个比较重要的功能,在分布式环境中,需要考虑使用redis或etcd等缓存中间件来实现
这里使用redis
搭配golang
要实现
基础思想
1.使用redis中的set
实现
2.在lua脚本中处理保证一致性
3.每抢一个红包就从set中捞取一个值
4.最终一致性由关系数据库保证
5.防止重复从关系数据库加载未抢数据,在抢完后把set删除添加同名string值
向redis设置未抢红包数据
返回
-1
表示已经存在
返回0
表未设置失败
返回大于零
表示设置成功(实则是成功添加元素的个数)
if redis.call('exists',KEYS[1]) == 1 then
return -1 -- 存在时不进行设置
end
return redis.call('sadd',KEYS[1],unpack(ARGV))
设置红包已经抢完
OK
表示设置成功
NotTakeOut
表示红包未抢完设置失败
提示:
在set为空时redis会自动删除这个set对应的key
local keyType = redis.call('type',KEYS[1]) -- key类型
if keyType['ok'] == "none" then
redis.call('setex',KEYS[1],120,"noItem")
return 'OK'
elseif keyType['ok'] == "set" then
return 'NotTakeOut'
else
return "OK"
end
抢一个红包
noKey
红包数据尚未加载到redis
takeOut
红包已经抢完
其它返回
成功抢到一个红包,这个返回是预先存的红包数据
local keyType = redis.call('type',KEYS[1]) -- key类型
if keyType['ok'] == "none" then
return 'noKey' -- key 不存在
elseif keyType['ok'] == "set" then
local take = redis.call('spop', KEYS[1]) -- 一定有值的
if redis.call('exists',KEYS[1]) == 0 then -- 如果被抢完了
redis.call('setex',KEYS[1],120,"noItem") -- 则设置为noItem
end
return take
else
return "takeOut" -- 红包抢完了
end
go代码
package red_packet
import (
"context"
_ "embed"
"errors"
redis2 "github.com/go-redis/redis/v8"
"github.com/samber/lo"
"example.com/common/errs"
"example.com/common/util/encodeutil"
)
// 抢红包 抽奖
// 字符串 列表
// language=lua 抢一个红包
// return no_key end [id]
//
//go:embed lua/takeOneRedPacketCmd.lua
var takeOneRedPacketCmd string
// language=lua 红包抢完了 设置key为字符串类型
// return 1 0
//
//go:embed lua/setUnTakeRedPacketListEmptyCmd.lua
var setUnTakeRedPacketListEmptyCmd string
// language=lua 设置未抢的红包
// return [num of item to add]
//
//go:embed lua/setUnTakeRedPacketListCmd.lua
var setUnTakeRedPacketListCmd string
// RedPacketGetOne 抢一个红包
func RedPacketGetOne[T any](ctx context.Context, redis *redis2.Client, cacheKey string, noRedisCacheFn, takeOutFn func() error, takeOneFn func(T) error) error {
text, err := redis.Eval(ctx, takeOneRedPacketCmd, []string{cacheKey}).Text()
if err != nil {
return err
}
if text == "noKey" {
if noRedisCacheFn == nil {
return errors.New("redis no cache found")
}
return noRedisCacheFn()
} else if text == "takeOut" {
if takeOneFn == nil {
return errors.New("red packet is take out")
}
return takeOutFn()
} else if text == "" {
return errs.NewErrNormal("redis get empty val")
} else {
return takeOneFn(encodeutil.FromGobBytes[T]([]byte(text)))
}
}
// RedPacketSetUnTake 设置未抢到的红包到redis data 长度可为空
// 设置成功或失败 失败是由于key已经存在了
func RedPacketSetUnTake[T any](ctx context.Context, redis *redis2.Client, cacheKey string, data []T) (bool, error) {
// 长度为零
if len(data) == 0 {
setCount, err := redis.Eval(ctx, setUnTakeRedPacketListEmptyCmd, []string{cacheKey}).Text()
if err != nil {
return false, err
}
if setCount == "NotTakeOut" {
return false, nil
} else if setCount == "OK" {
return true, nil
} else {
return false, errs.NewErrNormal("unknown redis error")
}
}
// 长度非零
var toSetByteList = lo.Map(data, func(item T, index int) any {
return encodeutil.ToGobBytes(item)
})
setCount, err := redis.Eval(ctx, setUnTakeRedPacketListCmd, []string{cacheKey}, toSetByteList...).Int()
if err != nil {
return false, err
}
if setCount == -1 {
return false, nil
} else if setCount == len(toSetByteList) {
return true, nil
} else {
return false, errs.NewErrNormal("unknown redis error or partly set")
}
}