写点什么

B 站服务器开发一二面

作者:王中阳Go
  • 2025-11-27
    北京
  • 本文字数:7502 字

    阅读完需:约 25 分钟

B站服务器开发一二面

今天分享一下训练营内部朋友在 B 站游戏服务器开发面试的详解,


主要整理了问到的技术问题,项目介绍类问题去掉了,覆盖分布式、中间件、数据库、并发控制等知识点,大家可以参考学习一下。

一面

1. 项目最终一致性的设计思路

核心思路:基于“事务消息+重试机制+幂等性”实现,优先选择低侵入性方案,适用于订单支付后库存、积分、日志等跨服务同步场景。


具体实现(以订单支付为例):


  1. 本地事务与消息发送原子性:使用“本地消息表+定时任务”或 RocketMQ 事务消息。比如用 RocketMQ 时,先执行本地订单更新(状态改为“待支付”→“已支付”),成功后提交事务消息,失败则回滚本地事务。

  2. 消息消费与重试:下游服务(库存、积分)订阅事务消息,消费成功则更新自身状态,失败则触发 MQ 重试(阶梯式重试:10s/30s/5min,避免瞬时故障)。

  3. 幂等性保障:每个消息携带唯一 ID(如订单号+流水号),下游服务消费前先查“消息消费记录表”,已消费则直接返回成功,未消费则执行逻辑。

  4. 最终兜底:定时任务扫描“未同步成功”的订单,主动触发补偿逻辑(如调用库存服务接口重试),确保最终所有服务状态一致。

2. 项目异步设计的思路

核心思路:解耦服务依赖、提高吞吐量,优先用“消息队列+Go 协程”组合,覆盖跨服务异步和本地异步场景。


具体设计:


  1. 跨服务异步(解耦):用 Kafka/RocketMQ 做异步通信,比如用户注册后,同步发送“注册成功”消息,下游服务(短信、邮件、日志)订阅消费,主流程无需等待。

  2. 本地异步(提效):用 Go 协程处理无依赖的本地任务,比如订单创建后,启动协程异步生成订单快照、记录操作日志,通过 sync.WaitGroup 控制协程等待(如需等待结果)或 channel 传递结果。

  3. 关键保障:

  4. 幂等性:同最终一致性的消息 ID 校验;

  5. 超时处理:用 context.WithTimeout 控制协程执行时间,避免阻塞;

  6. 错误处理:协程 panic 捕获(defer recover())、消息消费失败入死信队列,定期复盘;

  7. 结果回调:如需同步异步结果,用“回调函数+channel”或“状态轮询”(如前端轮询订单支付状态)。


项目落地:游戏充值接口通过异步化改造,吞吐量从 500 QPS 提升至 3000 QPS,响应时间从 300ms 降至 50ms。

3. 消息队列怎么消费不同标签的信息

以 RocketMQ(Tag 机制)和 Kafka(Topic+Partition 二级分类)为例,核心是“** broker 端过滤+消费端订阅**”:


  1. 标签(Tag)设计:Tag 是消息的二级分类,基于业务场景划分(如订单消息:ORDER_PAID/ORDER_CANCELLED/ORDER_REFUNDED)。

  2. 消费端订阅逻辑(Go 实现):

  3. RocketMQ:使用 Go SDK(如 github.com/apache/rocketmq-client-go),在创建消费者时,通过 ConsumerOption 指定订阅的 Tag,格式为 Topic:Tag1||Tag2(多 Tag 用 || 分隔),Broker 会仅将匹配 Tag 的消息投递给消费者。

  4. Kafka:无原生 Tag,但可通过“Topic+消息头”模拟,消费端读取消息头中的 tag 字段过滤,或直接按 Tag 拆分 Topic(如 order_paid_topic/order_cancelled_topic),更高效。

  5. 优势:Broker 端过滤减少无效消息传输,提升消费效率;消费端可灵活订阅所需 Tag,实现业务解耦。

4. Golang 的线程池、协程池的使用?比如 running buffer

Go 无内置线程池/协程池,但协程(Goroutine)轻量(初始栈 2KB),可通过 channel 手动实现协程池,核心是“控制并发数+任务调度”:

