Lincxlock Example--模拟发奖系统

Lincxlock Example–模拟发奖系统

需求背景

​ 在游戏发奖系统中,为了防止用户多次点击领奖请求引起多次领奖问题,需要确保每个游戏玩家在一次抽奖活动中只能得到获得一次奖励机会,且由于礼包数量有限,需要确保礼包按给定的数量发放,防止“超发”现象发生。

需求分析

​ 因为发奖服务是部署在分布式系统当中,要防止上述问题的发生,需要使用到分布式锁的功能,可以直接应用LINCXLOCK的分布式锁。在发奖系统中,为了保证效率,使用Redis作为缓存,所以在使用LINCXLOCK分布式锁的时候可基于Redis,作为插件方式引入。

具体实现

代码执行流程如下图所示:

LDQacQ.png

1、引用LINCXLOCK插件;

2、通过编写配置文件或者直接赋值的方式初始化LINCXLOCK分布式锁

3、创建用户id的分布式锁对象,调用加锁方法,成功后,继续执行;否则,返回“重复请求”的信息;

4、判断可以领奖,创建礼包的分布式锁对象,调用加锁方法,成功后,继续执行下面的逻辑,否则,调用用户分布式锁对象的解锁方法,返回“已领奖”信息;

5、结束之后,调用两个锁对象的解锁方法,释放锁,返回礼包。

main.go文件

package main

import (
	"errors"
	"log"
	"net/http"
	"strconv"
	"sync"
	"time"
	"weblincx/redisclient"
	"github.com/lincx-911/lincxlock" //引入LINCXLOCK分布式锁框架
	"github.com/gin-gonic/gin"
)

const(
	OK             = iota
	ParamsError    // 传参错误
	ServerError    // 服务错误
)

var (
	timeout               = 3
	Gift_Lock_Prefix      = "gift_lock_"
	User_Gift_Lock_Prefix = "user_gift_lcok"
	USER_GIFTED_KEY       = "user_gift"
)

// GetRewardHandle 领奖controller层
func GetRewardHandle(ctx *gin.Context) {
	user_id := ctx.Query("user_id")
	if len(user_id)==0{
		HandleAPIReturn(ctx,ParamsError,"param user_id is nil",nil)
		return 
	}
	gift_id := ctx.Query("gift_id")
	if len(gift_id)==0{
		HandleAPIReturn(ctx,ParamsError,"param gift_id is nil",nil)
		return 
	}
	res,err := RewardService(user_id,gift_id)
	if err!=nil{
		HandleAPIReturn(ctx,ServerError,err.Error(),nil)
		return
	}
	HandleAPIReturn(ctx,OK,"success",res)
}

//HandleAPIReturn 返回请求
func HandleAPIReturn(ctx *gin.Context, code int, msg string, data interface{}) {
	ctx.JSON(http.StatusOK, gin.H{
		"code": code,
		"msg":  msg,
		"data": data,
	})
}


// RewardService 领奖service层
func RewardService(userid, giftid string) (string, error) {
	conf,err:=config.NewLockConf(locktype,5,hosts)
	if err!=nil{
		return "",err
	}
	uMutex ,err:= lock.NewLincxLock(User_Gift_Lock_Prefix+userid,conf)// 幂等性处理,防止重复请求
    if err!=nil{
        return "",err
    }
	err := uMutex.Lock()
	if err!=nil{
		return "",errors.New("重复请求")
	}
	defer uMutex.Unlock()
	endTime := time.Now().Add(time.Second * time.Duration(timeout))
	for time.Now().Before(endTime) {
		if redisclient.SISMembers(redisclient.Pool.Get(), USER_GIFTED_KEY, userid) {
			return "", errors.New("已领奖")
		}
		// 加锁,领奖
		lock_key := Gift_Lock_Prefix + giftid
		gMutex err:= lock.NewLincxLock(lock_key,conf) // 对礼包加锁,防止超发
        if err!=nil{
            continue
        }
		err = gMutex.Lock()
		if err!=nil{
			continue
		}
		defer gMutex.Unlock()
		//礼包数量
		numstr, err := redisclient.Get(redisclient.Pool.Get(), giftid)
		if err != nil {
			return "", err
		}
		num, _ := strconv.Atoi(numstr)
		if num <= 0 {
			return "", errors.New("礼包已领完")
		}
		// 礼包数量减一
		err = redisclient.Decr(redisclient.Pool.Get(), giftid)
		if err != nil {
			return "", err
		}
		// 设置用户领奖状态
		err = redisclient.SAdd(redisclient.Pool.Get(), USER_GIFTED_KEY, userid)
		if err != nil {
			return "", err
		}
		
        // 发奖礼包号
		return giftid, nil
	}
	return "", errors.New("请求失败,请重试")
}

