golang实现基于redis和consul的可水平扩展的排行榜服务范例
概述
排行榜在各种互联网应用中广泛存在。本文将用一个范例说明如何利用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以提供服务。
- 【插件开发】—— 14 Site is incorrect!编辑器启动报错!
- Java魔法堂:Date与日期时间格式化
- Java魔法堂:打包知识点之META-INF/MAINFEST.MF
- WordPress快速建站
- 大数据时代下的生活
- 【Spring实战】—— 1 入门讲解
- 博客园小技巧
- JS魔法堂:关于元素位置和鼠标位置的属性
- MyBatis魔法堂:Insert操作详解(返回主键、批量插入)
- Winodws安装系统时,通过安装磁盘进行分区
- Eclipse安装SVN插件
- JS魔法堂:IE5~9的Drag&Drop API
- mysql 5.7版本目录无data文件夹的解决办法
- 【UML】——为什么要使用UML
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- HDU Problem D [ Humble number ]——基础DP丑数序列
- Java开发编程规范: 3.代码格式
- flex-direction
- css中清除浮动方式五
- css中-清除浮动方式四
- forin forof forEach myForEach
- 深拷贝,你懂吗?
- Codeforce-CodeCraft-20 (Div. 2)-C. Primitive Primes(本原多项式+数学推导)
- DOM事件机制(原理级别的)
- 杭电60题--part 1 HDU1003 Max Sum(DP 动态规划)
- js的的的图片随屏幕滚动而滑入滑出的效果(万 万。。。字长文)
- Codeforce-CodeCraft-20 (Div. 2)-B. String Modification (找规律+模拟)
- Codeforce-CodeCraft-20 (Div. 2)-A. Grade Allocation
- Cypress系列(69)- route() 命令详解
- Codeforce-Ozon Tech Challenge 2020-D. Kuroni and the Celebration(交互题+DFS)