最近碰到的一个分布式一致性问题

在之前的游戏设计中,好友功能只在同一服务器内生效,涉及到以下几个操作:

  • A玩家想加B玩家为好友,首先需要向B玩家发出好友申请
  • B玩家可以选择删除申请,也可以同意。一旦同意,AB的好友列表中都会同时显示对方。
  • 成为好友后,A每天可以向B送花,AB的关系会因此增加友情值。当B收到A的花时,双方的友情值也会增加。每日送花的次数会在0点重置,但累计的友情值不会清空。
  • 根据友情值的累积,AB可以领取不同档次的奖励,每个档次的奖励只能领取一次。

最近策划提出了一个新需求,要求去掉服务器之间的限制,使所有服务器的玩家都可以互加好友,且在操作上与同服好友无异。

面对这个需求,我有两个选择:

  1. 单独做一个服务,将所有好友关系集中管理。这样可以避免数据一致性问题。
  2. 扩展现有的本服好友系统,将跨服好友纳入其中,但会涉及到同步问题。

虽然单独做一个服务能够较轻松地解决数据一致性问题,但也带来了新的挑战:

  • 其他模块可能依赖好友关系,如果把好友关系放在一个单独的服务中,依赖此关系的业务需要异步查询,增加了不确定性。
  • 当玩家数量和在线人数达到一定规模时,如果需要进行水平扩展,数据一致性问题仍然会重新浮现。
  • 由于游戏已上线,如果跨服好友和本服好友分开存储,两个系统的融合也会带来不小的复杂度。

因此,我最终选择了扩展本服好友系统,让它能够接管所有好友关系,而不仅仅是本服好友。毕竟,“复杂度留给自己,为他人提供简洁的接口”才是最佳选择。


好友关系是强双向的,因此将好友关系存储在各自玩家的服务器中必然涉及数据一致性问题。

在处理好友申请同意申请时问题不大,这些操作可以很容易地设计为幂等操作。比如,B同意A的好友申请时,可以先向A所在服务器添加好友关系,成功后再从B的申请列表中删除关于A的申请。即便在中途协议超时,也可以通过重试机制来解决。

然而,送花收花友情值的处理让我陷入了困境。

因为送花只能每天进行一次,而收花必须在送花之后才能进行。这两个操作都和每日的0点相关联,意味着送花操作无法保证幂等。

不过这也不是很重要,因为其最终的导向是友情值,而友情值才和奖励挂勾。

我最初的想法是,送花收花完成时,先在本地计算出最新的友情值,然后将这个值同步给好友的服务器。

但我很快发现这个策略存在问题:AB送的花会增加AB之间的友情值,而BA的花也会增加同样的友情值。如果双方同时进行送花收花操作,友情值可能会被相互覆盖。


这就像是数据库的双主机制,问题开始变得棘手。

经过与同事讨论。他建议不再进行数据同步,当需要使用友情值时,直接从对方服务器获取对方操作产生的友情值,然后与本地的友情值相加。

我很快否定了这个方案。

因为玩家登录时,客户端需要拉取好友列表进行红点提示或其他展示。

如果按照此方案,我必须查询所有好友所在的服务器。

最坏的情况是,有50个好友分别在50个不同的服务器,这会导致协议请求数量放大50倍。

尤其是停服维护再开服时,这个瞬间的消息量是不可接受的。

这些年我刻意对抗性能强迫症时养成了一个习惯,就算一个方案在性能上不可行,我也会忽略性能来思考他的结果究竟是不是对的,有没有可能达成更简洁的抽象。

经过仔细的思考之后发现,虽然性能问题让这个方案行不通,但它揭示了友情值机制的底层逻辑。

通过简化,我们可以将操作归纳为以下公式:假设玩家AB友情值F

  • A一次送花产生的友情值为a1,一次收花产生的友情值为a2
  • B一次送花产生的友情值为b1,一次收花产生的友情值为b2

那么,F的最终值就是a1 + a2 + b1 + b2

从公式中不难看出,F是由AB各自操作产生的友情值之和。

因此,我们无需在用到友情值时查询对方服务器,而是将友情值拆分为F_AF_B,在需要时将F视为F_A + F_B

这样,A送花时可以本地更新F_A,并尝试将F_A同步到B所在的服务器。B的操作也是如此。

这解决了双主问题,两人分别同步自己所有权的数据,即使同时操作也不会产生冲突。


ps. 这篇博客之所以值得一写,是因为在扩展已有代码时,很容易陷入旧有的思维框架,难以跳脱出来。当发现数据所有权冲突时,如果我们仔细思考一下,这块数据中,到底哪部分该由管理,也许就可以解决双主问题。

pps. 解决完所有权问题,之后的解决方案又回到了我之前提到的本地事务 + 重试的分布式编程范式。

ppps. 在游戏分布式编程中,强一致性几乎是不可能的,最终一致性才是我们追求的目标。

发表评论

89 + = ninety nine