// InitRouter 初始化路由
func InitRouter() *gin.Engine{
	router := gin.Default()
	rLock := router.Group("/lincx")
	{
		rLock.GET("/reward",GetRewardHandle)
		
	}
	return router
}

func main() {
	r := InitRouter()
	r.Run(":8089") //启动端口号为8089
}

weblincx/redisclient

package redisclient

import (
	"log"
	"time"

	"github.com/go-redsync/redsync/v4"
	"github.com/go-redsync/redsync/v4/redis"
	"github.com/go-redsync/redsync/v4/redis/redigo"
	redigolib "github.com/gomodule/redigo/redis"
)

const(
	SUCCESS_REPLY = "OK"
)

var (
	Pool *redigolib.Pool
)

func init() {
	Pool = GetPool("172.27.76.104:6379","")
}



// GetPool 创建redis连接池
func GetPool(server ,password string)*redigolib.Pool{
	if Pool!=nil{
		return Pool
	}
	return &redigolib.Pool{
		MaxIdle:     5,//空闲数
        IdleTimeout: 240 * time.Second,
        MaxActive:   10,//最大数
		Wait:        true,
        Dial: func() (redigolib.Conn, error) {
            c, err :=redigolib.Dial("tcp", server)
            if err != nil {
                return nil, err
            }
            if password != "" {
                if _, err := c.Do("AUTH", password); err != nil {
                    c.Close()
                    return nil, err
                }
            }
            return c, err
        },
		TestOnBorrow: func(c redigolib.Conn, t time.Time) error {
            _, err := c.Do("PING")
            return err
        },
	}
}

// Get redigolib get
func Get(conn redigolib.Conn, key string) (string, error) {
	defer conn.Close()
	val, err := redigolib.String(conn.Do("GET", key))
	if err != nil {
		log.Printf("redigolib get error: %s\n", err.Error())
		return "", err
	}
	return val, err
}

// Set redigolib set
func Set(conn redigolib.Conn, key string,value interface{}) (bool, error) {
	defer conn.Close()
	ok, err := conn.Do("SET", key, value)
	if err != nil {
		log.Printf("redigolib set error: val:%s %s\n", ok,err.Error())
		return false, err
	}
	return ok=="OK", nil
}
// SetNX redigolib setnx
func SetNX(conn redigolib.Conn, key, value string,timeout int)(bool,error){
	defer conn.Close()
	reply, err := redigolib.String(conn.Do("SET", key, value, "EX", timeout, "NX"))
	if reply==SUCCESS_REPLY{
		return true,nil
	}
	if err==redigolib.ErrNil{
		return false,nil
	}
	return false,err
}

// HSet redigolib hset
func HSet(conn redigolib.Conn, key, field string, data interface{}) error {
	defer conn.Close()
	_, err := conn.Do("HSET", key, field, data)
	if err != nil {
		log.Printf("redigolib hSet error: %s\n", err.Error())
	}
	return err
}

// HGet redigolib hget
func HGet(conn redigolib.Conn, key, field string) (interface{}, error) {
	defer conn.Close()
	data, err := conn.Do("HGET", key, field)
	if err != nil {
		log.Printf("redigolib hSet error: %s\n", err.Error())
		return nil, err
	}
	return data, nil
}