(1)协程池核心设计
  • 核心组件:任务队列(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 } }
复制代码
(2)关键概念与使用场景
  • running buffer:用 atomic.Int32 维护当前运行的 worker 数,可用于监控协程池负载(如通过 Prometheus 暴露指标)。

  • 使用场景:高并发 I/O 操作(如批量调用第三方接口、数据库批量写入)、避免无限制创建协程导致的内存溢出。

  • 注意点:任务队列需设置缓冲(避免提交任务阻塞)、worker 优雅退出(通过 context 控制)、错误处理(任务执行失败需记录日志或重试)。

5. 用的什么中间件监听数据库 binlog

项目中用 Canal 监听 MySQL binlog,核心是“模拟 MySQL 从库同步协议,解析 binlog 并推送变更”:


  1. 工作流程:

  2. Canal 伪装成 MySQL 从库,向主库发送 dump 命令,获取 binlog 日志;

  3. 解析 binlog(支持 row 格式,记录具体数据变更),提取表名、操作类型(insert/update/delete)、变更前后数据;

  4. 通过 Canal Client(Go SDK:github.com/alibaba/canal-go)订阅变更事件,推送至业务逻辑(如同步数据到 Redis、ES,或触发跨服务通知)。

  5. 优势:轻量、低侵入(无需修改业务代码)、支持高可用部署(Canal Server 集群)。

6. Redis 常用的数据结构


进阶用法:Hash 用 HSCAN 避免大 key 阻塞、Sorted Set 用 ZREMRANGEBYRANK 维护 TopN 排行榜、String 用 SETEX 实现过期缓存。

7. ETCD 的作用

ETCD 是分布式键值存储(基于 Raft 协议),核心作用是“分布式一致性保障”,项目中主要用于 3 个场景:


  1. 服务注册与发现:微服务(如游戏网关、战斗服、道具服)启动时向 ETCD 注册(key=/services/{serviceName}/{instanceID},value=服务地址+元数据),客户端通过 ETCD 的 Watch 机制监听服务变更,动态获取可用实例(配合 gRPC 负载均衡)。

  2. 配置中心:存储全局配置(如数据库连接池大小、活动开关、限流阈值),通过 Watch 机制实现配置动态更新(无需重启服务),Go 中用 etcd/clientv3 订阅配置变更。

  3. 分布式锁:基于 ETCD 的 Lease(租约)+ CAS 操作实现,用于跨服务并发控制(如游戏跨服活动报名、分布式任务调度),避免死锁(租约过期自动释放锁)。


优势:强一致性、高可用(集群部署)、轻量、支持 TTL 过期键。

8. 百库百表分库分表思路(玩家场景)

核心思路:水平分片(按玩家 ID 哈希分片),目标是分散数据压力、提升查询效率,适配百万级玩家数据存储:


  1. 分片维度选择:按玩家 ID 分片(玩家操作自身数据时,可直接路由到对应库表,无跨库联查)。

  2. 分片策略:

  3. 分库分表规则:100 库 × 100 表 = 10000 张表。玩家 ID 经过哈希计算(如 hash(playerID) % 100)得到库索引,hash(playerID) / 100 % 100 得到表索引,最终路由到 db{库索引}.t_player_{表索引}

  4. 哈希算法:用一致性哈希(带虚拟节点),支持后续扩容(新增库表时仅迁移部分数据,影响范围小)。

  5. 中间件选型:ShardingSphere-JDBC(Go 项目中用 shardingsphere-go),透明化分库分表逻辑(业务代码无需关注分片规则,直接操作逻辑表)。

  6. 关键问题解决:

  7. 全局 ID:用雪花算法(Snowflake)生成唯一订单号/玩家 ID,避免分库分表后 ID 冲突。

  8. 跨库查询:避免跨库联查,通过“宽表冗余”(如玩家订单表冗余玩家基础信息)或“应用层聚合”(先查各库数据,再在服务端合并)。

  9. 扩容方案:新增库表时,基于一致性哈希迁移旧数据,双写新旧库一段时间(确保数据一致),再切换到新库表。


