# Day30 Golang 鍵值資料庫 Redis 實作抽獎小遊戲 接下來要用程式來模擬Client執行的動作。 ## 安裝 go-redis [**go-redis**](https://github.com/go-redis/redis)目前主要有`v6`版跟`v8`版,兩者的語法使用上不相同。 #### go get 全域安裝 V6版本 $ go get github.com/go-redis/redis V8版本 $ go get github.com/go-redis/redis/v8 這裡會先介紹[**v6**的版本](https://github.com/go-redis/redis/tree/v6.15.9) 在這邊稍加修改Github上的[**Quickstart**](https://github.com/go-redis/redis/tree/v6.15.9#quickstart),並且運行 ```go package main import ( "fmt" "github.com/go-redis/redis" ) func main() { c := NewClient() test(c) } func NewClient() *redis.Client { // 實體化redis.Client 並返回實體的位址 client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password set DB: 0, // use default DB }) pong, err := client.Ping().Result() fmt.Println(pong, err) return client } func test(c *redis.Client) { // 對該 redis.Client 進行操作 err := c.Set("key", "value", 0).Err() // => SET key value 0 數字代表過期秒數,在這裡0為永不過期 if err != nil { panic(err) } val, err := c.Get("key").Result() // => GET key if err != nil { panic(err) } fmt.Println("key", val) val2, err := c.Get("key2").Result() // => GET key2 if err == redis.Nil { fmt.Println("key2 does not exist") } else if err != nil { panic(err) } else { fmt.Println("key2", val2) } } ``` --- 事不宜遲,直接入主題:今晚,我想來點... 答對了!就是抽獎小遊戲! ## 抽獎遊戲 既然`Redis`能在短時間內快速讀寫,我想透過他、並且加上`Gin`網頁框架,來製做一個迷你遊戲。 * 每位玩家註冊後預設有1000塊。可以投注任意正整數金額,投越多錢、中獎機率越高。 * 伺服器每分鐘會抽一位幸運者出來,並把這局所有的錢給予這名幸運者。 很適合用Redis中 `Sorted-Set` 這個類型的Score來計分,當作玩家擁有的錢。 #### 一開始先宣告會用到的物件,〝玩家〞以及 〝玩家下注〞 ```go type User struct { Id string `json:"Id"` // 玩家 ID Balance int `json:"balance"` // 玩家餘額 } type UserBet struct { Id string `json:"Id"` Round int `json:"round"` // 局數 Amount int `json:"amount"` // 下注金額 } ``` #### 設定一些常數 ```go const ( RoundSecond = 60 // 每一局的時間 DefaultBalance = 1000 // 玩家初始化金額 UserMember = "game" // 儲存所有使用者的Balance Redis:`Sorted-Set` SCORE -> USER BetThisRound = "bet_this_round" // 儲存目前局的下注狀況 Redis:`Sorted-Set` SCORE -> USER ) ``` #### Gin的操作 ```go router.GET("/bet/:user", GetUserBalance) // 玩家註冊(不須密碼,填入帳號即可)`user`區分大小寫 router.GET("/bet/:user/:amount", Bet) // 玩家對目前的局面進行下注,`amount`金額 ``` #### 獲取bet的玩家帳號 及金額 ```go user.Id = c.Param("user") amountStr := c.Param("amount") ``` #### go-Redis 操作 下注前,先對用戶做查詢,查看玩家餘額足不足夠。 ```go balance, err := RC.ZScore(UserMember, user.Id).Result() // => ZSCORE `Table` UserID ``` 如果成功下注,玩家餘額為 目前餘額減去下注金額。 並且用`BetThisRound` 表來記錄 此局此玩家的`權重`(籤數,越高越容易中獎)。 ```go RC.ZIncrBy(UserMember, float64(-amount), user.Id) RC.ZIncrBy(BetThisRound, float64(amount), user.Id) ``` 查詢`BetThisRound`獲取此局目前的獎金池 ```go bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result() for _, bet := range bets { var userBet UserBet userBet.Amount = int(bet.Score) prizePool += userBet.Amount } ``` #### 抽出一個幸運兒 ```go winNum := rand.Intn(prizePool + 1) // 亂數一個幸運號碼 for _, userBet := range userBets { winNum -= userBet.Amount if winNum <= 0 { winner = userBet.Id break } } ``` ## 完整遊戲小程式 ```go package main import ( "errors" "fmt" "log" "math/rand" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/go-redis/redis" ) const ( RoundSecond = 60 // 每一局的時間 DefaultBalance = 1000 // 玩家初始化金額 UserMember = "game" // 儲存所有使用者的Balance Redis:`Sorted-Set` SCORE -> USER BetThisRound = "bet_this_round" // 儲存目前局的下注狀況 Redis:`Sorted-Set` SCORE -> USER ) var Round = 0 var startTimeThisRound time.Time var RC *redis.Client type User struct { Id string `json:"Id"` // 玩家 ID Balance int `json:"balance"` // 玩家餘額 } type UserBet struct { Id string `json:"Id"` Round int `json:"round"` // 局數 Amount int `json:"amount"` // 下注金額 } func init() { RC = newClient() // 初始化清空所有Redis RC.Del(UserMember) RC.Del(BetThisRound) go GameServer() } func main() { router := gin.Default() router.RedirectFixedPath = true router.GET("/bet/:user", GetUserBalance) // 玩家註冊(不須密碼,填入帳號即可)`user`區分大小寫 router.GET("/bet/:user/:amount", Bet) // 玩家對目前的局面進行下注,`amount`金額 router.GET("/prize", GetCurrentPrize) // 此局目前的獎金池 router.GET("/bets", GetUserBets) // 此局所有玩家目前的下注 router.Run(":80") } func newClient() *redis.Client { client := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", DB: 0, }) pong, err := client.Ping().Result() log.Println(pong) if err != nil { log.Fatalln(err) } return client } func GameServer() { rand.Seed(time.Now().UTC().UnixNano()) ticker := time.NewTicker(RoundSecond * time.Second) // 每過RoundSecond秒,執行一次以下迴圈 go func() { for { Round++ startTimeThisRound = time.Now() log.Println(startTimeThisRound.Format("2006-01-02 15:04:05"), "\t round", Round, "start") _ = <-ticker.C var prizePool = getCurrentPrize() var userBets = getUserBets() if len(userBets) == 0 { log.Println("Round", Round, "沒有任何玩家下注") continue } // 抽獎選贏家 winNum := rand.Intn(prizePool + 1) var winner string for _, userBet := range userBets { winNum -= userBet.Amount if winNum <= 0 { winner = userBet.Id break } } log.Println("獎金池:", prizePool, "\t 得主:", winner) // 發獎金給得主 RC.ZIncrBy(UserMember, float64(prizePool), winner) // 刪除現有Table RC.Del(BetThisRound) } }() } func Bet(c *gin.Context) { var user User user.Id = c.Param("user") amountStr := c.Param("amount") amount, err := strconv.Atoi(amountStr) if err != nil { wrapResponse(c, nil, errors.New("下注金額有誤")) return } balance, err := RC.ZScore(UserMember, user.Id).Result() if err == redis.Nil { wrapResponse(c, nil, errors.New("查無此用戶,請先註冊")) return } user.Balance = int(balance) if amount <= 0 { wrapResponse(c, nil, errors.New("下注金額需為正整數")) return } if amount > user.Balance { wrapResponse(c, nil, errors.New("餘額不足")) return } user.Balance -= amount RC.ZIncrBy(UserMember, float64(-amount), user.Id) RC.ZIncrBy(BetThisRound, float64(amount), user.Id) wrapResponse(c, user, nil) } func GetUserBalance(c *gin.Context) { var user User user.Id = c.Param("user") balance, err := RC.ZScore(UserMember, user.Id).Result() if err == redis.Nil { //查無使用者,註冊新帳號 balance = DefaultBalance RC.ZAdd(UserMember, redis.Z{ Score: balance, Member: user.Id, }) } user.Balance = int(balance) wrapResponse(c, user, nil) } func GetCurrentPrize(c *gin.Context) { wrapResponse(c, getCurrentPrize(), nil) } func getCurrentPrize() (prizePool int) { bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result() for _, bet := range bets { var userBet UserBet userBet.Amount = int(bet.Score) prizePool += userBet.Amount } return } func GetUserBets(c *gin.Context) { UserBets := getUserBets() if len(UserBets) == 0 { wrapResponse(c, nil, errors.New("目前沒有任何記錄")) return } wrapResponse(c, UserBets, nil) } func getUserBets() (userBets []UserBet) { bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result() for _, bet := range bets { var userBet UserBet userBet.Id = fmt.Sprintf("%s", bet.Member) userBet.Amount = int(bet.Score) userBet.Round = Round userBets = append(userBets, userBet) } return } func wrapResponse(c *gin.Context, data interface{}, err error) { type ret struct { Status string `json:"status"` Msg string `json:"msg"` Data interface{} `json:"data"` } d := ret{ Status: "ok", Msg: "", Data: []struct{}{}, } if data != nil { d.Data = data } if err != nil { d.Status = "failed" d.Msg = err.Error() } c.JSON(http.StatusOK, d) } ``` ## 玩法 返回的Json 數值皆為`使用者的餘額`。 程式執行起來後,開啟多個瀏覽器,每個瀏覽器作為一個獨立的玩家。 註冊`Jack`帳號:http://127.0.0.1/bet/Jack `Jack`下注50元:http://127.0.0.1/bet/Jack/50 註冊`Timmy`帳號:http://127.0.0.1/bet/Timmy `Timmy`下注333元:http://127.0.0.1/bet/Timmy/333 (接著靜待一分鐘,抽出一名幸運兒。) 查看餘額`Jack`餘額:http://127.0.0.1/bet/Jack 查看餘額`Timmy`餘額:http://127.0.0.1/bet/Timmy