排队服务器的设计

排队服务不在玩家的主流程上,主要功用是在服务器爆满时使未进入的玩家体验更好。

需求

业务需求

  1. 服务器空闲时玩家无感知
  2. 服务器爆满时按照玩家登录的先后顺序排队等待进入服务器
  3. 已排队进入服务器的玩家掉线重连不需要重新排队

约束

  1. 排队服务器不在主流程上,挂掉不应该影响玩家正常游戏

设计

  1. 排队服务器是一个单独的服务器,不与游戏在一起。这样做可以满足多 gate 的情况。还有额外的好处是不会给游戏带来额外的压力,并且提高排队服的通用性,之后的游戏可以直接接入。
  2. 排队服不需要绝对的可靠。因此,我把排队的数据全部保存在进程内存中,这样简化了排队服的业务逻辑。付出的代价是一旦排队服挂掉,所有排队号都会失效。
  3. 排队服使用 http 协议,是一个 web 应用。这样比较简单,客户端也表示他们更方便接入。
  4. 排队服使用业务使用单线程设计,简化实现,且排队服业务简单,数据全部在内存中,性能不是瓶颈。

客户端交互流程图

核心数据结构

排队使用 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(游戏服在线人数,排队服在线人数)。