golang实现基于redis和consul的可水平扩展的排行榜服务范例

本文的完整代码见 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]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
  增加初始玩家信息(略)。
  注册服务器接口,此部分详细说明请参考《go通过反射使用json字符串调用struct的指定方法及返回json结果》http://changjixiong.com/reflect-invoke-method-of-struct-and-get-json-format-result/
reflectinvoke.RegisterMethod(rankservice.DefaultRankService)
  将服务注册到consul,此部分详细说明请参考《go使用服务发现系统consul》http://changjixiong.com/use-consul-in-go/
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服务(来源 《go使用服务发现系统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以提供服务。

本文分享自微信公众号 - Golang语言社区(Golangweb)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-06-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

51. Socket服务端和客户端使用TCP协议通讯 | 厚土Go学习笔记

Socket服务器是网络服务中常用的服务器。使用 go 语言实现这个业务场景是很容易的。 这样的网络通讯,需要一个服务端和至少一个客户端。 我们计划构建一个这样...

30640
来自专栏Golang语言社区

[转载]Golang 编译成 DLL 文件

首先撰写 golang 程序 exportgo.go: package main import "C" import "fmt" //export Print...

43540
来自专栏Golang语言社区

Go 语言构建高并发分布式系统实践

你知道互联网最抢手的技术人才有哪些吗?最新互联网职场生态报告显示,最抢手的十大互联网技术人才排名中Go语言开发人员位居第三,从中不难见得,Go语言的渗透率越来越...

78850
来自专栏Golang语言社区

[转载]Go JSON 技巧

相对于很多的语言来说, Go 的 JSON 解析可谓简单至极. 问题 通常情况下, 我们在 Go 中经常这样进行 JSON 的解码: package main ...

41530
来自专栏Golang语言社区

[基础篇]Go语言变量

变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念。变量可以通过变量名访问。 Go 语言变量名由字母、数字、下划线组成,其中首个字母不能为数字。 声...

41870
来自专栏Golang语言社区

Golang Template 简明笔记

作者:人世间 链接:https://www.jianshu.com/p/05671bab2357 來源:简书 前后端分离的Restful架构大行其道,传统的模板...

99460
来自专栏深度学习之tensorflow实战篇

golang 格式“占位符”%d,%f,%s等应用类型

golang 的fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf。 红色部分为常用占位符 ? ? ? ? ? ? ? 对于 ...

41070
来自专栏Golang语言社区

Go代码打通HTTPs

TL;DR 手工创建CA证书链,手写代码打通HTTPs的两端 HTTPs最近是一个重要的话题,同时也是一个有点难懂的话题。所以网上有大量的HTTPs/TLS/S...

45240
来自专栏Golang语言社区

golang 几种字符串的连接方式

最近在做性能优化,有个函数里面的耗时特别长,看里面的操作大多是一些字符串拼接的操作,而字符串拼接在 golang 里面其实有很多种实现。 实现方法 直接使用运算...

35970
来自专栏Golang语言社区

厚土Go学习笔记 | 29. 接口

在go语言中,接口类型是由一组方法定义的集合。 一个类型是否实现了一个接口,就看这个类型是否实现了接口中定义的所有方法。在go语言中,无需特别的指明定义一个接口...

38650

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励