這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文的完整代碼見https://github.com/changjixiong/goNotes/tree/master/redisnote ,https://github.com/changjixiong/goNotes/tree/master/utils 及https://github.com/changjixiong/goNotes/tree/master/reflectinvoke如果文中沒有顯示連結說明連結在被轉寄的時候被幹掉了,請搜尋找到原文閱讀。
概述
熱門排行榜在各種互連網應用中廣泛存在。本文將用一個範例說明如何利用redis和consul實現可水平擴充的等級熱門排行榜服務。
redis的使用
實現熱門排行榜有2個地方需要用到redis:
1.儲存玩家的排行資訊,這裡使用的是Sorted Sets,代碼如下
err := Rds.ZAdd( PlayerLvRankKey, redis.Z{ Score: lvScoreWithTime(playerInfo.Lv, time.Now().Unix()), Member: playerInfo.PlayerID, }, ).Err()
其中lvScoreWithTime根據玩家等級及到達的時間計算score用於排名,等級相同的情況下,先到達等級的計算分值大於後達到的。
2.儲存玩家自身的資訊(名字,ID等),用於在熱門排行榜中顯示,畢竟僅僅只有排行的ID是不夠的。這裡採用hashset,代碼如下
// ma的類型為map[string]stringerr := Rds.HMSet(fmt.Sprintf("playerInfo:%d", playerID), ma).Err()
伺服器端
先初始化redis串連
rdsClient := redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", "127.0.0.1", 6379), Password: "123456", DB: 0, }) playercache.Rds = rdsClient rankservice.Rds = rdsClient
增加初始玩家資訊(略)。
註冊伺服器介面,此部分詳細說明請參考《golang通過反射使用json字串調用struct的指定方法及返回json結果》http://changjixiong.com/reflect-invoke-method-of-struct-and-get-json-format-result/
reflectinvoke.RegisterMethod(rankservice.DefaultRankService)
將服務註冊到consul,此部分詳細說明請參考《golang使用服務發現系統consul》http://changjixiong.com/use-consul-in-golang/
go registerServer()
在連接埠9528上開啟服務用於結構client請求並返回結果
ln, err := net.Listen("tcp", "0.0.0.0:9528") if nil != err { panic("Error: " + err.Error()) } for { conn, err := ln.Accept() // 對Accept()產生的臨時錯誤的處理,可以參考net/http/server.go中的func (srv *Server) Serve(l net.Listener) if err != nil { panic("Error: " + err.Error()) } go RankServer(conn) }
增加玩家經驗及設定玩家的熱門排行榜資料的介面如下
func (rankService *RankService) AddPlayerExp(playerID, exp int) bool { player := playercache.GetPlayerInfo(playerID) if nil == player { return false } player.Exp += exp // 固定經驗升級,可以按需要修改 if player.Exp >= playercache.LvUpExp { player.Lv += 1 player.Exp = player.Exp - playercache.LvUpExp rankService.SetPlayerLvRank(player) } playercache.SetPlayerInfo(player) return true}func (rankService *RankService) SetPlayerLvRank(playerInfo *playercache.PlayerInfo) bool { if nil == playerInfo { return false } err := Rds.ZAdd( PlayerLvRankKey, redis.Z{ Score: lvScoreWithTime(playerInfo.Lv, time.Now().Unix()), Member: playerInfo.PlayerID, }, ).Err() if nil != err { log.Println("RankService: SetPlayerLvRank:", err) return false } return true}
擷取指定排行的玩家資訊的介面
func (rankService *RankService) GetPlayerByLvRank(start, count int64) []*playercache.PlayerInfo { playerInfos := []*playercache.PlayerInfo{} ids, err := Rds.ZRevRange(PlayerLvRankKey, start, start+count-1).Result() if nil != err { log.Println("RankService: GetPlayerByLvRank:", err) return playerInfos } for _, idstr := range ids { id, err := strconv.Atoi(idstr) if nil != err { log.Println("RankService: GetPlayerByLvRank:", err) } else { playerInfo := playercache.LoadPlayerInfo(id) if nil != playerInfos { playerInfos = append(playerInfos, playerInfo) } } } return playerInfos}
用戶端
串連到consul並查到到熱門排行榜服務的地址,串連並發送請求
func main() { client, err := consulapi.NewClient(consulapi.DefaultConfig()) if err != nil { log.Fatal("consul client error : ", err) } for { time.Sleep(time.Second * 3) var services map[string]*consulapi.AgentService var err error services, err = client.Agent().Services() log.Println("services", strings.Repeat("-", 80)) for _, service := range services { log.Println(service) } if nil != err { log.Println("in consual list Services:", err) continue } if _, found := services["rankNode_1"]; !found { log.Println("rankNode_1 not found") continue } log.Println("choose", strings.Repeat("-", 80)) log.Println("rankNode_1", services["rankNode_1"]) sendData(services["rankNode_1"]) }}
運行情況
consul上註冊了2個自訂的服務,一個是名為serverNode的echo服務(來源 《golang使用服務發現系統consul》),另一個是本文的熱門排行榜服務rankNode。
伺服器接收到的請求片段
get: {"func_name":"AddPlayerExp","params":[4,41]}get: {"func_name":"AddPlayerExp","params":[2,35]}get: {"func_name":"AddPlayerExp","params":[5,27]}get: {"func_name":"GetPlayerByLvRank","params":[0,3]}
用戶端在consul中尋找到服務並串連rankNode_1
services ----------------------------------------------------------&{consul consul [] 8300 false}&{rankNode_1 rankNode [serverNode] 9528 127.0.0.1 false}&{serverNode_1 serverNode [serverNode] 9527 127.0.0.1 false}choose ------------------------------------------------------------rankNode_1 &{rankNode_1 rankNode [serverNode] 9528 127.0.0.1 false}
用戶端收到的回應片段
get: {"func_name":"AddPlayerExp","data":[true],"errorcode":0}get: {"func_name":"AddPlayerExp","data":[true],"errorcode":0}get: {"func_name":"AddPlayerExp","data":[true],"errorcode":0}get: {"func_name":"GetPlayerByLvRank","data":[[{"player_id":3,"player_name":"玩家3","exp":57,"lv":4,"online":true},{"player_id":2,"player_name":"玩家2","exp":31,"lv":4,"online":true},{"player_id":1,"player_name":"玩家1","exp":69,"lv":3,"online":true}]],"errorcode":0}
一點說明
為什麼說是可水平擴充的熱門排行榜服務呢?文中已經看到,目前有2個自定的服務註冊在consul上,client選擇了rankNode_1,那麼如果註冊了多個rankNode,則可以在其中某些節點不可用時,client可以選擇其他可用的節點擷取服務,而當停用節點重新可用時,可以繼續註冊到consul以提供服務。