接下來要用程式來模擬Client執行的動作。
go-redis目前主要有v6
版跟v8
版,兩者的語法使用上不相同。
V6版本
$ go get github.com/go-redis/redis
V8版本
$ go get github.com/go-redis/redis/v8
這裡會先介紹v6的版本
在這邊稍加修改Github上的Quickstart,並且運行
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
網頁框架,來製做一個迷你遊戲。
很適合用Redis中 Sorted-Set
這個類型的Score來計分,當作玩家擁有的錢。
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"` // 下注金額
}
const (
RoundSecond = 60 // 每一局的時間
DefaultBalance = 1000 // 玩家初始化金額
UserMember = "game" // 儲存所有使用者的Balance Redis:`Sorted-Set` SCORE -> USER
BetThisRound = "bet_this_round" // 儲存目前局的下注狀況 Redis:`Sorted-Set` SCORE -> USER
)
router.GET("/bet/:user", GetUserBalance) // 玩家註冊(不須密碼,填入帳號即可)`user`區分大小寫
router.GET("/bet/:user/:amount", Bet) // 玩家對目前的局面進行下注,`amount`金額
user.Id = c.Param("user")
amountStr := c.Param("amount")
下注前,先對用戶做查詢,查看玩家餘額足不足夠。
balance, err := RC.ZScore(UserMember, user.Id).Result() // => ZSCORE `Table` UserID
如果成功下注,玩家餘額為 目前餘額減去下注金額。
並且用BetThisRound
表來記錄 此局此玩家的權重
(籤數,越高越容易中獎)。
RC.ZIncrBy(UserMember, float64(-amount), user.Id)
RC.ZIncrBy(BetThisRound, float64(amount), user.Id)
查詢BetThisRound
獲取此局目前的獎金池
bets, _ := RC.ZRangeWithScores(BetThisRound, 0, -1).Result()
for _, bet := range bets {
var userBet UserBet
userBet.Amount = int(bet.Score)
prizePool += userBet.Amount
}
winNum := rand.Intn(prizePool + 1) // 亂數一個幸運號碼
for _, userBet := range userBets {
winNum -= userBet.Amount
if winNum <= 0 {
winner = userBet.Id
break
}
}
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