项目落地:游戏玩家中心存储 500 万玩家数据,分 100 库 100 表,单表数据量控制在 5000 以内,查询响应时间稳定在 10ms 内。

二面

1. 压测时遇到的性能瓶颈及解决

压测工具:用 k6(Go 编写,高并发支持)+ Prometheus+Grafana 监控指标(QPS、响应时间、CPU/内存/网络),遇到的核心瓶颈及解决方案:



优化结果:接口 QPS 从 800 提升至 5000,响应时间稳定在 50-80ms,CPU 使用率控制在 70% 以内。

2. MySQL 相关优化

从“索引、SQL、配置、架构”四层优化,结合项目实践:


  1. 索引优化:

  2. 核心原则:给查询频繁的字段建索引,避免过度索引(影响写入性能);

  3. 实践:玩家订单表(player_id、create_time、status)建联合索引,覆盖查询(select id, amount from t_order where player_id=? and status=? order by create_time desc),避免回表。

  4. SQL 优化:

  5. 避免 select *(只查需要的字段)、避免 or(用 union 代替)、子查询转 join;

  6. 实践:将“查询玩家近 30 天订单并关联商品信息”的子查询,改为 join 查询,执行时间从 300ms 降至 50ms。

  7. 配置优化:

  8. innodb_buffer_pool_size = 物理内存的 50%-70%(缓存数据和索引,减少磁盘 I/O);

  9. max_connections = 2000(适配高并发场景);

  10. 关闭 binlog 或设置为 row 格式(减少 binlog 体积,提高写入性能)。

  11. 架构优化:

  12. 主从复制(一主两从),读请求分流到从库(通过 ShardingSphere-JDBC 实现读写分离);

  13. 分库分表(如玩家表、订单表),分散单库单表压力。

3. 实际项目中发现 MySQL 查询瓶颈的方法

核心是“监控+日志+执行计划”三位一体,步骤如下:


  1. 慢查询日志定位:开启 MySQL 慢查询日志(slow_query_log=1long_query_time=1),捕获执行时间>1s 的 SQL,定期分析日志(用 pt-query-digest 工具汇总)。

  2. 执行计划分析:对慢查询用 EXPLAIN 分析,重点看 type(索引类型,如 ref、range 优于 all)、key(是否使用索引)、rows(扫描行数,越少越好)、Extra(是否 Using filesort/Using temporary,需优化)。

  3. 实时监控:通过 Prometheus+Grafana 监控 MySQL 指标(slow_queries 慢查询数、innodb_rows_read 扫描行数、Threads_running 运行线程数),设置阈值告警(如慢查询数>10 触发告警)。

  4. 业务日志关联:在应用日志中记录 SQL 执行时间(如 Go 中用 sqlx 拦截器),当接口响应变慢时,直接定位到耗时 SQL。


项目案例:通过慢查询日志发现“玩家累计充值金额查询”SQL 未走索引,扫描全表(rows=50w),用 EXPLAIN 分析后,给 player_id 建索引,查询时间从 1.2s 降至 8ms。

4. 分布式系统 100 台服务器,玩家报错的处理流程

核心思路:快速定位故障范围→精准排查根因→临时止损→永久修复,步骤如下:


  1. 收集报错信息:让玩家提供“报错提示(如‘支付失败’)、操作时间、玩家 ID、服务器区服”,前端同时上报报错时的 traceID(链路追踪 ID)。

  2. 定位故障服务与节点:

  3. 通过 traceID 在 Jaeger 中查询跨服务调用链路,确认是哪个服务(如支付服、订单服)报错;

  4. 在 ELK 日志平台中,按“traceID+玩家 ID+时间范围”过滤日志,找到报错的服务器节点(IP+端口)。

  5. 排查节点问题:

  6. 应用日志:查看该节点的错误堆栈,定位代码层面问题(如空指针、数据库连接超时);

  7. 系统监控:查看节点的 CPU、内存、磁盘 I/O、网络(用 Prometheus+Grafana),是否存在资源耗尽;

  8. 依赖服务:检查该节点依赖的数据库、Redis、MQ 是否正常(如 Redis 连接超时、数据库主从切换)。

  9. 临时止损:

  10. 若单节点故障:通过负载均衡下线该节点,将流量转发到其他健康节点;

  11. 若服务级故障:触发熔断(如用 Hystrix/Resilience4j),返回友好提示(“系统临时维护,请稍后再试”),避免雪崩。

  12. 永久修复与复盘:

  13. 修复代码 bug(如空指针判断、重试机制优化);

  14. 优化监控告警(补充关键链路告警);

  15. 复盘会议,总结故障原因(如“未处理 Redis 连接超时”),避免同类问题。