/**
redigolib INCR 将 key 所储存的值加上增量 1
*/
func Incr(conn redigolib.Conn, key string) error {
	defer conn.Close()
	_, err := conn.Do("INCR", key)
	if err != nil {
		log.Printf("redigolib incrby error: %s\n", err.Error())
		return err
	}
	return nil
}

/**
redigolib INCRBY 将 key 所储存的值加上增量 n
*/
func IncrBy(conn redigolib.Conn, key string, n int) error {
	defer conn.Close()
	_, err := conn.Do("INCRBY", key, n)
	if err != nil {
		log.Printf("redigolib incrby error: %s\n", err.Error())
		return err
	}
	return nil
}

/**
redigolib DECR 将 key 中储存的数字值减一。
*/
func Decr(conn redigolib.Conn, key string) error {
	defer conn.Close()
	_, err := conn.Do("DECR", key)
	if err != nil {
		log.Printf("redigolib decr error: %s\n", err.Error())
		return err
	}
	return nil
}

/**
redigolib SADD 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
*/
func SAdd(conn redigolib.Conn,key, v string) error {
	defer conn.Close()
	_, err := conn.Do("SADD", key, v)
	if err != nil {
	   log.Printf("SADD error: %s", err.Error())
	   return err
	}
	return nil
 }
 
 /**
 redigolib SMEMBERS 返回集合 key 中的所有成员。
 return map
 */
 func SMembers(conn redigolib.Conn,key string) (interface{}, error) {
	defer conn.Close()
	data, err := redigolib.Strings(conn.Do("SMEMBERS", key))
	if err != nil {
	   log.Printf("json nil: %v", err)
	   return nil, err
	}
	return data, nil
 }
 
 /**
 redigolib SISMEMBER 判断 member 元素是否集合 key 的成员。
 return bool
 */
 func SISMembers(conn redigolib.Conn,key, v string) bool {
	defer conn.Close()
	b, err := redigolib.Bool(conn.Do("SISMEMBER", key, v))
	if err != nil {
	   log.Printf("SISMEMBER error: %s", err.Error())
	   return false
	}
	return b
 }

  /**
 redigolib Script 执行脚本
 return interface{},error
 */
 func Script(conn redigolib.Conn,scriptStr string,args ...interface{})(interface{},error){
	defer conn.Close()
	lua := redigolib.NewScript(1, scriptStr)
	lua.Load(conn)
	redigolibargs := redigolib.Args.Add(args)
	reply, err := lua.Do(conn, redigolibargs...)
	if err!=nil{
		log.Printf("SISMEMBER error: %s", err.Error())
		if err==redigolib.ErrNil{
			return nil,nil
		}
		return nil,err
	}

	return reply,err
 }

 func RedisInt(reply interface{}, err error)(int,error){
	 
	num,err:=redigolib.Int(reply,err)
	if err==redigolib.ErrNil{
		return 0,nil
	}
	return num,err
 }

测试程序功能

首先在Redis中设置某个礼包的礼包数量,例如礼包号“wzry11220-3332211-5565222”的礼包数为100,

LrmDOI.png

然后执行测试程序:

func TestRewardService(){
    username := "user"
	gift_id := "wzry11220-3332211-5565222"
	var wg sync.WaitGroup
		for i := 0; i < 120; i++ {
			wg.Add(1)
			go func(user string) {
				defer wg.Done()
				_, err := RewardService(user, gift_id)
				if err != nil {
					log.Printf("%s reword fail:%v", user, err)
					return
				}
				log.Printf("%s reword successfully", user)

			}(username+strconv.Itoa(i))
		}
		wg.Wait()
}

预期结果,礼包数量剩为0,已领取礼包的用户为100。实际结果如下:

运行输出:

LrnwNT.png

Redis中礼包号数量

Lrn2Hx.png

Redis中已领取的用户数量

LruE5T.png

总结

使用LINCXLOCK的分布式锁服务,确实能很方便地开发业务代码,且能保证代码的正确性。