重构登录逻辑

终于又甩掉了一个包袱, 这是我重构完第一句想说的话。

从我入职开始, 就因为这套登录逻辑的复杂性而略为不满, 其间还坑过我一次,但是因为一直勉强能用,所以也没有理由对它动手。

这一次终于因为不能够满足新需求,而让我有理由可以重构这段代码了。


旧的登录流程大致如下:

    $UST=(uid,session,token)
    Client-------------AuthServer---------------------GameServer-----------DBProxy
     *--"账号密码认证" ---> * 
                     检查账号密码,
                     生成session和token
                          *----------发送$UST----------->*
                                                     记录$UST
                          *<----------成功--------------*
    *<------$UST----------*
    *-------使用$UST登陆-------------------------------->*
                                                     校验$UST是否匹配
                                                        *------请求加载------->*
                                                                      加载玩家数据
                                                        *<---返回玩家数据------*
    *<---------------------------返回登陆成功------------*

每一个认证流程都会生成一个惟一session,类似tcp连接. 甚至加载玩家数据也会带着同样的session请求。如果此次数据加载与当前登陆session不符,则拒绝使用,直接抛弃。

session的存在使的整个“认证-登陆”流程可以做到精确无比。

然而,这种精确会带来其他问题,例如客户端出于某种bug多发了一次认证,就会导致整个登陆流程失败,并且会导致重复加载玩家数据(事实上我真的被这个问题坑过,查了好久才发现问题)。


经过这次重构后,我去掉了session机制,并将GameServer的整个登陆流程切分成三块相对独立的逻辑,即“认证逻辑”,“加载逻辑”,“登陆逻辑”。

  1. 当AuthServer检查账号密码正确后,即颁发token, 将uid,token发往GameServer。
  2. 当GameServer的“认证逻辑”到消息uid,token之后,记下token并随即调用“加载逻辑”进行数据加载。
  3. 当GameServer的“登陆逻辑”收到协议后会检查uid,token是否匹配,如果匹配则等待数据加载完成,然后向客户端返回登陆成功。

“认证逻辑”在收到AuthServer的消息后,仅简单的记录每一个要登陆的uid对应的最新的token,并踢掉当前在线的链接。

“加载逻辑”则需要对加载请求进行过滤,防止重复加载。当收到加载请求后,如果数据已经加载途中或已经加载完成,则直接扔掉加载请求。为了防止DBProxy挂掉,而造成加载卡死。每一个玩家会有一个30秒的加载超时时间(即超过30s后即使DBProxy还没有返回数据,下次收到加载数据请求,依然会再次向DBProxy请求)。

加载数据返回后,总是检查当前内存中是否已经有加载成功的数据,如果没有才使用当前DBProxy返回的玩家数据,如果已经有数据,则直接抛弃。

之所以这样做,是为了数据一致性问题。来看一下如下操作序列。

GameServer视角:加载数据请求A—〉等待30秒—>加载数据请求B—->加载数据请求A返回—>玩家开始操作,修改玩家数据—>向DBProxy更新数据—>另载数据请求B返回。

DBProxy视角: 加载数据A->返回加载数据A->加载数据B->返回加载数据B->修改数据

如果此时应用数据B,则在A返回和B返回对玩家数据的修改则全部会丢失。而旧的登陆逻辑,之所以在加载数据时依然带着session,应该也是为了防止数据一致性问题,但是我们玩家数据都是采用Write-Through方式来将一个玩家的所有数据都cache内存之后才进行修改的。因此我认为只取第一份成功加载的数据就可以解决一致性问题。

重构之后的登陆流程大概如下:

    $UT=(uid,token)
    Client-------------AuthServer---------------------GameServer-----------DBProxy
     *--"账号密码认证" ---> * 
                     检查账号密码,
                     生成session和token
                          *----------发送$UT----------->*
                                                     记录$UT
                          *<----------成功--------------*--------请求加载----->*
    *<------$UT----------*                                         加载玩家数据
                                                         | <-----返回玩家数据--*
    *-------使用$UST登陆--------------------------------> |
                                                         |
    *<------返回登陆成功----------------------------------* 

本来“加载逻辑”还要负责登陆超时的淘汰操作,即数据加载之后,客户端迟迟不向GameServer发送登陆请求,这时需要将加载出来的数据从GameServer淘汰掉。

但是由于我们专为手游实现了半弱联网机制。在socket断掉之后,N分钟以内会保留玩家数据。这可以保证在地铁或电梯等信号不好处链接断掉后,可以在信号良好时快速登陆。

因此“加载逻辑”将数据加载完成之后,直接交给原本的数据淘汰逻辑即可。


除了登陆流程以外,这次重构我还修改了"登陆"的语义。

重构前, 对AuthServer发起认证时,会先尝试对账号进行注册,再发送登陆协议。虽然这样可以避免在新号时连着发送三条认证相关的协议。但总感觉有点bad taste。

重构后,我将"登陆"定义为"如果账号不存在则先执行注册, 然后执行登陆逻辑"。

重构前,对GameServer发起登陆时,如果uid还没有对应的游戏数据,同样会返回错误。然后客户端根据错误码去发初始化消息进行初始化, 在初始化的过程中,如果由于某种原因连续多发,同样会出现问题。

重构后,当GameServer收到$UT准备加载数据前,会先检查uid是否被初始化过数据,如果没有,则直接向DBProxy请求创建。DBProxy创建玩家数据与DBProxy加载玩家数据具有相同的返回。

这样整个"认证-登陆"逻辑相关的协议,由4条("注册","认证","创建","登陆")降为2条("认证","登陆")。

这里会有一个问题,刚开服时,可能有很多玩家只注册完就卸载走人了。这样会浪费我们的服务器资源。因为他只要发一条"认证",我们就帮他把数据创建好了。

幸运的是,我们新实现里, 恰好提前实现了删号功能。即一个低级玩家多久没登陆过之后,过一段时间会自动被清理。

ps.重构之后,个人觉得对于整个流程,服务端和客户端都各种更内聚了,因为整个登陆流程的判断都被挪到了服务端。