B 站服务器开发一二面

今天分享一下训练营内部朋友在 B 站游戏服务器开发面试的详解,
主要整理了问到的技术问题,项目介绍类问题去掉了,覆盖分布式、中间件、数据库、并发控制等知识点,大家可以参考学习一下。
一面
1. 项目最终一致性的设计思路
核心思路:基于“事务消息+重试机制+幂等性”实现,优先选择低侵入性方案,适用于订单支付后库存、积分、日志等跨服务同步场景。
具体实现(以订单支付为例):
本地事务与消息发送原子性:使用“本地消息表+定时任务”或 RocketMQ 事务消息。比如用 RocketMQ 时,先执行本地订单更新(状态改为“待支付”→“已支付”),成功后提交事务消息,失败则回滚本地事务。
消息消费与重试:下游服务(库存、积分)订阅事务消息,消费成功则更新自身状态,失败则触发 MQ 重试(阶梯式重试:10s/30s/5min,避免瞬时故障)。
幂等性保障:每个消息携带唯一 ID(如订单号+流水号),下游服务消费前先查“消息消费记录表”,已消费则直接返回成功,未消费则执行逻辑。
最终兜底:定时任务扫描“未同步成功”的订单,主动触发补偿逻辑(如调用库存服务接口重试),确保最终所有服务状态一致。
2. 项目异步设计的思路
核心思路:解耦服务依赖、提高吞吐量,优先用“消息队列+Go 协程”组合,覆盖跨服务异步和本地异步场景。
具体设计:
跨服务异步(解耦):用 Kafka/RocketMQ 做异步通信,比如用户注册后,同步发送“注册成功”消息,下游服务(短信、邮件、日志)订阅消费,主流程无需等待。
本地异步(提效):用 Go 协程处理无依赖的本地任务,比如订单创建后,启动协程异步生成订单快照、记录操作日志,通过
sync.WaitGroup控制协程等待(如需等待结果)或channel传递结果。关键保障:
幂等性:同最终一致性的消息 ID 校验;
超时处理:用
context.WithTimeout控制协程执行时间,避免阻塞;错误处理:协程 panic 捕获(
defer recover())、消息消费失败入死信队列,定期复盘;结果回调:如需同步异步结果,用“回调函数+channel”或“状态轮询”(如前端轮询订单支付状态)。
项目落地:游戏充值接口通过异步化改造,吞吐量从 500 QPS 提升至 3000 QPS,响应时间从 300ms 降至 50ms。
3. 消息队列怎么消费不同标签的信息
以 RocketMQ(Tag 机制)和 Kafka(Topic+Partition 二级分类)为例,核心是“** broker 端过滤+消费端订阅**”:
标签(Tag)设计:Tag 是消息的二级分类,基于业务场景划分(如订单消息:
ORDER_PAID/ORDER_CANCELLED/ORDER_REFUNDED)。消费端订阅逻辑(Go 实现):
RocketMQ:使用 Go SDK(如
github.com/apache/rocketmq-client-go),在创建消费者时,通过ConsumerOption指定订阅的 Tag,格式为Topic:Tag1||Tag2(多 Tag 用||分隔),Broker 会仅将匹配 Tag 的消息投递给消费者。Kafka:无原生 Tag,但可通过“Topic+消息头”模拟,消费端读取消息头中的
tag字段过滤,或直接按 Tag 拆分 Topic(如order_paid_topic/order_cancelled_topic),更高效。优势:Broker 端过滤减少无效消息传输,提升消费效率;消费端可灵活订阅所需 Tag,实现业务解耦。
4. Golang 的线程池、协程池的使用?比如 running buffer
Go 无内置线程池/协程池,但协程(Goroutine)轻量(初始栈 2KB),可通过 channel 手动实现协程池,核心是“控制并发数+任务调度”:
(1)协程池核心设计
核心组件:任务队列(
taskChan)、worker 协程池、并发控制(maxWorkers)、运行状态标识(running buffer,即当前活跃 worker 数)。实现步骤(Go 代码简化):
(2)关键概念与使用场景
running buffer:用atomic.Int32维护当前运行的 worker 数,可用于监控协程池负载(如通过 Prometheus 暴露指标)。使用场景:高并发 I/O 操作(如批量调用第三方接口、数据库批量写入)、避免无限制创建协程导致的内存溢出。
注意点:任务队列需设置缓冲(避免提交任务阻塞)、worker 优雅退出(通过 context 控制)、错误处理(任务执行失败需记录日志或重试)。
5. 用的什么中间件监听数据库 binlog
项目中用 Canal 监听 MySQL binlog,核心是“模拟 MySQL 从库同步协议,解析 binlog 并推送变更”:
工作流程:
Canal 伪装成 MySQL 从库,向主库发送 dump 命令,获取 binlog 日志;
解析 binlog(支持 row 格式,记录具体数据变更),提取表名、操作类型(insert/update/delete)、变更前后数据;
通过 Canal Client(Go SDK:
github.com/alibaba/canal-go)订阅变更事件,推送至业务逻辑(如同步数据到 Redis、ES,或触发跨服务通知)。优势:轻量、低侵入(无需修改业务代码)、支持高可用部署(Canal Server 集群)。
6. Redis 常用的数据结构
进阶用法:Hash 用 HSCAN 避免大 key 阻塞、Sorted Set 用 ZREMRANGEBYRANK 维护 TopN 排行榜、String 用 SETEX 实现过期缓存。
7. ETCD 的作用
ETCD 是分布式键值存储(基于 Raft 协议),核心作用是“分布式一致性保障”,项目中主要用于 3 个场景:
服务注册与发现:微服务(如游戏网关、战斗服、道具服)启动时向 ETCD 注册(key=/services/{serviceName}/{instanceID},value=服务地址+元数据),客户端通过 ETCD 的 Watch 机制监听服务变更,动态获取可用实例(配合 gRPC 负载均衡)。
配置中心:存储全局配置(如数据库连接池大小、活动开关、限流阈值),通过 Watch 机制实现配置动态更新(无需重启服务),Go 中用
etcd/clientv3订阅配置变更。分布式锁:基于 ETCD 的 Lease(租约)+ CAS 操作实现,用于跨服务并发控制(如游戏跨服活动报名、分布式任务调度),避免死锁(租约过期自动释放锁)。
优势:强一致性、高可用(集群部署)、轻量、支持 TTL 过期键。
8. 百库百表分库分表思路(玩家场景)
核心思路:水平分片(按玩家 ID 哈希分片),目标是分散数据压力、提升查询效率,适配百万级玩家数据存储:
分片维度选择:按玩家 ID 分片(玩家操作自身数据时,可直接路由到对应库表,无跨库联查)。
分片策略:
分库分表规则:100 库 × 100 表 = 10000 张表。玩家 ID 经过哈希计算(如
hash(playerID) % 100)得到库索引,hash(playerID) / 100 % 100得到表索引,最终路由到db{库索引}.t_player_{表索引}。哈希算法:用一致性哈希(带虚拟节点),支持后续扩容(新增库表时仅迁移部分数据,影响范围小)。
中间件选型:ShardingSphere-JDBC(Go 项目中用
shardingsphere-go),透明化分库分表逻辑(业务代码无需关注分片规则,直接操作逻辑表)。关键问题解决:
全局 ID:用雪花算法(Snowflake)生成唯一订单号/玩家 ID,避免分库分表后 ID 冲突。
跨库查询:避免跨库联查,通过“宽表冗余”(如玩家订单表冗余玩家基础信息)或“应用层聚合”(先查各库数据,再在服务端合并)。
扩容方案:新增库表时,基于一致性哈希迁移旧数据,双写新旧库一段时间(确保数据一致),再切换到新库表。
项目落地:游戏玩家中心存储 500 万玩家数据,分 100 库 100 表,单表数据量控制在 5000 以内,查询响应时间稳定在 10ms 内。
二面
1. 压测时遇到的性能瓶颈及解决
压测工具:用 k6(Go 编写,高并发支持)+ Prometheus+Grafana 监控指标(QPS、响应时间、CPU/内存/网络),遇到的核心瓶颈及解决方案:
优化结果:接口 QPS 从 800 提升至 5000,响应时间稳定在 50-80ms,CPU 使用率控制在 70% 以内。
2. MySQL 相关优化
从“索引、SQL、配置、架构”四层优化,结合项目实践:
索引优化:
核心原则:给查询频繁的字段建索引,避免过度索引(影响写入性能);
实践:玩家订单表(player_id、create_time、status)建联合索引,覆盖查询(
select id, amount from t_order where player_id=? and status=? order by create_time desc),避免回表。SQL 优化:
避免 select *(只查需要的字段)、避免
or(用 union 代替)、子查询转 join;实践:将“查询玩家近 30 天订单并关联商品信息”的子查询,改为 join 查询,执行时间从 300ms 降至 50ms。
配置优化:
innodb_buffer_pool_size = 物理内存的 50%-70%(缓存数据和索引,减少磁盘 I/O);
max_connections = 2000(适配高并发场景);
关闭 binlog 或设置为 row 格式(减少 binlog 体积,提高写入性能)。
架构优化:
主从复制(一主两从),读请求分流到从库(通过 ShardingSphere-JDBC 实现读写分离);
分库分表(如玩家表、订单表),分散单库单表压力。
3. 实际项目中发现 MySQL 查询瓶颈的方法
核心是“监控+日志+执行计划”三位一体,步骤如下:
慢查询日志定位:开启 MySQL 慢查询日志(
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,需优化)。实时监控:通过 Prometheus+Grafana 监控 MySQL 指标(
slow_queries慢查询数、innodb_rows_read扫描行数、Threads_running运行线程数),设置阈值告警(如慢查询数>10 触发告警)。业务日志关联:在应用日志中记录 SQL 执行时间(如 Go 中用
sqlx拦截器),当接口响应变慢时,直接定位到耗时 SQL。
项目案例:通过慢查询日志发现“玩家累计充值金额查询”SQL 未走索引,扫描全表(rows=50w),用 EXPLAIN 分析后,给 player_id 建索引,查询时间从 1.2s 降至 8ms。
4. 分布式系统 100 台服务器,玩家报错的处理流程
核心思路:快速定位故障范围→精准排查根因→临时止损→永久修复,步骤如下:
收集报错信息:让玩家提供“报错提示(如‘支付失败’)、操作时间、玩家 ID、服务器区服”,前端同时上报报错时的 traceID(链路追踪 ID)。
定位故障服务与节点:
通过 traceID 在 Jaeger 中查询跨服务调用链路,确认是哪个服务(如支付服、订单服)报错;
在 ELK 日志平台中,按“traceID+玩家 ID+时间范围”过滤日志,找到报错的服务器节点(IP+端口)。
排查节点问题:
应用日志:查看该节点的错误堆栈,定位代码层面问题(如空指针、数据库连接超时);
系统监控:查看节点的 CPU、内存、磁盘 I/O、网络(用 Prometheus+Grafana),是否存在资源耗尽;
依赖服务:检查该节点依赖的数据库、Redis、MQ 是否正常(如 Redis 连接超时、数据库主从切换)。
临时止损:
若单节点故障:通过负载均衡下线该节点,将流量转发到其他健康节点;
若服务级故障:触发熔断(如用 Hystrix/Resilience4j),返回友好提示(“系统临时维护,请稍后再试”),避免雪崩。
永久修复与复盘:
修复代码 bug(如空指针判断、重试机制优化);
优化监控告警(补充关键链路告警);
复盘会议,总结故障原因(如“未处理 Redis 连接超时”),避免同类问题。
5. 如何定位日志
基于“分布式日志架构+链路追踪”,实现日志快速定位,架构:ELK(Elasticsearch+Logstash+Kibana)+ 链路追踪(Jaeger):
日志规范:
统一日志格式(JSON 格式),包含核心字段:
traceID(链路追踪 ID)、spanID、serviceName(服务名)、instanceIP(节点 IP)、playerID(玩家 ID)、time(时间戳)、level(日志级别)、msg(日志内容)、stack(错误堆栈)。链路追踪透传:用 gRPC 拦截器或 HTTP 中间件,在服务间传递
traceID,确保同一请求的所有日志都携带相同traceID。定位步骤:
玩家报错后,获取
traceID(从前端或玩家提供的报错信息中提取);打开 Kibana,在索引中按
traceID:xxx过滤,获取该请求的所有日志(从网关→业务服→依赖服务);按时间排序日志,找到报错节点的错误堆栈,定位问题(如“支付服调用微信支付接口超时”);
结合 Jaeger 查看该
traceID的调用链路,确认超时环节(如微信支付接口响应时间>3s)。
项目落地:通过该方案,将日志定位时间从 30 分钟缩短至 5 分钟,大幅提升故障排查效率。
6. 超买超卖的订单处理
核心是“并发控制+原子操作”,基于 Redis+MySQL 实现双重保障:
方案一:Redis 分布式锁+库存预扣减(高并发场景首选)
锁 key:
lock:goods:{goodsID}(同一商品共享一把锁);流程:
玩家下单时,用 Redis SET NX EX 命令获取锁(
SET lock:goods:123 1 EX 10 NX);获取锁成功后,查询 Redis 库存(
GET goods:stock:123),库存不足则返回“商品已售罄”;库存充足则预扣减(
DECR goods:stock:123),释放锁(DEL lock:goods:123);预扣减成功后,异步写入数据库(订单表+库存表),数据库库存表加行锁(
select stock from t_goods where id=? for update),确保最终库存一致。注意:锁超时时间需大于业务执行时间,避免死锁;用 Lua 脚本保证“查库存+扣库存”原子性。
方案二:MySQL 乐观锁(低并发场景,无锁竞争)
库存表添加
version字段;扣库存 SQL:
update t_goods set stock=stock-1, version=version+1 where id=? and stock>=1 and version=?;执行后判断影响行数,若为 0 则说明库存不足或已被其他请求扣减,返回“操作失败”。
项目落地:游戏限时抢购活动用方案一,支持 1w+ QPS 并发下单,超买超卖率为 0,库存一致性 100%。
7. 并发场景避免二次执行(如重复发货)
核心是“幂等性设计”,结合业务场景选择以下方案:
方案一:唯一请求 ID(客户端层面)
客户端(如游戏客户端)生成唯一请求 ID(UUID),每次请求携带该 ID;
服务端接收请求后,先查 Redis:
EXISTS request:id:{requestID},存在则返回“操作已执行”,不存在则执行业务逻辑;业务逻辑执行成功后,将请求 ID 存入 Redis(
SET request:id:{requestID} 1 EX 3600),过期时间设为业务操作有效时间。方案二:业务唯一键(数据库层面)
订单表创建唯一索引:
UNIQUE KEY uk_player_goods (player_id, goods_id, activity_id)(同一玩家同一活动同一商品只能创建一次订单);重复请求时,数据库会抛出
Duplicate key error,服务端捕获后返回“操作已执行”。方案三:分布式锁(服务端层面)
锁 key:
lock:player:{playerID}:goods:{goodsID}(同一玩家同一商品的操作共享一把锁);只有获取锁的请求能执行业务逻辑,其他请求等待或直接返回,避免并发执行。
项目落地:游戏道具发放用“方案一+方案二”,既通过请求 ID 快速拦截重复请求,又通过数据库唯一索引兜底,确保无二次发货。
8. 支付体系的回调
支付回调是支付平台(微信/支付宝)向商户服务器发送的异步支付结果通知,核心流程:“签名验证+幂等处理+订单更新+响应确认”:
完整流程:
回调配置:在支付平台(如微信支付商户平台)配置回调地址(必须 HTTPS,公网可访问);
回调触发:用户支付成功后,支付平台向回调地址发送 POST 请求,参数包含:支付流水号、订单号、支付金额、签名等;
签名验证:服务端用商户密钥验证参数签名(如微信支付的 HMAC-SHA256 签名),确保请求来自官方,防止伪造;
幂等处理:通过订单号查询本地订单状态,若已处理(如“已支付”),直接返回成功响应;
业务处理:未处理则更新订单状态为“已支付”,执行后续逻辑(扣库存、发道具、加积分);
响应确认:向支付平台返回指定格式的成功响应(如微信支付返回
<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>),否则支付平台会阶梯式重试(如 15s/30s/1min/2min/5min/10min/30min/1h/2h/6h/15h,共 11 次)。关键注意点:
签名验证:必须验证,避免恶意回调;
幂等处理:支付平台会重试,必须保证回调处理幂等;
日志记录:详细记录回调参数、处理结果,便于排查问题;
超时处理:回调处理时间需<10s,避免支付平台判定超时重试。
9. 仅用 Redis 和 MySQL 防止多个请求反复执行
核心是“Redis 原子操作快速拦截+MySQL 唯一索引兜底”,无需额外中间件:
实现方案(以玩家购买商品为例):
步骤 1:Redis 原子判断+锁定(快速拦截)
玩家发起购买请求时,服务端执行 Redis 命令:
SETNX lock:player:{playerID}:goods:{goodsID} 1 EX 30(30s 过期,避免死锁);若返回 1(获取锁成功),则继续执行;若返回 0(已被其他请求锁定),则返回“操作中,请稍后再试”。
步骤 2:MySQL 唯一索引兜底(防止 Redis 宕机)
订单表创建唯一索引:
uk_player_goods (player_id, goods_id),确保同一玩家同一商品只能创建一次订单;执行订单插入 SQL:
insert into t_order (player_id, goods_id, amount, status) values (?, ?, ?, ?);若插入成功(影响行数=1),则执行后续逻辑(扣库存、发道具);若抛出
Duplicate key error,则返回“操作已执行”。步骤 3:释放锁
订单创建成功或失败后,执行
DEL lock:player:{playerID}:goods:{goodsID}释放锁(或等待自动过期)。优势:
Redis 层面快速拦截高并发重复请求,减少数据库压力;
MySQL 唯一索引兜底,即使 Redis 宕机,也能防止重复执行;
无额外中间件依赖,部署简单。
项目落地:游戏内玩家购买月卡场景用该方案,支持 5k+ QPS 并发请求,无重复购买、重复发货问题。
欢迎关注 ❤
我们搞了很多免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/47eb9d55b0d37cbb765e1c5f2】。文章转载请联系作者。







评论