(本文面经来自牛客大佬)
得物go后端校招二面面经
1、协程和进程
2、goroutine过多的问题
3、客户端到服务端的完整链路
4、负载均衡的策略
5、缓存和数据库一致性的问题
6、先删缓存再删数据库会有什么问题
7、消息队列的用处和可能出现的问题
8、设计一个热门话题榜单,支持实时更新与c端高并发,怎么设计这么系统
9、刷榜问题怎么解决
10、反问
回答
1、协程和进程
协程:Go 语言层面的轻量级 “用户态线程”,由 Go 运行时(runtime)调度,而非操作系统内核调度,占用资源极少,程初始栈仅 2KB。
进程:操作系统分配资源的基本单位(独立内存,CPU,磁盘IO等),是一次程序运行的实例。
再补充一下线程:操作系统调度的基本单位,依附于进程,一个进程可包含多个线程,线程共享进程的内存空间;
2、goroutine过多的问题
Go 虽然支持创建海量 Goroutine,但并非越多越好,当 Goroutine 数量超出合理范围(比如单机百万级),会引发一系列性能和稳定性问题。
简单来说就是协程虽然资源占比小,但数量过多,资源占比就大了。
过多会出现的问题:
- 内存耗尽:虽然协程初始栈为2KB,但会动态扩容,而且数量过多的话,总占比上去了
- 调度开销暴增,性能下降:Go 运行时的调度器(P)需要不断切换 Goroutine(G),切换本身有成本(虽然是用户态,但量大积少成多)
- 系统资源耗尽:协程阻塞的时候,系统会创建新的M(系统性线程)来绑定P(调度器);
- 文件句柄泄漏:若每个 Goroutine 都创建网络连接 / 打开文件,且未及时关闭,会耗尽系统的
ulimit -n(默认 1024/65535)
3、客户端到服务端的完整链路
逻辑:先 DNS 找 IP,再 TCP 握手,然后发 HTTP,后端处理完返回。
导航与寻址:DNS & CDN:首先是浏览器解析域名。它会先查本地缓存,如果没有,就发起递归查询。在这个阶段,通常会涉及CDN(内容分发网络)。如果请求的是静态资源,CDN 会根据负载均衡和地理位置,直接从边缘节点返回数据,避免请求打到源站
建立通信:TCP / TLS / HTTP3:拿到 IP 后,客户端发起连接。如果是传统的 HTTP/1.1 或 2,会经历TCP 三次握手;如果是 HTTPS,还会进行TLS 握手来交换密钥。不过,现在的趋势是HTTP/3 (QUIC),它基于 UDP,通过单次往返就能建立安全连接,极大地优化了弱网下的表现。
门户与调度:负载均衡与网关:请求进入机房后,首先迎接它的是负载均衡(如 Nginx 或 LVS)。它负责把请求分发给健康的服务器。接着会经过API 网关,在这里完成鉴权、限流、日志记录和协议转换(比如从 HTTP 转成内部的 gRPC)。
业务工厂:服务端处理:后端应用接收请求后,进入业务逻辑层。通常会涉及Redis 缓存查询(以减轻 DB 压力)和数据库读写。如果是微服务架构,可能还会通过RPC调用其他服务。处理完成后,封装成标准的 HTTP 响应返回。
4、负载均衡的策略
首先聊聊负载均衡是什么
简单来说,负载均衡(Load Balancing)就是一个“流量调度员”。
用实例来说,想象你开了一家火爆的奶茶店,起初只有一个店员(服务器),他能应付得来。后来生意越来越好,排队的人太多了,店员忙到冒烟,顾客等得想打人。于是你又雇了 3 个店员。负载均衡器(Load Balancer)就是站在门口分发号码牌的那个人:他会根据每个店员的忙碌程度,把新来的顾客指引到最空闲的柜台前。
那么负载均衡是为了什么,他的核心如下:
高可用性(High Availability):如果其中一台服务器“宕机”了,负载均衡器会自动绕过它,把流量分给剩下的正常服务器,保证业务不中断。
可扩展性(Scalability):当流量暴涨时,你只需要多塞几台服务器到后端,负载均衡器就能立刻把压力平摊出去。
性能优化:避免出现“一端忙死,一端闲死”的情况,让每台服务器都在最佳状态运行。
策略:
1. 轮询(Round Robin)
依次分发:1、2、3、1、2、3…
适用:服务器配置相同、请求耗时差不多
缺点:某台机器慢,会堆积请求
2. 加权轮询(Weighted Round Robin)
给性能好的机器权重更高
例:权重 3:1 → 前 3 台给 A,第 4 台给 B
适用:服务器配置不一样
3. 随机(Random)
随机选一台
简单、高效,请求越多越均匀
适用:高并发、简单场景
4. 加权随机(Weighted Random)
性能好的机器被随机到的概率更高
5. 最小连接数(Least Connections)
发给当前连接最少的机器
适用:请求处理时间差异大(长连接、慢接口)
最智能、最常用之一
6. 最小响应时间(Least Response Time)
按响应时间 + 连接数综合选择
响应越快,优先分发
适用:对延迟敏感的业务
7. IP Hash(源地址哈希)
同一个 IP → 永远发给同一台服务器
作用:保持会话(Session 粘连)
适用:需要登录态、有本地缓存的服务
5、缓存和数据库一致性的问题
这题的核心问题为缓存和数据库的数据更新不同步,导致读取到脏数据。
其实一致性分为两种情况:
- 最终一致性(99% 业务场景):允许短时间内缓存和数据库数据不一致,但最终会同步(如电商、社交、资讯);
- 强一致性(极少数场景):缓存和数据库数据必须实时一致(如金融交易、支付、核心账户)。
对于最终一致性:
方案 1:Cache Aside(旁路缓存)
更新:先更新数据库 → 再删除缓存(而非更新缓存)
读取:缓存命中 → 返回;缓存未命中 → 读数据库 → 写入缓存 → 返回
这个方案当“删缓存后、更数据库前”的情况时,会出现脏读;
方案 2:延迟双删 —— 解决 Cache Aside 的并发脏读
在方案1的基础上加短暂延迟重试:更新数据库后,延迟 100ms 再删一次缓存(覆盖线程 B 提前写回的脏数据)
1. 先删除缓存 2. 更新数据库 3. 延迟N毫秒(如100ms),再次删除缓存
方案 3:Canal(基于 MySQL binlog)—— 最终一致性最优解
更新:更新数据库 → MySQL写binlog → Canal监听 → 异步删缓存
读取:缓存命中 → 返回;缓存未命中 → 读数据库 → 写入缓存 → 返回
对强一致性:
方案1:加分布式锁
更新:加分布式锁 → 更新数据库 → 删除缓存 → 释放锁
读取:加分布式锁 → 缓存命中则返回 → 缓存未命中则读库写缓存 → 释放锁
缺点:
- 锁竞争导致性能大幅下降;
- 可能出现死锁,需设置锁超时。
方案 2:缓存只存热点,且设置极短过期时间
1. 缓存过期时间设为1~5秒; 2. 读取时优先读缓存,缓存失效则读数据库并写缓存; 3. 更新时直接更新数据库,不操作缓存;
6、先删缓存再删数据库会有什么问题
无论 “先删缓存再删数据库” 还是 “先删缓存再更新数据库”,都会导致严重的脏数据问题,是绝对的错误操作。
先删缓存 → 再更新数据库
线程A(更新操作):删除缓存(user:1) → 线程A:更新数据库(zhangsan → lisi)(耗时100ms) (线程A更新数据库的100ms内,线程B发起读取)
线程B(读取操作):缓存未命中 → 线程B:读取数据库(还没更新完,仍是zhangsan) → 线程B:把旧数据zhangsan写回缓存 ↓
线程A:数据库更新完成(lisi)
最终结果:脏数据永久存在
先删缓存 → 再删数据库
线程A:删除缓存(user:1) → 线程A:删除数据库中user:1的数据
线程B:缓存未命中 → 线程B:读取数据库(user:1已被删除) → 线程B:误以为“数据不存在”,不会写缓存 ↓
线程A:删库操作失败(如网络异常),数据库中user:1仍存在
7、消息队列的用处和可能出现的问题
先讲讲MQ的核心功能也是用处
- 解耦(最核心):就是把绑起来的系统拆开,例如电商系统,用户付完款了,系统还要干好几件事情,扣库存,发优惠卷,加积分,短信通知等等。如果直接系统挨个去执行,那么整个就高度耦合,如果哪一天不需要哪个功能或者新加功能,那么需要整体更改,还有如果某个环节挂掉了或报错,会影响整个支付流程,引入MQ后,只需要支付完后,对MQ发送一条消息(用户A已支付),接下来主线任务完成,只需要子系统自己去订阅MQ,不需要管有几个子系统,最后如果增改子系统,也不需要对订单代码修改。
- 异步:接着上面的例子,没有MQ前是挨个执行的,假设一个子系统功能需要t ms, 那么整个子系统流程要tn ms,那么长的等待时间,就会导致用户体验感不好,用了MQ后,其实只要主流程把状态一改,发给MQ,就可以立即给前端响应,而子系统的功能并发需要在这么短的时间完成。
- 削峰: 如果突然有几万个并发请求,他们打到数据库,数据库承受不住,数据库会直接宕机。这时候用MQ,就扮演排队区的作用,请求打过来,我们先统一扔到MQ里,根据数据库处理情况,慢慢从MQ里往外拉任务。
问题 1:消息丢失(最常见)
成因(3 个环节):
- 生产者丢失:发送消息后,MQ 未收到,但生产者误以为发送成功;
- MQ 服务端丢失:消息写入 MQ 后,未持久化就宕机;
- 消费者丢失:消费者拿到消息后,未处理完就宕机,MQ 误以为消费成功。
解决:
生产者:1. 同步发送 + 确认(如 Kafka 的 acks=all、RabbitMQ 的 confirm 模式);2. 本地消息表兜底(发送前写库,成功后删库);
MQ服务端:1. 开启持久化(Kafka 刷盘、RabbitMQ 持久化到磁盘);2. 集群部署(副本机制);
消费者:1. 关闭自动确认,处理完消息后手动确认;2. 消费失败时重试,重试失败入死信队列;
问题 2:消息重复消费(必然存在)
成因:
- 网络延迟:消费者手动确认消息时,MQ 未收到确认,重新推送;
- 重试机制:生产者重试发送,导致 MQ 中出现重复消息。
解决方案:
- 消费端幂等处理(核心):
- 方案 1:基于唯一 ID(如订单 ID),消费前查数据库是否已处理;
- 方案 2:基于 Redis / 数据库做幂等标记(SETNX 锁 + 过期时间);
- 方案 3:数据库操作本身幂等(如 UPDATE user SET num=num+1 WHERE id=1);
- MQ 层面:设置消息去重队列(如 RabbitMQ 的去重插件)。
问题 3:消息顺序消费(部分场景需要)
成因:
MQ 集群部署时,同批次消息可能被分发到不同消费者,导致顺序错乱(如订单创建→支付→发货,被消费成支付→创建→发货)。
解决方案:
分区 / 队列绑定:
Kafka:相同业务 ID(如订单 ID)的消息发送到同一个分区,一个分区只被一个消费者消费;
RabbitMQ:相同业务 ID 的消息发送到同一个队列,一个队列只被一个消费者消费;
业务层面:消费后按顺序号重排(如给消息加 seq,消费后排序再处理)。
问题 4:消息积压(高并发 / 消费慢)
成因:
生产速度远大于消费速度(如秒杀产生 10 万消息,消费者仅能处理 1 万 / 分钟);
消费者故障:消费服务宕机,消息堆积在 MQ 中。
解决方案:
临时扩容:增加消费者实例数,提升消费速度;
分流处理:将积压消息转发到临时队列,用专门的消费集群处理;
限流降级:生产端限流,避免积压加剧;
预警机制:监控 MQ 消息堆积量,超过阈值(如 1 万)及时报警。
问题 5:分布式事务问题(跨服务一致性)
成因:
生产者发送消息和本地业务操作(如扣库存)无法保证原子性(库存扣成功,消息没发出去;或消息发出去,库存没扣成)。
解决方案:
本地消息表方案(主流):
步骤 1:生产者本地事务,同时写业务数据 + 消息表;
步骤 2:定时任务扫描消息表,重发未确认的消息;
步骤 3:消费者处理完消息后,回调生产者标记消息已确认;
Seata AT 模式:基于 TC(事务协调器)实现分布式事务,兼容 MQ 场景;
RocketMQ 事务消息:原生支持 “半消息 + 回查”,保证生产端原子性。
问题 6:MQ 集群宕机(可用性问题)
成因:
单节点 MQ 宕机,生产 / 消费全部中断;
网络分区,MQ 集群脑裂。
解决方案:
集群部署:Kafka(副本 + 分区)、RabbitMQ(镜像队列 + 集群)、RocketMQ(主从架构);
多活部署:异地多活 MQ 集群,某一区域宕机切换到另一区域;
降级方案:MQ 宕机时,生产端临时写本地日志,恢复后补发到 MQ。
8、设计一个热门话题榜单,支持实时更新与c端高并发,怎么设计这么系统
先来分析一下这个系统需要什么功能?
- 实时更新
- 高并发读
- 防刷/防作弊
- 兜底(系统异常时)
热门话题榜单核心是分层设计,写侧用消息队列异步接收行为数据,Redis ZSet 实时计算热度;读侧用本地缓存 + Redis+CDN 多级缓存扛高并发,同时通过防刷、降级保证榜单真实和系统可用。
整体流程:用户行为上报 → 接入层(Nginx/LB) → 接入服务 → 消息队列 → 热度计算服务 → 榜单存储 → 多级缓存 → C端API服务 → 客户端
- 接入层:Nginx/LB 做负载均衡,限流(令牌桶),防止突发流量打垮后端;
- 接入服务:无状态化部署,仅做参数校验、用户鉴权,不做业务逻辑;
- 异步化:将行为数据写入消息队列(Kafka/RocketMQ),立即返回成功(C 端感知不到延迟);
- 兜底:消息队列开启持久化 + 副本,避免行为数据丢失。
- 热度计算:需异步计算 + 增量更新,避免全量排序消耗资源
增量计算:用Redis Sorted Set(ZSet)存储各维度的热度榜单,ZSet 的
score为计算后的热度值,member为话题 ID;- 定时全量校准:定时对热点进行计算(每5分钟),计算结果先写入临时ZSet,没问题再上刀线上ZSet
- 防刷:安全措施
- 榜单存储:多级缓存 + 分层存储
9.刷榜问题怎么解决
- 实时过滤:
- 行为上报时,限制单用户 / IP 的行为频率(如 1 分钟点赞≤10 次),异常行为直接丢弃;
- 对新账号、异常设备的行为做降权处理(如权重 ×0.1);
- 离线审计:
- 定时分析热度曲线,标记 “短时间内热度暴涨” 的话题,人工复核;
- 作弊话题直接剔除榜单,并清除违规热度,封禁刷量账号。