你的定位:业务Go后端开发,不是中间件专家、不是K8s运维
✅ 你能做:业务接入、参数配置、排查业务侧问题、处理使用中遇到的业务故障、写业务代码对接SDK
❌ 你不负责:集群搭建、内核调优、源码改造、运维层面的高可用架构搭建
万能兜底句(遇到超纲问题,语气平稳说,不要慌):
“这个组件的集群搭建、底层内核调优这块我们团队是运维同学负责维护,我日常主要是业务侧接入使用,解决业务层面的问题,底层原理有看过文档,还在持续学习。”
下面分四块,每一块:是什么 → 项目里你怎么用(贴合简历,这是重中之重)→ 高频面试题+标准回答(分基础版+进阶版,进阶答不上就切兜底句)→ 踩坑故事(非常关键!面试官最爱听,显得你真的用过)
Redis是基于内存的Key-Value非关系型数据库,支持多种数据结构(String、Hash、List、Set、ZSet),读写速度很高;同时支持RDB、AOF两种持久化,把内存数据落地磁盘,防止宕机全部丢失。
MySQL是磁盘数据库,主打持久化存业务主数据;Redis是内存库,不适合作为核心业务唯一数据源,一般做缓存、辅助能力。
项目中使用go-redis这个SDK对接Redis,主要4个场景:
热点数据缓存:用户信息、配置信息、商品基础数据,先查Redis,命中直接返回;没命中再查MySQL,查到之后回写Redis,减少MySQL压力;
分布式锁:避免并发重复操作,比如防止重复下单、重复创建工单;用SET key value NX EX的方式(NX=不存在才设置,EX=自动过期);
计数器:文章阅读量、接口访问次数;
限流:接口维度的IP限流,防止恶意刷接口。
注意:你不要说用Redis做消息队列,你简历写了RabbitMQ,消息队列统一交给RabbitMQ,Redis只做简单的临时队列,生产异步任务优先用MQ。
穿透:查询一个根本不存在的数据,Redis里一直没有,请求直接打到MySQL。
回答:我们方案是缓存空值+接口层参数校验;如果是高并发场景可以布隆过滤器,但是我们当前业务量不大,用缓存空值就够用。
击穿:某一个热点key过期,大量并发请求同时过来,同时打向MySQL。
回答:两种方案,一是给热点key设置永不过期,后台异步更新;二是用分布式锁,只有一个请求去更新缓存,其他请求等待重试。我们项目用的是永不过期+后台定时刷新。
雪崩:大量key集中在同一个时间过期,大量请求同时压垮数据库。
回答:给过期时间加上随机偏移量,比如原本30分钟过期,改成30min + 0~300s随机值;同时做好服务降级,缓存失效的时候保护数据库。
回答:RDB是定时生成快照,把整个Redis内存数据存到磁盘,文件小、恢复快,缺点是两次快照之间宕机,这段时间数据会丢;AOF是记录每一条写命令,追加写入文件,丢数据更少,但是文件体积大、恢复慢。生产环境我们一般是RDB+AOF同时开启,AOF作为主持久化,RDB用来快速恢复。
回答:我们采用「先更新MySQL,再删除缓存」的方案。
坑:一开始想过先删缓存再更新DB,并发场景下会出现:A删缓存 → B读到旧数据写入缓存 → A更新DB,最后缓存是旧数据,不一致。
补充:如果对一致性要求极高,我们会用延迟双删;大部分业务是最终一致性,不需要强一致。
背诵版本:之前踩过一个分布式锁的坑,锁过期时间设置太短,业务逻辑还没执行完,锁自动释放了,别的请求拿到锁,导致并发问题。后面改成:锁的过期时间大于业务最大执行时长,同时业务里尽量缩短锁内逻辑;后面也了解了红锁,但是我们业务简单,没有上红锁。
如果问:Redis集群原理?哨兵怎么主从切换?
👉 直接兜底:我们线上Redis集群是运维统一维护,我业务侧只做SDK接入,集群高可用、主从切换的运维细节我看过文档,没有实操搭建经验。
RabbitMQ是消息中间件,核心作用:异步解耦、削峰填谷。
简单理解:生产者不用等下游所有逻辑跑完,把消息丢给MQ,立刻返回给前端;消费者异步慢慢消费消息。
核心组件:
Producer生产者:发消息的服务(你的Go后端)
Consumer消费者:消费消息的服务
Queue队列:存放消息
Exchange交换机:接收生产者消息,根据路由规则把消息转发到队列;4种常用类型:
direct:精准匹配路由键
fanout:广播,发给所有绑定队列
topic:模糊匹配,最常用
headers:按header匹配,很少用
RoutingKey:路由键
ACK:消息确认机制
使用amqp091-goSDK对接RabbitMQ。
场景:下单业务解耦。用户下单成功之后,订单主库落库完成,发送一条消息到MQ;消费者监听消息,异步去做:发送短信通知、增加用户积分、生成订单日志。
好处:短信、积分这些非核心逻辑失败,不会影响下单主流程;大促流量突增的时候,消息堆积在队列里,消费者慢慢消费,不会直接打垮积分、短信服务,实现削峰。
回答:消息丢失分三个环节,我们分别做保障:
生产者端:开启生产者确认(publisher confirm),消息成功投递到交换机才认为发送成功,失败就重试;
MQ服务端:交换机、队列设置持久化,消息设置持久化;
消费者端:关闭自动ACK,手动ACK,业务逻辑处理成功之后,再手动提交ACK;处理失败就NACK,消息重回队列或者丢到死信队列。
回答:MQ本身不保证消息只投递一次(at-most-once、at-least-once),RabbitMQ是至少投递一次,所以一定会有重复。
解决方案:业务幂等。我们用业务唯一ID(订单号),消费之前先查数据库,这个订单号已经处理过,直接跳过,不重复执行业务。数据库唯一索引兜底。
回答:先排查根源:消费者消费速度跟不上生产速度。
处理步骤:
先看消费代码有没有慢SQL、阻塞逻辑,优化消费逻辑;
横向扩容消费者实例,增加消费能力;
紧急情况,把消息转发到死信队列,避免正常队列爆满;
我们业务遇到过一次,消费逻辑里面写了同步调用第三方接口,第三方超时阻塞,消费卡主,消息疯狂堆积,后面改成异步调用第三方,设置超时时间,解决了。
回答:消息超时、被拒绝NACK、队列满了,消息会变成死信;死信队列就是专门接收死信的队列,用来做失败消息的重试、告警,避免消息丢了无声无息。
问:怎么保证消息顺序?
👉 兜底:我们当前业务不需要严格顺序消息,所以没有做顺序保障;如果要严格顺序,需要同一个路由key绑定同一个队列,单消费者消费。
问:RabbitMQ事务消息?
👉 兜底:事务消息性能差,我们业务没有使用。
Google推出的结构化数据序列化协议,和JSON、XML是同类,作用是把结构体序列化成二进制字节流,用于服务之间传输数据。
JSON是文本格式,人可读;Protobuf是二进制格式,体积更小、编解码速度更快,适合内网微服务RPC通信。
你的rpcx、tRPC框架,默认就是用protobuf定义接口入参、出参。
新建.proto文件,定义message结构体、service服务、rpc接口;
使用protoc + go插件,自动生成Go的结构体代码、RPC桩代码;
服务调用的时候,直接用生成好的结构体,框架自动完成序列化、反序列化。
区分:对外给前端的REST接口,我们用JSON;内部服务之间RPC调用,统一用protobuf。
回答:
优点:
二进制,包体积更小,网络传输流量更少;
编解码速度更快,性能更好;
强类型,proto文件定义字段,自动生成代码,自带字段校验,减少参数错误;
天然支持向前、向后兼容。
缺点:
二进制,抓包调试不方便,不能直接看懂内容;
版本变更有规范,不能随便删除、修改字段编号。
回答:核心规则:字段编号是唯一标识
新增字段:直接加新的编号,老版本代码会忽略这个新字段,新版本能识别老数据,兼容;
不要修改已经存在的字段编号;
不要删除已经在用的字段(如果废弃,只能标记reserved保留编号,不能给别的字段复用);
字段类型不要随便改。
之前踩过坑:改proto的时候,直接把老字段删掉,复用了编号,导致线上新旧服务之间序列化解析错乱,服务报错。后面定下规范:废弃字段必须写reserved,不允许复用编号。
👉 兜底:底层的varint、zigzag编码原理我看过文档,但是项目开发中主要是写proto定义、生成代码,没有深入改造底层编解码逻辑。
划重点:面试官是后端岗,他不考你怎么写复杂statefulset、调度策略、网络插件,他想看你知不知道:你的Go服务怎么在K8s上跑起来。
Docker:把应用、依赖、配置打包成镜像,一次打包,到处运行;隔离单个应用容器。
Kubernetes(K8s):容器编排平台,用来批量管理大量Docker容器。自动实现:服务自愈、滚动发布、副本扩缩容、内部负载均衡、资源调度。
Pod:K8s最小调度单元,一个Pod里面可以放一个或多个容器,我们业务一般一个Pod一个Go服务容器;
Deployment:管理无状态服务(你的Go后端服务就是无状态),控制Pod副本数量、滚动更新、回滚;
Service:给一组Pod提供统一访问入口、内部负载均衡,Pod漂移重启IP变了,Service名字不变;
ConfigMap:放配置文件(yaml、json),不写死在镜像里面;Secret:放密码、密钥等敏感信息;
Node:集群里的一台服务器;
Namespace:命名空间,用来隔离环境(dev、test、prod)。
我作为后端开发,主要负责:
编写Go服务的Dockerfile,打包镜像,推到镜像仓库;
配合运维,编写简单的Deployment、Service的yaml文件,定义副本数、资源限制、健康检查;
上线之后,会用kubectl命令:查看pod状态、看pod日志、describe看pod启动失败原因,排查业务侧问题;
新版本上线,配合运维做滚动发布;如果发布出问题,协助回滚。
集群搭建、网络插件、存储、调度策略、集群监控这些,是运维同学负责维护。
回答:新版本发布的时候,Deployment会逐步创建新版本Pod,等新Pod就绪之后,再销毁旧版本Pod,分批替换,服务不会中断;如果新版本健康检查失败,会停止发布,我们可以执行回滚,切回旧版本。
回答:livenessProbe存活探针(判断容器是不是挂了,挂了就重启Pod);readinessProbe就绪探针(判断服务能不能接收流量,没就绪就不会把流量转发给它)。我们Go服务会写/health健康接口,给K8s做探针。
之前踩过坑:Dockerfile没有做资源限制,上线K8s之后,服务内存飙高,把节点资源打满,Pod被OOM杀掉。后面在yaml里面加上request和limit资源限制,同时优化代码内存泄漏问题。还有一次健康接口写错,导致新Pod一直就绪失败,发布卡住。
👉 兜底:这些资源我了解概念,但是日常业务开发很少写,由运维统一维护配置。
问底层源码、集群调优、运维部署细节:
“这块我们是运维统一维护,我业务侧主要是接入使用,解决业务层面问题,底层原理我有学习,还在持续深耕。”
问:你这个项目里面,中间件是单机还是集群?
“线上是集群高可用部署,运维维护,我开发的时候本地是单机版本做开发调试。”
问:你有没有做过压测?
“我做过简单接口压测,发现过Redis热点key、MQ消费慢的问题,完整全链路压测是测试和运维配合一起做。”
我主要做Go后端开发,基于Gin、Beego开发RESTful接口,tRPC、rpcx做内部微服务通信,使用protobuf定义RPC接口协议;
中间件上,Redis用来做热点数据缓存、分布式锁、计数器;RabbitMQ用来异步解耦、削峰,处理下单之后的通知、积分这类非核心流程;
服务使用Docker打包镜像,部署在K8s集群,我这边负责Dockerfile、基础yaml编写、上线后日志排查、简单故障定位,集群运维由运维同学负责;
同时我也在用Cursor、Codex这类AI编程工具,做过RAG知识库、Agent Skill编排的落地实践。
如果你想要,我还可以给你做一份【一页纸精简背诵版】,全部压缩成短句,适合手机上快速翻看,面试前突击。
本文作者:枣子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!