今天分享一下训练营内部朋友在B站游戏服务器开发面试的详解,
主要整理了问到的技术问题,项目介绍类问题去掉了,覆盖分布式、中间件、数据库、并发控制等知识点,大家可以参考学习一下。
核心思路:基于“事务消息+重试机制+幂等性”实现,优先选择低侵入性方案,适用于订单支付后库存、积分、日志等跨服务同步场景。
具体实现(以订单支付为例):
核心思路:解耦服务依赖、提高吞吐量,优先用“消息队列+Go 协程”组合,覆盖跨服务异步和本地异步场景。
具体设计:
sync.WaitGroup 控制协程等待(如需等待结果)或 channel 传递结果。context.WithTimeout 控制协程执行时间,避免阻塞;defer recover())、消息消费失败入死信队列,定期复盘;项目落地:游戏充值接口通过异步化改造,吞吐量从 500 QPS 提升至 3000 QPS,响应时间从 300ms 降至 50ms。
以 RocketMQ(Tag 机制)和 Kafka(Topic+Partition 二级分类)为例,核心是“** broker 端过滤+消费端订阅**”:
ORDER_PAID/ORDER_CANCELLED/ORDER_REFUNDED)。github.com/apache/rocketmq-client-go),在创建消费者时,通过 ConsumerOption 指定订阅的 Tag,格式为 Topic:Tag1||Tag2(多 Tag 用 || 分隔),Broker 会仅将匹配 Tag 的消息投递给消费者。tag 字段过滤,或直接按 Tag 拆分 Topic(如 order_paid_topic/order_cancelled_topic),更高效。Go 无内置线程池/协程池,但协程(Goroutine)轻量(初始栈 2KB),可通过 channel 手动实现协程池,核心是“控制并发数+任务调度”:
核心组件:任务队列(taskChan)、worker 协程池、并发控制(maxWorkers)、运行状态标识(running buffer,即当前活跃 worker 数)。
实现步骤(Go 代码简化):
type Task func() error
type Pool struct {
taskChan chan Task // 任务队列
maxWorkers int // 最大并发数
running int32 // 当前运行的worker数(原子变量,避免竞态)
ctx context.Context
cancel context.CancelFunc
}
// 初始化协程池
func NewPool(maxWorkers int) *Pool {
ctx, cancel := context.WithCancel(context.Background())
pool := &Pool{
taskChan: make(chan Task, 100), // 任务队列缓冲
maxWorkers: maxWorkers,
ctx: ctx,
cancel: cancel,
}
// 启动worker
for i := 0; i < maxWorkers; i++ {
go pool.worker()
}
return pool
}
// worker协程:循环消费任务
func (p *Pool) worker() {
defer atomic.AddInt32(&p.running, -1)
atomic.AddInt32(&p.running, 1)
for {
select {
case <-p.ctx.Done():
return
case task, ok := <-p.taskChan:
if !ok {
return
}
_ = task() // 执行任务
}
}
}
// 提交任务
func (p *Pool) Submit(task Task) error {
select {
case <-p.ctx.Done():
return fmt.Errorf("pool closed")
case p.taskChan <- task:
return nil
}
}
running buffer:用 atomic.Int32 维护当前运行的 worker 数,可用于监控协程池负载(如通过 Prometheus 暴露指标)。项目中用 Canal 监听 MySQL binlog,核心是“模拟 MySQL 从库同步协议,解析 binlog 并推送变更”:
github.com/alibaba/canal-go)订阅变更事件,推送至业务逻辑(如同步数据到 Redis、ES,或触发跨服务通知)。数据结构 | 核心用途 | 项目应用场景 |
|---|---|---|
String | 简单键值存储、计数器 | 存储玩家验证码(key=player:{id}:code)、游戏在线人数计数(INCR/DECR) |
Hash | 复杂对象存储(字段-值映射) | 存储玩家信息(key=player:{id},field=name/level/gold)、商品属性 |
List | 队列、栈、消息列表 | 游戏公告队列(LPUSH/RPOP)、玩家邮件列表 |
Set | 去重、交集/并集运算 | 玩家好友关系(SADD/SISMEMBER)、抽奖活动去重(避免重复中奖) |
Sorted Set | 有序排序、排行榜 | 游戏战力排行榜(ZADD/ZRANGE)、限时活动积分排名 |
Bitmap | 位运算、布尔值存储 | 玩家签到记录(key=sign:{date},bit=playerID,1=已签到) |
Geo | 地理位置计算 | 游戏附近玩家查找(GEORADIUS) |
进阶用法:Hash 用 HSCAN 避免大 key 阻塞、Sorted Set 用 ZREMRANGEBYRANK 维护TopN排行榜、String 用 SETEX 实现过期缓存。
ETCD 是分布式键值存储(基于 Raft 协议),核心作用是“分布式一致性保障”,项目中主要用于 3 个场景:
etcd/clientv3 订阅配置变更。优势:强一致性、高可用(集群部署)、轻量、支持 TTL 过期键。
核心思路:水平分片(按玩家 ID 哈希分片),目标是分散数据压力、提升查询效率,适配百万级玩家数据存储:
hash(playerID) % 100)得到库索引,hash(playerID) / 100 % 100 得到表索引,最终路由到 db{库索引}.t_player_{表索引}。shardingsphere-go),透明化分库分表逻辑(业务代码无需关注分片规则,直接操作逻辑表)。项目落地:游戏玩家中心存储 500 万玩家数据,分 100 库 100 表,单表数据量控制在 5000 以内,查询响应时间稳定在 10ms 内。
压测工具:用 k6(Go 编写,高并发支持)+ Prometheus+Grafana 监控指标(QPS、响应时间、CPU/内存/网络),遇到的核心瓶颈及解决方案:
瓶颈类型 | 现象 | 排查方式 | 解决方案 |
|---|---|---|---|
数据库慢查询 | 接口响应时间>500ms,MySQL CPU 100% | EXPLAIN 分析 SQL,慢查询日志 | 1. 给订单表添加联合索引(player_id+create_time);2. 分页查询优化(用游标代替 limit offset);3. 读写分离(读请求路由到从库) |
Redis 缓存穿透 | 大量请求穿透到数据库,Redis 命中率<80% | Redis 监控面板查看命中率 | 1. 无效 key 缓存空值(SETEX key 3600 "");2. 布隆过滤器(RedisBloom)过滤不存在的玩家 ID |
协程泄露 | 内存持续增长,协程数>10w | pprof 分析 goroutine 栈 | 1. 协程池控制并发数(maxWorkers=100);2. 用 context.WithTimeout 控制协程生命周期,避免无限阻塞 |
网络瓶颈 | 跨服务调用延迟>200ms | tcpdump 抓包,链路追踪(Jaeger) | 1. 服务本地缓存热点数据(如活动配置);2. gRPC 连接池优化(复用连接,减少握手开销) |
优化结果:接口 QPS 从 800 提升至 5000,响应时间稳定在 50-80ms,CPU 使用率控制在 70% 以内。
从“索引、SQL、配置、架构”四层优化,结合项目实践:
select id, amount from t_order where player_id=? and status=? order by create_time desc),避免回表。or(用 union 代替)、子查询转 join;核心是“监控+日志+执行计划”三位一体,步骤如下:
slow_query_log=1,long_query_time=1),捕获执行时间>1s 的 SQL,定期分析日志(用 pt-query-digest 工具汇总)。EXPLAIN 分析,重点看 type(索引类型,如 ref、range 优于 all)、key(是否使用索引)、rows(扫描行数,越少越好)、Extra(是否 Using filesort/Using temporary,需优化)。slow_queries 慢查询数、innodb_rows_read 扫描行数、Threads_running 运行线程数),设置阈值告警(如慢查询数>10 触发告警)。sqlx 拦截器),当接口响应变慢时,直接定位到耗时 SQL。项目案例:通过慢查询日志发现“玩家累计充值金额查询”SQL 未走索引,扫描全表(rows=50w),用 EXPLAIN 分析后,给 player_id 建索引,查询时间从 1.2s 降至 8ms。
核心思路:快速定位故障范围→精准排查根因→临时止损→永久修复,步骤如下:
基于“分布式日志架构+链路追踪”,实现日志快速定位,架构:ELK(Elasticsearch+Logstash+Kibana)+ 链路追踪(Jaeger):
traceID(链路追踪 ID)、spanID、serviceName(服务名)、instanceIP(节点 IP)、playerID(玩家 ID)、time(时间戳)、level(日志级别)、msg(日志内容)、stack(错误堆栈)。traceID,确保同一请求的所有日志都携带相同 traceID。traceID(从前端或玩家提供的报错信息中提取);traceID:xxx 过滤,获取该请求的所有日志(从网关→业务服→依赖服务);traceID 的调用链路,确认超时环节(如微信支付接口响应时间>3s)。项目落地:通过该方案,将日志定位时间从 30 分钟缩短至 5 分钟,大幅提升故障排查效率。
核心是“并发控制+原子操作”,基于 Redis+MySQL 实现双重保障:
lock:goods:{goodsID}(同一商品共享一把锁);SET lock:goods:123 1 EX 10 NX);GET goods:stock:123),库存不足则返回“商品已售罄”;DECR goods:stock:123),释放锁(DEL lock:goods:123);select stock from t_goods where id=? for update),确保最终库存一致。version 字段;update t_goods set stock=stock-1, version=version+1 where id=? and stock>=1 and version=?;项目落地:游戏限时抢购活动用方案一,支持 1w+ QPS 并发下单,超买超卖率为 0,库存一致性 100%。
核心是“幂等性设计”,结合业务场景选择以下方案:
EXISTS request:id:{requestID},存在则返回“操作已执行”,不存在则执行业务逻辑;SET request:id:{requestID} 1 EX 3600),过期时间设为业务操作有效时间。UNIQUE KEY uk_player_goods (player_id, goods_id, activity_id)(同一玩家同一活动同一商品只能创建一次订单);Duplicate key error,服务端捕获后返回“操作已执行”。lock:player:{playerID}:goods:{goodsID}(同一玩家同一商品的操作共享一把锁);项目落地:游戏道具发放用“方案一+方案二”,既通过请求 ID 快速拦截重复请求,又通过数据库唯一索引兜底,确保无二次发货。
支付回调是支付平台(微信/支付宝)向商户服务器发送的异步支付结果通知,核心流程:“签名验证+幂等处理+订单更新+响应确认”:
<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>),否则支付平台会阶梯式重试(如 15s/30s/1min/2min/5min/10min/30min/1h/2h/6h/15h,共 11 次)。核心是“Redis 原子操作快速拦截+MySQL 唯一索引兜底”,无需额外中间件:
DEL lock:player:{playerID}:goods:{goodsID} 释放锁(或等待自动过期)。uk_player_goods (player_id, goods_id),确保同一玩家同一商品只能创建一次订单;insert into t_order (player_id, goods_id, amount, status) values (?, ?, ?, ?);Duplicate key error,则返回“操作已执行”。SETNX lock:player:{playerID}:goods:{goodsID} 1 EX 30(30s 过期,避免死锁);项目落地:游戏内玩家购买月卡场景用该方案,支持 5k+ QPS 并发请求,无重复购买、重复发货问题。