惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Project Zero
Project Zero
D
DataBreaches.Net
博客园_首页
罗磊的独立博客
Last Week in AI
Last Week in AI
博客园 - 【当耐特】
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Forbes - Security
Forbes - Security
Attack and Defense Labs
Attack and Defense Labs
S
Secure Thoughts
雷峰网
雷峰网
Jina AI
Jina AI
O
OpenAI News
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 三生石上(FineUI控件)
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
SegmentFault 最新的问题
V
Visual Studio Blog
Webroot Blog
Webroot Blog
GbyAI
GbyAI
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
腾讯CDC
IT之家
IT之家
C
Cyber Attacks, Cyber Crime and Cyber Security
Y
Y Combinator Blog
T
The Blog of Author Tim Ferriss
T
Troy Hunt's Blog
博客园 - 叶小钗
N
News and Events Feed by Topic
B
Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 司徒正美
C
Check Point Blog
T
Threatpost
SecWiki News
SecWiki News
爱范儿
爱范儿
阮一峰的网络日志
阮一峰的网络日志
P
Privacy International News Feed
J
Java Code Geeks
L
LINUX DO - 最新话题
PCI Perspectives
PCI Perspectives
T
Tailwind CSS Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
Cisco Blogs
S
Schneier on Security

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG
重归混沌 · 2025-07-10 · via 重归混沌的BLOG

在我所见过的常规跨服玩法中(跨服相关数据由跨服进程管理),通常或多或少都会包含以下几种典型的工作模式:

  1. 跨服在特定时机(如玩家某个操作之后、每日 0 点、赛季结束)需要根据当前状态进行结算,并向游戏服发放奖励。
  2. 玩家的请求在游戏服处理后得到一个结果,该结果需要同步到跨服(一个非常典型的例子是跨服排行榜)。
  3. 玩家在执行某个跨服操作(比如攻击玩家)成功时,需要从背包中扣除相关资源(战斗次数、体力、道具等)。

对于模式 1 和 2,实现数据一致性虽然需要花点功夫,但并不算困难。

只要做好消息重发机制,游戏服加上去重逻辑,基本就能实现。

而对于模式 3,我曾经很长一段时间都没有找到比较好的解法,因为它确实非常复杂。

下面来看一下,当游戏服收到玩家请求后,常见的两种处理策略:

  • 策略一:先扣资源,再请求跨服处理。这种方式面临以下几种情况:

    1. 跨服返回成功,游戏服也成功收到,皆大欢喜。

    2. 跨服返回失败,游戏服补偿玩家资源,也没问题。

    3. 跨服消息因各种原因(如链接断开、服务器重启等)丢失,类似 timeout 的场景:

      如果这条丢失的消息是失败的,那我们就无法对玩家进行资源补偿,导致玩家莫名承担了不应有的损失。

  • 策略二:先请求跨服处理,收到成功后再扣除资源。这同样存在问题:

    1. 在跨服消息返回之前,客户端连续发起多次请求。当跨服陆续返回成功消息时,玩家资源已不足,会形成恶意刷资源漏洞。

      这时,通常需要对该模块实现一个逻辑锁,确保所有消耗资源的操作不会并行执行。

      即便如此,如果资源是金币这类公共资源,也很难确保跨服消息返回时玩家还有足够的余额。

      因此还需要加上预转换逻辑:即在跨服处理之前先扣除玩家资源,并发放模块内临时道具,等跨服结果回来后再正式扣除这些道具。

      这样只要有逻辑锁的存在,我们就可以保证,玩家在跨服消息返回之后,可以成功扣除资源。

    2. 如果跨服消息最终丢失了,我们也失去了扣除资源的机会。

尽管以上两种策略都有缺陷,我们通常还是倾向于采用先请求跨服处理,再扣除资源的方式。

如果跨服消息的丢失是我们自身系统的问题,那就当这次操作白送了。只要这个策略没有被恶意利用的漏洞,仍在可接受范围内。

在我过去的一些分布式事务相关的文章评论区里,经常有人推荐我使用 消息队列中间件。但我认为它并不适用于这个场景。

消息队列中间件的主要作用是削峰填谷,其代价是消息延迟显著增加。在 Web 场景里延迟几秒可能没什么,但在游戏中,即便是卡牌游戏,几秒也难以接受。

而且游戏服务器的负载本身就高度集中,比如玩家通常会在每天的 0 点、12 点、18 点、22 点等时间段集中上线,刷怪、打 Boss、做任务等操作。

本着“良好抽象可以减少开发工作量、降低 Bug 数量”的原则,我对上述三种模式做了归纳,并抽象成了三个通用组件(它们都是在原有的 TCP 链接上进行,不会单独建立新的链接, 这也便于我们充分利用 TCP 的 FIFO 特性)。


先来看看模式 1的场景。

在这种场景下,奖励是由跨服主动发起的,而不是由玩家触发。既然不是玩家主动操作,那就算有点延迟,玩家也是感知不到的。

更关键的是,这类消息往往比较单纯,与功能内的其他消息不存在顺序相关性。

因此我借鉴了 消息队列中间件 的思路,设计了一个叫做 Mq 的组件。

