米哈游 服务器开发方向 C++ 二面 面经
1. 自我介绍,介绍一下你做过的最有挑战性的项目
这道题考察的是表达能力和技术深度,回答框架建议用 STAR 法则:
答:我参与过一个游戏服务器的高并发架构重构项目。背景是原有服务器在日活 50 万时出现明显卡顿,延迟峰值达到 800ms。我负责的部分是网络层和消息队列的重新设计。
具体做了几件事:① 将原来的 select 模型换成 epoll ET 模式,配合非阻塞 IO,连接数从 1 万提升到 10 万;② 引入无锁 SPSC 队列替换原来加互斥锁的消息队列,热路径延迟降低了约 40%;③ 用内存池管理频繁分配的消息对象,减少了 GC 压力和内存碎片。最终上线后延迟峰值降到 120ms 以内,服务器 CPU 利用率下降了 25%。
挑战在于:无锁队列的内存序问题非常难调试,用 ThreadSanitizer 发现了两处 data race,最终通过仔细分析 acquire/release 语义解决。
2. 项目中遇到过内存泄漏吗?你是怎么定位和解决的?
答:遇到过。定位内存泄漏的常用手段:
① Valgrind Memcheck:运行程序后报告所有未释放的堆内存,精确到分配的调用栈。命令:valgrind --leak-check=full ./server,缺点是运行速度慢约 10-20 倍,适合测试环境。
② AddressSanitizer(ASan):编译时加 -fsanitize=address,运行时检测内存越界、use-after-free、内存泄漏,速度比 Valgrind 快很多,适合 CI 集成。
③ 自定义内存追踪:重载 operator new/delete,记录每次分配的地址、大小、调用栈,程序退出时打印未释放的记录。
我项目中的一次泄漏是:玩家断线时,持有该玩家 shared_ptr 的定时器没有被取消,导致玩家对象无法释放。用 Valgrind 定位到分配点后,发现是定时器回调里捕获了 shared_ptr 的 lambda,改成捕获 weak_ptr 后解决。
3. 如何设计一个支持百万并发连接的游戏服务器架构?
答:百万并发连接不能用单机单进程,需要分层架构:
网络接入层:多台 Gateway 服务器,每台用 epoll + 协程处理 IO,只负责连接维护和消息收发,不做业务逻辑。单机 epoll 支持 10 万级连接,10 台 Gateway 即可支撑百万连接。
逻辑层:按游戏功能拆分为多个服务(场景服、战斗服、社交服、背包服等),Gateway 根据消息类型路由到对应服务。服务间通过消息队列(如 Kafka)或 RPC(如 gRPC)通信。
数据层:Redis 做热数据缓存(玩家状态、排行榜),MySQL 做持久化存储,写操作异步落库,读操作优先走缓存。
关键设计点:① 无状态 Gateway,方便水平扩展;② 玩家 session 信息存 Redis,任意 Gateway 都能处理同一玩家的消息;③ 心跳检测 + 断线重连机制;④ 消息协议用 Protobuf,序列化效率高且跨语言。
4. 分布式系统中如何保证消息不丢失、不重复?
答:这是分布式系统的经典问题,核心是幂等性 + 持久化 + 确认机制。
不丢失:① 消息发送方持久化到本地 WAL(Write-Ahead Log)再发送;② 消息队列(Kafka)配置 acks=all,确保所有副本写入才返回成功;③ 消费方处理完业务后再提交 offset,而非收到就提交。
不重复:网络重试必然导致重复投递,解决方案是幂等处理。每条消息带唯一 message_id,消费方处理前先查 Redis 是否已处理过该 id,处理完后写入 Redis(设置过期时间)。这样即使消息重复投递,业务逻辑也只执行一次。
恰好一次(Exactly Once):Kafka 0.11+ 支持事务,配合幂等 Producer 可实现端到端 Exactly Once,但性能有损耗,游戏场景通常用 At Least Once + 幂等消费即可。
5. 数据库事务的隔离级别有哪些?可重复读是如何实现的?
答:四个隔离级别(从低到高):
- 读未提交(Read Uncommitted):可读到其他事务未提交的数据,存在脏读。
- 读已提交(Read Committed):只读已提交数据,解决脏读,但存在不可重复读(同一事务两次读结果不同)。
- 可重复读(Repeatable Read):同一事务内多次读结果一致,解决不可重复读,MySQL InnoDB 默认级别。
- 串行化(Serializable):完全串行执行,解决幻读,性能最差。
可重复读的实现(InnoDB MVCC):事务开始时创建一个 Read View,记录当前活跃事务列表。后续每次读操作都用这个 Read View 判断数据版本可见性,只读取在 Read View 创建时已提交的版本。因为 Read View 不变,所以同一事务内读到的数据始终一致。
幻读问题:InnoDB 在 RR 级别下通过 Gap Lock(间隙锁)+ Next-Key Lock 防止幻读,锁住查询范围内的间隙,阻止其他事务插入新行。
6. Redis 的持久化方式有哪些?AOF 重写的原理是什么?
答:两种持久化方式:
RDB(快照):定期将内存数据序列化为二进制文件。优点:文件紧凑,恢复速度快;缺点:两次快照之间的数据可能丢失,fork 子进程时有短暂阻塞。
AOF(追加日志):每条写命令追加到 AOF 文件。优点:数据更完整,最多丢失 1 秒数据(fsync every second);缺点:文件越来越大,恢复速度慢。
AOF 重写原理:AOF 文件会随时间膨胀(同一个 key 被修改多次,历史命令都保留)。重写时 fork 子进程,子进程遍历当前内存数据,将每个 key 的当前状态用最少的命令表示(如一个 list 用一条 RPUSH 替代多条操作),写入新 AOF 文件。重写期间父进程继续处理请求,新命令同时写入旧 AOF 和重写缓冲区,重写完成后将缓冲区追加到新文件,原子替换旧文件。
7. 什么是一致性哈希?它解决了什么问题?虚节点的作用是什么?
答:普通哈希取模(key % N)的问题:增减服务器节点时,N 变化导致几乎所有 key 重新映射,缓存大面积失效,引发缓存雪崩。
一致性哈希:将哈希空间组织成一个环(0 到 2^32-1),服务器节点映射到环上的某个位置,key 也映射到环上,顺时针找到的第一个节点就是负责该 key 的节点。增减节点时只影响相邻节点的数据,其他节点不受影响。
虚节点:真实节点少时,节点在环上分布不均匀,导致负载不均衡。虚节点是每个真实节点在环上的多个映射点(如每个节点对应 150 个虚节点),使数据分布更均匀。增减节点时,该节点的多个虚节点同时变化,数据迁移更均匀,不会集中压垮某个节点。
8. C++ 中的内存模型是什么?std::atomic 的 memory_order 有哪几种?
答:C++11 引入了多线程内存模型,定义了多线程环境下内存操作的可见性和顺序保证,解决了不同 CPU 架构(x86、ARM)的指令重排问题。
六种 memory_order:
memory_order_relaxed:只保证原子性,不保证顺序,性能最好,适合计数器累加。memory_order_acquire:读操作,保证此操作之后的读写不会被重排到此操作之前,配合 release 使用。memory_order_release:写操作,保证此操作之前的读写不会被重排到此操作之后。memory_order_acq_rel:同时具有 acquire 和 release 语义,用于读-改-写操作(如 fetch_add)。memory_order_seq_cst:顺序一致性,最强保证,所有线程看到的操作顺序一致,是默认值,性能最差。
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

查看19道真题和解析