golang实现基于redis和consul的可水平扩展的排行榜服务范例
2017-07-08 16:41
706 查看
本文的完整代码见https://github.com/changjixiong/goNotes/tree/master/redisnote ,https://github.com/changjixiong/goNotes/tree/master/utils 及https://github.com/changjixiong/goNotes/tree/master/reflectinvoke如果文中没有显示链接说明链接在被转发的时候被干掉了,请搜索找到原文阅读。
1.存储玩家的排行信息,这里使用的是Sorted Sets,代码如下
其中lvScoreWithTime根据玩家等级及到达的时间计算score用于排名,等级相同的情况下,先到达等级的计算分值大于后达到的。
2.存储玩家自身的信息(名字,ID等),用于在排行榜中显示,毕竟仅仅只有排行的ID是不够的。这里采用hashset,代码如下
增加初始玩家信息(略)。
注册服务器接口,此部分详细说明请参考《golang通过反射使用json字符串调用struct的指定方法及返回json结果》http://changjixiong.com/reflect-invoke-method-of-struct-and-get-json-format-result/
将服务注册到consul,此部分详细说明请参考《golang使用服务发现系统consul》http://changjixiong.com/use-consul-in-golang/
在端口9528上开启服务用于结构client请求并返回结果
增加玩家经验及设置玩家的排行榜数据的接口如下
获取指定排行的玩家信息的接口
服务器接收到的请求片段
客户端在consul中查找到服务并连接rankNode_1
客户端收到的回应片段
概述
排行榜在各种互联网应用中广泛存在。本文将用一个范例说明如何利用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
增加初始玩家信息(略)。
注册服务器接口,此部分详细说明请参考《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以提供服务。相关文章推荐
- 基于redis的分布式锁服务实现
- 基于Redis实现延时队列服务
- 服务注册发现consul之四: 分布式锁之四:基于Consul的KV存储和分布式信号量实现分布式锁
- 基于单点redis服务的分布式锁简单实现
- 基于Consul+Registrator+Nginx实现容器服务自动发现的集群框架 推荐
- golang sortedset 实现(基于redis skiplist)
- 短链接服务的接实现 基于nginx url php redis 、js二维码生成 、js二维码识别
- 基于Redis实现延时队列服务
- 基于 Consul 实现 MagicOnion(GRpc) 服务注册与发现
- 基于redis的zset实现排行榜功能
- 基于redis的排行榜设计和实现
- 基于反馈控制实现可靠的自动扩展服务
- Golang基于redis实现的分布式信号量(semaphore)
- 基于ESFramework的P2P实现 —— ESFramework扩展之EsfP2P
- 基于Spring-DM实现分布式服务框架(DSF)(一)
- 基于Mozilla Thunderbird的扩展开发(四)---修改Thunderbird源代码实现自动保存附件
- 《设计模式——基于C#的工程化实现及扩展》样书封面
- 基于FPGA的以太网MII接口扩展设计与实现
- WEB服务动态解析及调用技术的实现(基于WSDL4J及SAAJ)
- 【出版直播】博客园征途系列,《设计模式——基于C#的工程化实现及扩展》书签制作完成