Mq 会保证消息按顺序送达 —— 即必须在收到 Msg1回包之后,才会开始发送 Msg2

当然,Mq 只能负责重发顺序性,它无法确保消息只送达一次。

因此,在游戏服这边,我们需要针对不同的协议自行实现去重逻辑

好在由于 Mq 保证了消息的顺序性,我们只需要在消息中增加一个 ID 字段,并确保 ID 单调递增,就可以轻松实现去重。

当然,我们也可以在游戏服中实现一个通用的 消息去重 组件,进一步减轻业务层的负担。但目前我还没想到一个真正通用且完美的方案。


不同于模式 1 的维度是“服务器”,模式 2 的维度是“玩家”

同一条客户端消息(如 pve_battle),玩家 1 的整个请求链可以与玩家 2 的请求链交错执行。

如果此时仍然使用 Mq 组件,就会出现玩家 1 的请求执行速度影响玩家 2 的响应速度的情况。最终可能导致:CPU 负载很低,但响应延迟却很高

这类消息大多是由玩家在客户端主动触发,因此他们对处理延迟非常敏感。你玩游戏的时候也不希望点一下按钮后,要等 3 秒才看到结果吧 ^_^!

针对这种场景,我抽象并实现了 UserSync 组件。

Mq 不同,UserSync 对每个玩家的每个 cmd 仅保留最新的一条消息内容

来看一个最简单的时序示例:

  1. 玩家 1 向游戏服发送 pve_battle 请求,游戏服计算后认为积分为 100,向跨服排行榜发送消息:
    sync_rank{uid:1, score:100}

  2. 此时跨服链接断开,游戏服没有收到回包。

  3. 玩家 1 继续进行下一场 pve_battle,积分变为 200,游戏服再次发送:
    sync_rank{uid:1, score:200}

  4. 玩家 1 再次进行战斗,积分变为 300,游戏服发送:
    sync_rank{uid:1, score:300}

  5. 当跨服链接恢复时,UserSync只重试最新的一条消息
    sync_rank{uid:1, score:300},忽略之前的历史消息。

之所以这样设计,是因为这种消息既不能丢,又必须做到尽可能低延迟

因此在调用 UserSync.Send(uid, msg) 时,UserSync立即发送消息给跨服,并更新玩家当前消息内容

这个“立即”非常重要。根据我过往的实践经验,在打完 pve 战斗后,客户端的回包中通常需要包含当前排行榜排名

也就是说,在我调用完:UserSync.Send(uid, sync_rank{uid:1, score:300})之后,往往会立即跟一个 query_myrank 请求,用于查询当前排名。

这个排名必须是在我更新积分之后的排名

由于 UserSync 总是在第一时间尝试发送消息,而且 TCP 链接具有 FIFO(先进先出)特性,在一切正常的情况下,query_myrank 总是会返回最新积分更新之后的排名

即便遇到链接异常,也能保证:玩家的积分更新不会丢失


场景 3 是最复杂的,它通常发生在跨服 PVP 类玩法中,本质上是一个分布式事务,事务的两端分别是游戏服与跨服。

事实上,我已经被这个场景困扰了很多年。我曾多次尝试借鉴 Saga 模式 来解决,但都以失败告终。

直到最近,我才突然意识到:借助 MqUserSync 两个组件,其实可以在保持较低复杂度的前提下,解决跨服扣资源的问题。


当玩家发起一场跨服 PVP 战斗时,流程如下:

  1. 游戏服先扣除资源,然后生成结构:

    consume{uuid: <递增的 ID>, resource: <扣除的资源>}
  2. consume 嵌入到 pvp_battle 消息中,通过 UserSync 发送给跨服。

    由于 UserSync 对同一个玩家、同一个 cmd 只保留最新消息,在游戏服执行资源扣除之前,必须先检查该 cmd 是否还有未完成的同步。

    如果有,就需提示玩家稍后再试 —— 这个逻辑本质上就是一个业务逻辑锁

跨服收到pvp_battle 消息后,流程如下:

  1. 对消息进行去重。

  2. 然后根据当前跨服状态进行计算。

    • 如果一切正常:pvp_battle 中的 consume 就可以直接忽略。

    • 如果出现异常(例如对手已死亡、活动已过期等),跨服可以生成如下结构,并通过Mq发送回游戏服:

      consume_rollback{consume: consume}

游戏服收到 consume_rollback 消息后,流程如下:

  1. 根据其中的 consume.uuid 执行去重处理。

  2. consume.resource 中的资源返还给玩家。

至此,我们实现了一个最终一致性的分布式柔性事务。

一些旁支末节:

  • consume_rollback 的处理逻辑在游戏服只需要实现一次,之后所有跨服玩法都可以共用这一套流程。

  • 关于去重逻辑,其实有一个小技巧:

    只要每个服务器的 consume.uuid 是连续递增的,我们就可以下面结构来保存所有已处理的 uuid

    {start: int, end: int, fragments: array}

    由于这些消息都有时间相关性,最终一致达成之后,fragments 数组的体积一般不会太大。

相信这三种模式基本可以覆盖绝大多数跨服业务场景了。

ps. MqUserSync中的数据都会进行数据库落地。