由于游戏业务逻辑的特殊性,并不适合转化成简单的数据库的CRUD操作,以及游戏对于时延的敏感、频繁操作数据对性能的高要求等,服务器的逻辑一般并不直接操作数据库,大多都是预先加载在内存进行操作,内存数据则定时异步存盘,数据库在这里主要提供了持久化功能,鲜少用到数据库的事务。这样的数据管理策略,在进程宕机时只会丢几分钟数据,且大部分需要数据一致性的数据大多时候刚好能在同一个批次存盘或者不存盘,线上实践中一般不会出问题或者出的问题可以忍受。然而,到了跨服交易行这种跨多个进程的需求,保持事务是不可绕过的需要解决的问题。

问题描述

跨服交易行简单的来说就是:每个服的玩家都能往交易行中寄卖自己的道具,也能在交易行中购买其他玩家的寄卖道具。以购买流程为例来阐述我们需要解决的问题,主要是要保证以下四个步骤在一个事务中完成:

1. 买主需要扣除一定金币成功;
2. 交易行中扣除指定购买数目的道具;
3. 卖家需要加上一定金币;
4. 把指定购买数目的道具加入到买主背包;

跨服架构可以有多种,但都离不开跨进程之间的事务,这里假设跨服的架构是很多区服进程(game server)共享一个跨服组进程(zone server),所有各个区服玩家上架到交易行的道具由各个game server推到zone server上去,交易行逻辑都会转到zone server上执行,全区服共享一个交易行。 步骤1是在买主区服进程中执行,步骤2在zone server执行,步骤3在卖家区服进程中执行,步骤4又是转到买主区服进程中执行。

分布式事务解决方案

在不同进程中保持分布式事务有一些常见的解决方案:

  1. 二阶段提交 需要引入一个第三方事务管理器,在原本纯粹的游戏服务器架构中,引入这样的角色并不协调。如果一个进程响应变慢会拖慢整个事务进度,对于游戏中频繁发生的交易行事务势必会有性能瓶颈。我们能容忍低概率的游戏进程宕机,但是机房中并不低概率的网络波动导致网络链接断开进而丢失通信数据,也会引起数据的不一致性,破坏事务特性。

  2. TCC 更复杂的实现。无法同时满足游戏中频繁的交易行事务和低延时的需求。

  3. 基于消息队列的实现 需要引入消息中间件。目前游戏有一套成熟的消息RPC通信机制,许多业务和功能基于其上搭建,如果引入改动较大。

现在主要问题是进程间通信协议可能会丢失,无法确定对方是否已经执行过这个协议的处理,漏处理或者多处理都会打破事务性。一个方法是为每个这样的消息协议分配一个唯一的id,消息发送方生成带唯一id的消息存盘然后发往消息接收方处理,接收方处理并存下消息id,表示已经处理过。双方定时同步各自的消息id列表,如果双方都有的,代表处理过,可以删除释放存储空间。网络断线重连成功后,第一步先同步消息id列表,发送方发现接收发没有的消息需要重新重发。这种方法依赖消息处理是幂等的,就是无论什么时候执行都能是正确的。

还是以上面的例子为例,买主发起购买,在买主game server扣除玩家金币,生成购买消息协议和唯一id存盘,这两步通过本地事务保证。然后购买消息协议发往zone server,zone server判断货物存在,如果存在则扣除货物,存下购买消息协议的id,表示这个消息已经处理过,再生成购买回应消息协议和唯一id,通知买主game server购买成功,往后就是重复消息发送和消息处理过程,直到整个分布式事务完成或者失败回滚。整个分布式事务是有低概率在中途失败的,所以每一步都要支持回滚,回滚函数也必须是幂等的。

这一套机制在原来的不保证事务的跨服实现上进行简单改动,就刚好能契合游戏跨服玩法分布式事务的需求,比如拍卖行,黑市等交易系统都适用这套机制。