5. 如何定位日志

基于“分布式日志架构+链路追踪”,实现日志快速定位,架构:ELK(Elasticsearch+Logstash+Kibana)+ 链路追踪(Jaeger):


  1. 日志规范:

  2. 统一日志格式(JSON 格式),包含核心字段:traceID(链路追踪 ID)、spanIDserviceName(服务名)、instanceIP(节点 IP)、playerID(玩家 ID)、time(时间戳)、level(日志级别)、msg(日志内容)、stack(错误堆栈)。

  3. 链路追踪透传:用 gRPC 拦截器或 HTTP 中间件,在服务间传递 traceID,确保同一请求的所有日志都携带相同 traceID

  4. 定位步骤:

  5. 玩家报错后,获取 traceID(从前端或玩家提供的报错信息中提取);

  6. 打开 Kibana,在索引中按 traceID:xxx 过滤,获取该请求的所有日志(从网关→业务服→依赖服务);

  7. 按时间排序日志,找到报错节点的错误堆栈,定位问题(如“支付服调用微信支付接口超时”);

  8. 结合 Jaeger 查看该 traceID 的调用链路,确认超时环节(如微信支付接口响应时间>3s)。


项目落地:通过该方案,将日志定位时间从 30 分钟缩短至 5 分钟,大幅提升故障排查效率。

6. 超买超卖的订单处理

核心是“并发控制+原子操作”,基于 Redis+MySQL 实现双重保障:


  1. 方案一:Redis 分布式锁+库存预扣减(高并发场景首选)

  2. 锁 key:lock:goods:{goodsID}(同一商品共享一把锁);

  3. 流程:

  4. 玩家下单时,用 Redis SET NX EX 命令获取锁(SET lock:goods:123 1 EX 10 NX);

  5. 获取锁成功后,查询 Redis 库存(GET goods:stock:123),库存不足则返回“商品已售罄”;

  6. 库存充足则预扣减(DECR goods:stock:123),释放锁(DEL lock:goods:123);

  7. 预扣减成功后,异步写入数据库(订单表+库存表),数据库库存表加行锁(select stock from t_goods where id=? for update),确保最终库存一致。

  8. 注意:锁超时时间需大于业务执行时间,避免死锁;用 Lua 脚本保证“查库存+扣库存”原子性。

  9. 方案二:MySQL 乐观锁(低并发场景,无锁竞争)

  10. 库存表添加 version 字段;

  11. 扣库存 SQL:update t_goods set stock=stock-1, version=version+1 where id=? and stock>=1 and version=?

  12. 执行后判断影响行数,若为 0 则说明库存不足或已被其他请求扣减,返回“操作失败”。


项目落地:游戏限时抢购活动用方案一,支持 1w+ QPS 并发下单,超买超卖率为 0,库存一致性 100%。

7. 并发场景避免二次执行(如重复发货)

核心是“幂等性设计”,结合业务场景选择以下方案:


  1. 方案一:唯一请求 ID(客户端层面)

  2. 客户端(如游戏客户端)生成唯一请求 ID(UUID),每次请求携带该 ID;

  3. 服务端接收请求后,先查 Redis:EXISTS request:id:{requestID},存在则返回“操作已执行”,不存在则执行业务逻辑;

  4. 业务逻辑执行成功后,将请求 ID 存入 Redis(SET request:id:{requestID} 1 EX 3600),过期时间设为业务操作有效时间。

  5. 方案二:业务唯一键(数据库层面)

  6. 订单表创建唯一索引:UNIQUE KEY uk_player_goods (player_id, goods_id, activity_id)(同一玩家同一活动同一商品只能创建一次订单);

  7. 重复请求时,数据库会抛出 Duplicate key error,服务端捕获后返回“操作已执行”。

  8. 方案三:分布式锁(服务端层面)

  9. 锁 key:lock:player:{playerID}:goods:{goodsID}(同一玩家同一商品的操作共享一把锁);

  10. 只有获取锁的请求能执行业务逻辑,其他请求等待或直接返回,避免并发执行。


