redis配合golang实现简单的抢红包功能

抢红包功能

抢红包功能是一个比较重要的功能,在分布式环境中,需要考虑使用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")
	}
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独杆小蓬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值