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

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
WordPress大学
WordPress大学
T
Tailwind CSS Blog
V
Visual Studio Blog
月光博客
月光博客
Hugging Face - Blog
Hugging Face - Blog
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - Franky
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Last Week in AI
Last Week in AI
阮一峰的网络日志
阮一峰的网络日志
量子位
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
Jina AI
Jina AI
雷峰网
雷峰网
博客园 - 【当耐特】
博客园 - 叶小钗
美团技术团队
宝玉的分享
宝玉的分享
IT之家
IT之家

重归混沌的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 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的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性能分析器
谈谈游戏服务器代码抽象
重归混沌 · 2024-10-04 · via 重归混沌的BLOG

在我刚开始写代码时,许多书籍不断教导我遵循正交性DRY原则(Don’t Repeat Yourself)和SPOT原则(Single Point of Truth)等编程准则。

这些原则确实能够显著提高代码质量和可维护性,我也乐此不疲地在日常开发中应用这些原则。

然而,当我转向编写游戏服务器代码时,却发现自己常常难以严格执行这些原则。

在游戏服务器的开发中,各个模块通常独立存储它们所需的数据。

用关系型数据库的术语来说,每个模块可能会使用独立的table。不过,游戏服务器的存储方式通常并不是按字段构建table,而是将结构体(struct)序列化为blob格式后进行存储。

模块之间很少通过外键进行约束,数据一致性和容错性完全依赖于代码的逻辑处理。

通常,模块内部负责将数据存储到数据库。如果多个模块共享一个table,由于各模块落地时机问题,如果不妥善处理,会大大增加数据不一致的风险。

此外,加上强类型语言的约束以及各模块之间相似却微妙的逻辑差异,迫使开发者反复编写相似但不完全相同的代码。

这些年,我尝试了不同的方法来增加代码的复用。

  1. 抽象中间结构:当我们需要使用某段代码时,可以将DB结构转换成抽象的内存结构,处理完业务逻辑后再将内存结构转换回DB结构进行存储。
  2. 复用设计好的DB结构:在设计模块数据结构时,复用一些通用的DB结构,并直接调用相同的算法代码。

然而,这两种方式都有很大的局限性。

例如,方式1可能会转换1000个数据,但最终只修改其中的1个数据。不进行转换是不行的,因为修改一个元素时可能会读取其他数据。一个典型的例子就是抽卡。

方式2的复用性更差,举例说明:

struct common {
    // 一些公共字段
}
struct entry_a {
  struct common c;
  // 其他字段
}
struct entry_b {
  struct common c;
  // 其他字段
}
struct module_a {
  struct entry_a[] list;
}
struct module_b {
  struct entry_b[] list;
}

由于强类型语言的限制,几乎不可能直接用相同的代码同时处理module_a.listmodule_b.list

现实中的情况更复杂,比如entry_?.other_fields往往会以某种方式影响struct common[] list的逻辑。


在我以前的游戏服务器开发中,我一直被数据库落地给禁锢了。

我一直认为数据库落地的不一致是无法解决的,因此也就不可能抽象出公共模块来存储并处理不同模块中相同的逻辑。

这就导致了,在进行代码设计时,无法自由的进行抽象。从而在方式1方式2之间徘徊。

直到最近我终于找到了部分答案。

解决方案其实也很简单:模块不再负责数据的落地,而是通知落地框架需要落地的数据。

落地框架将在合适的时机(如每秒, 或每条协议之后)统一处理所有脏数据的落地。

如果在落地之前服务器崩溃,丢失的也仅仅是最后一秒的修改,相当于游戏回档到1秒前。

这对玩家几乎没有影响(充值问题可以通过掉单处理,充值服务器只需重新补发1秒前的订单即可)。

为强类型语言的游戏服务器设计这个落地框架会比较麻烦,但收益是巨大的。

从此在进行数据库抽象时,我们不必再有数据库的心智负担。可以像普通应用程序那样进行自由的抽象。