莉莉丝服务器开发秋招凉经:游戏服务端的并发与一致性
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 介绍下自己,重点讲讲实习做了什么。
- 游戏房间采用单线程 Tick 驱动,为什么 CPU 利用率不高,玩家仍然会遇到明显卡顿?
- 拍卖系统中,为什么不能把拍卖、背包和钱包全部放进同一个聚合来保证一致性?
- 拍卖截止前收到的出价,因服务器排队而在截止后才执行,应判有效还是无效?如何防止不同节点给出相反结论?
- 使用编码 Agent 修改长期维护的服务时,哪些文档应作为稳定约束,哪些内容应随代码一起更新?
- 多个线程分别更新数组中不同位置的计数器,为什么线程越多,吞吐量反而越低?
- 三个编码 Agent 同时修改协议层、业务层和测试层,怎样避免每个分支都通过测试,合并后却无法运行?
《参考解析》
单线程 Tick 卡顿但 CPU 不高:CPU 利用率是跨核心、跨时间的平均值,掩盖了单次 Tick 的瞬时阻塞。常见的元凶是同步日志、同步数据库/网络调用、锁竞争、以及集中在同一帧到期的定时任务。排查要分两层:一是记录 Tick 耗时分布(P50/P95/P99)和调度等待时间、消息队列长度、各阶段耗时,找出超预算的那一帧;二是用 JFR 或火焰图区分线程是在运行、等待锁还是等待 I/O。改造上,异步任务返回后不能直接修改房间状态——必须携带房间代次和状态版本,重新投递到所属线程执行;出现积压时限制补帧次数并削减非关键工作,不能无限追赶历史 Tick,否则会形成持续过载。
为什么不能合并成一个聚合:聚合表达的是「必须同步维护的不变量」,不是业务实体之间的全部关联。把拍卖、背包、钱包塞在一起,会让一次出价同时竞争公会、玩家钱包和背包的写入权限,聚合还会随参与人数无限增长、无法分片。正确的边界划分是:拍卖聚合负责出价有效性与结算状态,钱包负责余额与冻结金额约束,背包负责物品归属;跨边界操作通过冻结凭证、结算单和业务状态推进来衔接。拆分后必须明确每个中间状态允许发生什么、失败时资金和物品如何释放或继续交付——不能假装还存在单机事务。
截止前出价排队到截止后:先确定业务上的时间和顺序权威。如果规则是「以拍卖所属分区接受命令的时间为准」,就由该分区排序并记录出价事件,关闭拍卖也走同一有序执行路径;网关收到请求不等于拍卖已经接受。如果要以网关接收时间为准,就必须处理时钟误差、迟到消息和「何时确认不会再有更早请求」的问题,复杂度显著上升。实践中通常明确「截止前被权威节点接受才有效」,并让客户端区分已提交与已接受。主节点切换必须延续已提交日志,不能让两个主节点各自接受出价后再比较时间戳——那样不同节点必然给出相反结论。
哪些文档是稳定约束:稳定约束指资产守恒规则、协议兼容原则、模块依赖边界——注意「稳定」不等于永不修改,而是修改需要显式决策。接口契约、数据库结构、错误码和部署配置应跟随实现更新,否则代码已经变了、Agent 还在按旧文档干活。架构决策记录要保留当时的背景,后续用新记录替代旧决策,而不是覆盖历史。最关键的是把约束转成可执行检查——禁止跨模块直接访问数据表、校验协议兼容性——仅靠一份很长的说明文档,无法保证 Agent 每次都遵守。
多线程更新不同位置反而变慢:逻辑上不同的变量可能落在同一条缓存行,多个核心写入时反复争夺缓存行所有权,这就是伪共享。即使没有 Java 锁,也会产生大量缓存一致性流量。缓解手段:线程私有统计后再汇总、分散热点、或经过验证的填充布局(@Contended);LongAdder 适合高竞争统计,但它的 sum() 不是并发更新下的线性一致快照,不能直接当资产余额或严格限额判断用。诊断时要先排除真实共享、内存带宽瓶颈和基准测试误差,再结合硬件计数器验证——不能看到多线程变慢就一口咬定伪共享。
多个 Agent 并行改不同层:先固定跨模块契约(字段语义、错误处理、兼容范围),再让各分支基于同一版本开发。独立工作区只能避免文件互相覆盖,解决不了语义冲突。协议层先提供契约测试和测试替身,业务层与测试层据此并行推进;最终门禁必须针对真实合并提交执行,而不是把各分支的绿色结果简单汇总。涉及公共数据结构或数据库迁移时,要限制同时修改的范围;契约一旦变化,必须让依赖分支重新验证。