项目落地:游戏道具发放用“方案一+方案二”,既通过请求 ID 快速拦截重复请求,又通过数据库唯一索引兜底,确保无二次发货。

8. 支付体系的回调

支付回调是支付平台(微信/支付宝)向商户服务器发送的异步支付结果通知,核心流程:“签名验证+幂等处理+订单更新+响应确认”:


  1. 完整流程:

  2. 回调配置:在支付平台(如微信支付商户平台)配置回调地址(必须 HTTPS,公网可访问);

  3. 回调触发:用户支付成功后,支付平台向回调地址发送 POST 请求,参数包含:支付流水号、订单号、支付金额、签名等;

  4. 签名验证:服务端用商户密钥验证参数签名(如微信支付的 HMAC-SHA256 签名),确保请求来自官方,防止伪造;

  5. 幂等处理:通过订单号查询本地订单状态,若已处理(如“已支付”),直接返回成功响应;

  6. 业务处理:未处理则更新订单状态为“已支付”,执行后续逻辑(扣库存、发道具、加积分);

  7. 响应确认:向支付平台返回指定格式的成功响应(如微信支付返回 <xml><return_code><![CDATA[SUCCESS]]></return_code></xml>),否则支付平台会阶梯式重试(如 15s/30s/1min/2min/5min/10min/30min/1h/2h/6h/15h,共 11 次)。

  8. 关键注意点:

  9. 签名验证:必须验证,避免恶意回调;

  10. 幂等处理:支付平台会重试,必须保证回调处理幂等;

  11. 日志记录:详细记录回调参数、处理结果,便于排查问题;

  12. 超时处理:回调处理时间需<10s,避免支付平台判定超时重试。

9. 仅用 Redis 和 MySQL 防止多个请求反复执行

核心是“Redis 原子操作快速拦截+MySQL 唯一索引兜底”,无需额外中间件:


  1. 实现方案(以玩家购买商品为例):

  2. 步骤 1:Redis 原子判断+锁定(快速拦截)

  3. 玩家发起购买请求时,服务端执行 Redis 命令:SETNX lock:player:{playerID}:goods:{goodsID} 1 EX 30(30s 过期,避免死锁);

  4. 若返回 1(获取锁成功),则继续执行;若返回 0(已被其他请求锁定),则返回“操作中,请稍后再试”。

  5. 步骤 2:MySQL 唯一索引兜底(防止 Redis 宕机)

  6. 订单表创建唯一索引:uk_player_goods (player_id, goods_id),确保同一玩家同一商品只能创建一次订单;

  7. 执行订单插入 SQL:insert into t_order (player_id, goods_id, amount, status) values (?, ?, ?, ?)

  8. 若插入成功(影响行数=1),则执行后续逻辑(扣库存、发道具);若抛出 Duplicate key error,则返回“操作已执行”。

  9. 步骤 3:释放锁

  10. 订单创建成功或失败后,执行 DEL lock:player:{playerID}:goods:{goodsID} 释放锁(或等待自动过期)。

  11. 优势:

  12. Redis 层面快速拦截高并发重复请求,减少数据库压力;

  13. MySQL 唯一索引兜底,即使 Redis 宕机,也能防止重复执行;

  14. 无额外中间件依赖,部署简单。


项目落地:游戏内玩家购买月卡场景用该方案,支持 5k+ QPS 并发请求,无重复购买、重复发货问题。

欢迎关注 ❤

我们搞了很多免费的面试真题共享群,互通有无,一起刷题进步。


没准能让你能刷到自己意向公司的最新面试题呢。


感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。

发布于: 刚刚阅读数: 4
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
B站服务器开发一二面_Go_王中阳Go_InfoQ写作社区