golangはredisとconsulに基づく水平拡張可能なランキングサービスの例を実現する

13360 ワード

本文の完全なコードは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の使用
ランキングを実現するにはredisが必要な場所が2つあります.
  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]string
err := 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でサービスをオープンしてクライアントリクエストを構造し、結果を返す
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にはserverNodeというechoサービス(ソース「golang使用サービス発見システムconsul」)と、本明細書のランキングサービスrankNodeの2つのカスタムサービスが登録されています.
サーバが受信したリクエストフラグメント
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に登録してサービスを提供することができます.