排队服务器的设计
2020-07-24
4 min read
排队服务不在玩家的主流程上,主要功用是在服务器爆满时使未进入的玩家体验更好。
需求
业务需求
- 服务器空闲时玩家无感知
- 服务器爆满时按照玩家登录的先后顺序排队等待进入服务器
- 已排队进入服务器的玩家掉线重连不需要重新排队
约束
- 排队服务器不在主流程上,挂掉不应该影响玩家正常游戏
设计
- 排队服务器是一个单独的服务器,不与游戏在一起。这样做可以满足多 gate 的情况。还有额外的好处是不会给游戏带来额外的压力,并且提高排队服的通用性,之后的游戏可以直接接入。
- 排队服不需要绝对的可靠。因此,我把排队的数据全部保存在进程内存中,这样简化了排队服的业务逻辑。付出的代价是一旦排队服挂掉,所有排队号都会失效。
- 排队服使用 http 协议,是一个 web 应用。这样比较简单,客户端也表示他们更方便接入。
- 排队服使用业务使用单线程设计,简化实现,且排队服业务简单,数据全部在内存中,性能不是瓶颈。
客户端交互流程图
核心数据结构
排队使用 skipList 实现,score 由一个 AtomicLong 发放,依次递增,保证先后顺序。
排队号的过期使用 timeWheel 做定时(直接使用 netty 的 HashedWheelTimer)。
接口
与游戏服的接口
// 登录 gate 时通过此接口返回 ticket 并验证是否需要排队
@RequestMapping(value = "/get_ticket/v1/{profileId}/{serverId}", method = RequestMethod.GET)
@ApiOperation(value = "获取排队号")
@ApiImplicitParams({
@ApiImplicitParam(name = "profileId", value = "profile id", required = true, type = "path"),
@ApiImplicitParam(name = "serverId", value = "server id", required = true, type = "path")
})
public ApiResult<GetTicketResult> getTicket(@PathVariable("profileId") Long profileId, @PathVariable("serverId") Integer serverId) ;
// 退出或掉线时通过此接口延迟过期排队号
@RequestMapping(value = "/ticket_logout/v1/{ticket}", method = RequestMethod.GET)
@ApiOperation(value = "退出时延迟过期排队号")
@ApiImplicitParam(name = "ticket", value = "排队号", required = true, type = "path")
public ApiResult<Void> scheduleExpireTicket(@PathVariable("ticket") String ticket) ;
与客户端的接口
//客户端请求此接口获得自己的排队名次。客户端需要根据名次确定合理的请求频率,越接近进入游戏,频率越高
@RequestMapping(value = "/check/v1/{ticket}", method = RequestMethod.GET)
@ApiOperation(value = "检查排队号")
@ApiImplicitParam(name = "ticket", value = "queue ticket", required = true, dataType = "String", type = "path")
public ApiResult<CheckTicketResult> checkTicket(@PathVariable("ticket") String ticket);
宕机恢复
一旦排队服务器宕机,排队号全部过期,玩家只能依靠 Gate 限流,比较粗暴。
排队服再次启动后,由于排队服的在线数已丢失,无法正确排队。规避办法是排队服定期(每10秒)去查询一下游戏服在线人数。判断是否需要排队时取 Max(游戏服在线人数,排队服在线人数)。