Skip to content

面试口述稿 · 全栈开发工程师 ​

适用岗位:全栈开发工程师 / Java 后端(偏物联网协议方向) 用法:先按章节背熟「核心口径」,再练「追问应对」。所有技术细节均可展开到表结构、类名、参数级别。 配套八股见同栏目 Java 基础 等章节。


一、自我介绍(60-90 秒,背熟) ​

面试官你好,我是曹林超,5 年开发经验,目前在一家做新能源充电的公司,职位是前端负责人,同时做后端开发。

我 2021 年入行做前端,在上一家公司做过 ERP、H5、小程序,也做过医疗数据分析平台和在线问诊的跨端产品,React 和 Vue 两条线都熟,跨端主要用 uni-app 和 Taro。

2024 年 5 月入职现在这家公司,做充电业务。当时公司后端是 PHP 写的单体,随着充电桩设备接入量上来,长连接、高并发写入和资金对账这几块开始撑不住。我在这期间自学 Java,正好团队也在推进后端 Java 化,我就和同事一起把这个过程做完了,后面逐步承担了后端主力开发,负责的几个模块都能独立交付。

目前主要做两块:一个是充电桩协议接入网关,用 Netty 做 TCP 长连接,对接了好几个厂商的私有协议;另一个是充电互联互通网关,按 T/CEC 国家标准对接省级监管平台和第三方运营商。前端这边我也一直在负责,SaaS 管理后台和 3D 运营大屏都是我主导做的。

所以我现在的工作状态是前后端都能扛,这也是我这次看全栈岗位的原因。

口述要点:3 句话讲清「前端起家 → 转全栈 → 现在能独立交付」,不吹「一入职就是后端」。


二、必问题:PHP → Java 转型 ​

Q:你是什么时候开始写 Java 的?怎么学的? ​

入职的时候后端还是 PHP,我主要是前端。2024 年底到 2025 年初,公司业务量上来之后,PHP 单体在设备长连接和资金一致性上开始出问题,团队决定做 Java 化重构。我前期先把 Java 基础和 Spring Boot 补起来,从写一些边缘接口开始,慢慢参与核心模块,后面独立负责协议接入和互联互通这两块的开发。

Q:你和后端负责人怎么分工的? ​

整体架构方案是团队一起定的,早期我更多是承接具体模块的开发,把分配给我的部分做完整交付;随着对业务和框架熟悉了,我在协议网关、互联互通这块开始独立负责设计和实现,包括协议插件化、统一语义模型、幂等机制这些,都我这边落地。

Q:为什么从 PHP 换成 Java? ​

主要是三个问题扛不住:一是设备长连接,PHP 做常驻 TCP 服务不擅长;二是高并发写入,充电订单和状态上报的量级 PHP 单体处理起来吃力;三是资金一致性,对账、支付回调这些场景需要更严格的幂等和事务控制。Java 的多线程模型、生态和 Netty 这类框架更适合我们的场景。

Q:重构过程中怎么保证业务不中断? ​

我们的做法是模块分批迁,先切业务上相对独立、风险低的部分,比如运营后台的管理类接口,验证稳定之后再迁核心链路。核心链路切换前会做数据对账校验,确保新旧两边数据一致再切。整个过程是灰度推进的,不是一次性全量替换。

⚠️ 注意:如果实际没有做过双写/灰度切流,就不要主动说这两个词,说「分批迁移 + 切换前数据对账校验」即可,被追问细节时才展开。


三、项目一:充电桩协议接入平台(YCPS) ​

Q:整体架构讲一下 ​

这是一个物联网 TCP 网关,向上对接业务系统,向下对接各厂商充电桩。整体分三层:接入层用 Netty 做 TCP 长连接,每个协议一个端口独立监听;协议处理层负责编解码和厂商指令处理;业务层把协议数据转成统一模型后,通过 Kafka 投递给业务服务,时序数据写 InfluxDB,订单数据写 MySQL。

代码上是 Maven 多模块:ycpp-common 放公共基础能力,ycpp-protocol 放协议处理,ycpp-app 放启动和业务装配。

Q:为什么选 Netty? ​

三方面:一是 NIO 事件驱动模型天然适合高连接数低流量的设备场景;二是可扩展的 ChannelHandler 链路,编解码、心跳、鉴权都能拆成独立 handler,职责清晰;三是有成熟的 IdleStateHandler 做心跳检测。

Q:十万级长连接这个数字怎么来的?(重点准备) ​

这是我们单协议实例的连接配置容量,max_connections 配的 10 万,实际部署是多个协议端口 + 多实例分片,横向扩容靠 ZooKeeper 做服务发现和分区再均衡。

要说明的是这是架构设计容量,不是单机压测极限值。真正的瓶颈在文件描述符和内存,所以我们做了连接级的资源回收,以及对不活跃连接的主动清理。

追问预案:

  • 「压测过吗?」→ 用内部压测脚本模拟过多少连接、单机内存占用多少(上线前先跟运维/同事校准一个真实数字,不要临场编)。
  • 「实际在线设备多少?」→ 说出真实设备量级 + 峰值时段。

Q:TCP 粘包半包怎么处理的?(高频) ​

我们没直接用 LengthFieldBasedFrameDecoder,而是自研了一个拆包器。原因是各厂商的帧头定义、大小端、长度域偏移都不一样:有的长度域是 1 字节,有的是 2 字节,科陆那边甚至有 4 字节和 8 字节的。自研的拆包器做成可配置:帧头标识、大小端模式、长度域长度(1/2/3/4/8 字节可配)、长度域起始偏移都能配。

另外处理了两个坑:一是长度域本身跨包的情况,收到的字节不够读长度域时要先把数据缓存起来,等下一批数据到了再重定位解析;二是脏数据的处理,帧头校验不过就直接丢弃向后滑动一个字节重新找帧头,避免一条脏数据把整个连接卡死。

Q:协议插件化是怎么做的?(高频,能体现设计能力) ​

核心是「注解 + 抽象基类 + 命令表」三层。

每个厂商的命令实现类上加自定义注解标出协议类型和命令字,比如云快充是 @YunKuaiChongCmd、新页是 @XinYeCmd、科陆是 @KeLuCmd。应用启动时通过包扫描把带注解的类全部扫出来,反射实例化后按「协议类型 + 命令字」注册进一个 ConcurrentHashMap 当命令表。

报文进来之后先解析出协议类型和命令字,去命令表里查对应的处理器执行。抽象基类里把编解码、校验、异常处理这些公共逻辑封好了,子类只实现具体的业务处理。

好处是新增一个指令,只要写一个带注解的类,不用改任何核心链路;新增一个厂商,只要加一套注解和对应的处理器,接入成本降得很明显。

Q:厂商私有协议怎么统一?(Protobuf 语义模型) ​

各厂商报文的字段名、单位、金额口径都不一样,比如有的电量单位是 0.01kWh,有的是 0.001kWh;有的费率是尖峰平谷四段,有的是自定义分段。如果让上层业务直接吃各厂商报文,业务代码会被协议细节污染。

我们的做法是在协议层和业务层之间加一层转换:用 proto 定义了统一的语义模型,包含枪状态机、分时段费率、充电进度、交易记录这些,每个厂商的处理器负责把自己的私有字段映射到这个模型上。这样上层业务只认统一模型,加新厂商不用改业务代码。

Q:Kafka 那一层为什么要有? ​

削峰和异步解耦。设备上报的频率很高,尤其是充电中的进度上报,如果每条都同步写库,数据库压力很大。我们用 Kafka 做缓冲,按设备编号做哈希分片,同一个设备的报文落到同一个分区,保证会话维度的串行处理——因为充电状态是强顺序依赖的,顺序错了会导致状态机错乱;不同设备之间并行处理,吞吐就能上去。

分片用的是 murmur3_128 哈希,保证分片均匀,避免设备量上来之后某几个分区成为热点。

Q:ZK(ZooKeeper)在里面干什么? ​

服务发现和分区再均衡。网关是多实例部署的,当实例扩容或者宕机时,需要把分区重新分配,同时让上层知道某个设备应该路由到哪个实例。我们用 Curator 做了节点监听,实例上下线时触发再均衡。

Q:Outbox 模式解决什么问题?(加分点) ​

解决的是「网关处理完设备请求,但事件没投递到业务服务」的数据丢失问题。

场景是这样:设备发起充电,网关处理成功后需要通知业务服务生成订单。如果直接发 Kafka,可能出现网关本地事务提交了但消息没发出去,或者消息发出去了但业务侧消费失败,两边状态就不一致了。

我们的方案是本地事件表 + 定时补偿:事件先落一张 outbox 表,状态是 PENDING,然后有个投递组件去做发送,发送成功改成 SUCCESS,发送中标记 SENDING 避免重复投递,失败记 FAILED 并退避重试。另外有一个定时任务作兜底,周期性扫描卡在 PENDING/SENDING 状态超时的事件重新投递。每条事件带一个幂等的 eventId,业务侧消费时按 eventId 去重,保证最终一致。


四、项目二:充电互联互通(T/CEC)网关服务 ​

Q:这个项目和上面那个的区别? ​

YCPS 是设备侧的协议网关,对接的是充电桩硬件厂商;T/CEC 是平台侧的协议网关,对接的是省级监管平台、第三方充电运营商和高德地图,走的是 HTTP 接口,遵循 T/CEC 102.2/102.3 互联互通标准。前者是 TCP,后者是 HTTP + 国标接口规范。

Q:鉴权体系怎么设计的?(必被问) ​

分两块:

  • T/CEC 互联互通侧:报文用 AES 加密,模式是 CBC、填充 PKCS5;参数做 HMAC-MD5 签名防止篡改;请求头带 Token 做身份鉴权。加密、签名、验签、Token 生成这一整套封装在一个工具类里统一处理。
  • 高德地图侧:走的是 RSA2,SHA256WithRSA 公钥验签,这是高德开放平台的规范。

⚠️ 千万不要说「监管平台用 RSA 验签」,会被问穿。

Q:多省份监管平台怎么适配?(策略 + 模板方法) ​

虽然都是 T/CEC 标准,但各省落地的字段和流程有差异,比如江苏、武汉的字段命名和部分流程定义不完全一样。

我们的做法是抽象一个基类,把标准流程里固定的部分写成模板方法(比如参数校验、加解密、响应封装),各省的实现类只覆写差异化的字段转换逻辑。这样就避免了在每个接口里写 if-else 判断省份。

Q:一次请求要扇出到多个下游,怎么处理的? ​

建了一个 Feign 层的分发组件。上游来一次上报,可能需要同时推给多个下游运营商或监管平台,每个下游的加密方式、签名规则、地址都不一样。分发组件负责按下游逐个加密签名、并发分发,然后把多个下游的响应做聚合,统一返回给上游。用了 OpenFeign 做声明式调用,但封装了统一的拦截器处理签名和加解密。

Q:幂等是怎么保证的?(高频) ​

分场景做的,不是一把梭:

  • 对账:Redis setIfAbsent 做去重,同一个对账批次处理过就不再处理。
  • 开票、支付通知:数据库唯一索引 + 捕获 DuplicateKeyException,用数据库兜底,避免 Redis 抖动导致重复。
  • 下单:按互联互通单号判重。
  • 单号映射:内部订单号和互联互通单号是多对一关系的映射,用 Redis 维护双向映射关系,查询和转换都走这个映射。

核心思路是「Redis 挡第一层,数据库唯一索引兜底」,因为 Redis 有丢失和过期风险,纯靠 Redis 不可靠。

Q:订单状态机怎么设计的? ​

状态包括:下发中、启动中、充电中、停止中、完成、失败。状态流转由协议事件驱动,每次变更记录时间和操作来源。

重点是异常兜底:有一个定时补偿任务,扫描长时间卡在中间态(比如「启动中」超过一定时间没更新)的订单,主动去查下游的真实状态做修正;同时金额和电量也做了异常校验,比如上报的电量和费率算出来的金额对不上,会标记异常并告警。

Q:告警怎么做的? ​

通过企业微信机器人推送。做了两件事:一是故障码翻译,把设备上报的原始故障码转成中文可读的说明,运维能直接看懂;二是推送频次控制,同一个故障短时间内反复上报只推一次,避免告警风暴刷屏。


五、项目三:运营 SaaS 平台(后台服务 + 商户端 + 3D 大屏) ​

Q:权限体系怎么做的? ​

用 Sa-Token 做的 RBAC,前后端一体。后端是用户、角色、菜单、部门四张核心表,通过 Sa-Token 的权限接口扩展做接口级鉴权,注解标权限点。

前端拿到菜单树之后做两级控制:一级是路由,没有权限的路由不注册;二级是按钮,用自定义指令控制按钮的显示;另外还有数据权限,比如场站合伙人只能看到自己场站的订单,这块是通过后端在查询时拼接数据范围实现的,前端不做过滤——权限一定在后端兜底,前端只是体验优化。

Q:支付通道工厂 + 回调责任链讲一下(高频) ​

我们要接微信支付、支付宝、拉卡拉三个渠道,如果每个业务都写三个 if-else,维护会很难受。

所以做了两层抽象:

  • 下单侧:支付通道工厂。定义一个统一的下单接口,每个渠道实现自己的下单逻辑,工厂按渠道类型返回对应的实现。业务方只调统一接口,不关心具体渠道。
  • 回调侧:责任链。支付回调进来之后,先做验签和参数解析,然后走一串处理器:前置处理器负责参数校验和幂等检查,业务处理器负责改订单状态,后置处理器负责发通知、记账。新增一个渠道只要加一套通道实现 + 回调处理器,不用改主流程。

资金安全上还有几层:Redisson 分布式锁保证同一订单不会并发处理,@RepeatSubmit 注解防重复提交,另外还有一个基于注解的限流。

Q:ClickHouse 为什么引入? ​

后台的经营看板要按场站、按时间维度做聚合统计,数据量上来之后直接查 MySQL 会拖垮业务库。

所以把统计数据同步到 ClickHouse,看板的聚合查询走 ClickHouse,业务增删改还是走 MySQL,做了读写分离。ClickHouse 的列式存储在这种大范围聚合场景下比 MySQL 快很多。


六、项目四:C 端多端充电应用 ​

Q:双 Token 鉴权为什么这么做? ​

是为了解决「Token 过期用户被踢出登录」的体验问题。

传统单 Token 方案要么设得很长不安全,要么设得短导致用户频繁重新登录。双 Token 是 access token 短有效期,refresh token 长有效期:access 过期后用 refresh 静默换一个新的 access,用户无感知;refresh 也过期了才需要重新登录。

实现上是自研的:登录时签发两个 Token,请求进来通过 AOP 切面统一拦截校验,切面里解析客户端来源(微信、支付宝、高德、Android、iOS),做统一鉴权,业务代码不用关心鉴权逻辑。

Q:支付回调怎么处理的? ​

三个渠道的回调地址是分开的,但都会转发到统一的回调入口,由回调入口根据渠道去分发,然后走和后台服务一致的幂等和状态更新逻辑。这里也用了分布式锁,因为多渠道回调有可能因为用户重复支付、或者渠道重试导致同一笔订单多次回调。

Q:跨端项目怎么做的? ​

用 uni-app + Vue3。核心问题是各端 API 和壳能力不一样,我们做了两件事:一是把登录、扫码、支付这几个高频跨端能力封装成统一的 composable,各端差异在内部用条件编译抹平;二是做了渐进式重构,不是推倒重来,先把公共层抽出来,然后一页一页迁,保证重构期间业务还能正常迭代。


七、上线前必须校准的数字(不要临场编) ​

项需要确认的内容
长连接单机实际稳定连接数、峰值在线设备数、部署了几个协议端口/实例
设备量接入的充电桩总数、合作的厂商数量
协议实际对接了哪几家厂商(简历写的是云快充、新页、科陆)
订单量日订单量级、充电订单峰值
Kafka主题数、分区数、日吞吐量级
对账对账频次、对账文件规模
迁移迁移耗时、分了几个批次、切换时是否有回滚预案

数字说不准就说「大概」「量级是」,不要报精确到个位数的假数据。


八、容易被问倒的点 & 应对 ​

1. 「你前端转后端,基础扎实吗?」

直接接:Java 基础、JVM 内存模型、GC、并发包都有系统学过,项目里用到了多线程、锁、线程池、AQS 这些;数据库索引和事务也是日常工作内容。可以现场聊具体的。

2. 「你简历上写独立负责,但你们团队不是有后端负责人吗?」

架构方案是团队一起定的,我自己负责的模块从设计到实现到上线是我独立完成的,包括协议插件化、统一语义模型、幂等机制、Outbox 补偿这些。团队协作和独立负责不冲突。

3. 「虚拟线程用过吗?」

用过,Java 21 的虚拟线程在我们的服务里做过 IO 密集场景的改造,配的是虚拟线程的任务执行器。(面试前确认清楚用在哪个服务、改造了什么场景)

4. 「你未来想走前端还是后端?」

我现在的状态是两边都在做,也比较适应这种角色。长期看我想往架构方向走,全栈的理解对我做技术方案设计是有帮助的。

5. 「为什么离职?/ 为什么看机会?」

准备一个正向理由:希望在一个技术挑战更大的环境里,做更系统的全栈方向工作。


九、反问面试官(准备 3 个) ​

  1. 这个岗位前后端的比例大概是怎样的?团队现在前端和后端的人是怎么分工的?
  2. 团队现在的技术栈和架构,未来一年有规划演进吗?
  3. 您觉得这个岗位上,做得好的话一年后大概是什么状态?

十、表达纪律(自查清单) ​

  • [ ] 说「我负责」和「团队一起」,不说「参与」(显得没主责)
  • [ ] 说「架构设计容量」,不说「单机扛十万」(会被压测拷问)
  • [ ] 说「分批迁移 + 数据对账校验」,不主动说「双写/灰度」(除非真做过)
  • [ ] T/CEC 说 AES + HMAC-MD5 + Token,高德才说 RSA2
  • [ ] 不知道的细节坦承「这块是同事负责的,我了解的是……」,不硬编

十一、延伸面试题(按简历技能点拆解) ​

简历上每一项技能都可能被单独拎出来问。下面按模块把「可能问什么」列全,答案只写结合项目的口径,通用八股见 Java 基础 等章节。 纯前端技能点的延伸题见 前端工程师口述稿。

Java / Spring ​

1. Java 21 你实际用了哪些新特性?虚拟线程适合什么场景?

主要用虚拟线程。网关和互联互通服务是 IO 密集型——大量等数据库、等下游 HTTP、等 Redis,这种场景收益最大,因为阻塞时不占用平台线程。 要注意数据库连接池的并发上限没变,改造的重点是「业务逻辑线程」换成虚拟线程,连接池还是那个池子。

2. 虚拟线程的原理?和平台线程的区别?有什么坑?

平台线程 1:1 映射到操作系统线程;虚拟线程是 M:N,由 JVM 调度,挂在少量载体线程上。遇到阻塞 IO 时虚拟线程会卸载,载体线程去跑别的虚拟线程。 坑:synchronized 里阻塞会 pin 住载体线程,并发场景要换 ReentrantLock;ThreadLocal 在虚拟线程下可能被大量实例拖内存;CPU 密集型任务用虚拟线程没收益。

3. Spring Boot 3 相比 2.x 有哪些变化?

底层变化是 Java 17 起步、javax.* 全部换成 jakarta.*(我们从 PHP 转 Java 直接上的 3.x,反而没历史包袱);另外是 AOT / GraalVM 原生镜像、可观测性换成 Micrometer Observation、Spring Security 6 配置写法变化。

4. AOP 的实现原理?JDK 动态代理和 CGLIB 区别?

运行时代理:实现了接口用 JDK 动态代理,没接口用 CGLIB 生成子类。 JDK 代理基于反射、必须有接口;CGLIB 是字节码生成子类,不能代理 final 类和方法。 我们用它做统一鉴权(解析客户端来源)、防重复提交、限流、日志,都是切面 + 自定义注解。

5. @Transactional 什么情况下会失效?

同类内部方法直接调用(不走代理)、方法非 public、异常被自己 catch 没抛出、抛受检异常(默认只回滚 RuntimeException 和 Error)、多线程里调用、存储引擎不支持事务。 所以跨服务一致性我们不依赖单机事务,靠幂等 + 状态机 + 定时补偿。

6. Spring Event 同步还是异步?怎么和事务结合?

默认同步,加 @Async 变异步。要保证「事务提交后再发事件」用 @TransactionalEventListener 的 AFTER_COMMIT,否则事务回滚了事件已经发出去了。 我们在用户服务里用它做业务事件解耦,把主链路耗时压下来。

7. TransmittableThreadLocal 解决什么问题?

ThreadLocal 跨线程会丢,InheritableThreadLocal 只在创建线程时继承一次,配线程池就失效。TTL 通过装饰 Runnable/Callable,在任务提交和执行的边界传递上下文,解决线程池复用场景的透传。 我们用它传链路 ID 和当前登录用户上下文。

Netty / 协议 ​

8. Netty 的线程模型讲一下

主从 Reactor 多线程:BossGroup 负责 accept,Channel 注册到 WorkerGroup 的某个 EventLoop,之后这个 Channel 的所有 IO 事件都由该 EventLoop 单线程处理,单连接无锁串行。 「一个 Channel 绑定一个 EventLoop」也是我们做设备会话状态不加锁的依据。

9. Netty 为什么快?零拷贝体现在哪?

NIO 多路复用 + 事件驱动;ByteBuf 读写指针分离,不用像 ByteBuffer 那样 flip;池化内存分配减少 GC;零拷贝有 CompositeByteBuf 组合 buffer、FileRegion 的 sendfile、堆外内存避免来回拷贝。

10. 自定义协议的解码器怎么处理粘包半包?(简历重点)

继承 ByteToMessageDecoder,它内部有 cumulation 累积缓冲区:channelRead 进来的 ByteBuf 先累加,decode 里判断可读字节够不够一个完整包,不够就 return 等下一批,够就解包并 out.add(),循环继续。 我们的自研拆包器在这个机制上加了可配的帧头标识、大小端、长度域长度(1/2/3/4/8 字节)、长度域偏移,覆盖不同厂商的帧结构;另外处理了长度域本身跨包(先缓存等下一批)和脏数据(帧头对不上就滑动一个字节重新找头)。 ⚠️ 注意 decode 必须要么消费字节要么 add 元素,否则会死循环。

11. 心跳和空闲检测怎么做?

IdleStateHandler 配读空闲/写空闲/读写空闲三种超时,触发 userEventTriggered。我们的策略是读空闲超阈值就发心跳探测,连续多次无响应主动断开并清理会话,避免僵尸连接占内存和 fd。

12. 连接数上不去怎么排查?

按层查:ulimit -n、内核参数(somaxconn、tcp_max_syn_backlog、端口范围)、JVM 堆和 direct memory、GC 频率、业务层清理策略。 单协议实例 max_connections 配的 10 万是配置容量,真压到那个量级上面每一项都得对齐。

序列化 / RPC ​

13. Protobuf 为什么比 JSON 快?

二进制编码不用解析文本;varint 变长整数,小数值占字节少;只传 tag + value 不传字段名。代价是可读性差、要维护 .proto。 我们用它定义统一的设备语义模型,因为厂商字段多、还要跨语言。

14. gRPC 用在哪?和 Feign 的区别?

gRPC 基于 HTTP/2 + Protobuf,支持一元、服务端流、客户端流、双向流,适合内部高频强契约调用;Feign 是 HTTP + JSON 的声明式客户端,适合对外接口,调试方便。 我们把 gRPC 用在网关内部对性能敏感的通道,互联互通对外还是 HTTP。

消息队列 / 分布式 ​

15. Kafka 为什么按设备哈希分区?

充电状态强顺序依赖,同一设备的报文必须落同一分区被同一消费者顺序处理,否则会状态机错乱(比如先收到结束再收到启动)。不同设备天然并行,加分区就能扩吞吐。 哈希用 murmur3_128,均匀性好,避免设备量上来后某几个分区成热点。

16. Kafka 怎么保证不丢不重?

不丢分三段:生产端 acks=all + 重试 + 幂等 producer;Broker 端 replication.factor>=3、min.insync.replicas>=2;消费端关自动提交,处理成功再手动提交 offset。 不重:Kafka 只能保证单分区单会话幂等,跨会话重复要业务兜底——我们用 eventId 去重 + 唯一索引。

17. ZooKeeper 在项目里承担什么?

服务发现和分区再均衡。网关多实例部署,扩容或宕机时要重新分配分区,同时让上游知道设备该路由到哪个实例;用 Curator 监听节点上下线。 ZAB 协议保证顺序一致性,这种协调场景比直接用 Redis 更可靠。

18. Redis 分布式锁怎么实现?Redisson 看门狗是什么?

手写是 SET key value NX PX ttl,value 用唯一标识,释放用 Lua「比对 value 再删」保证原子性。问题是业务超时锁提前释放。 Redisson 看门狗:默认 30 秒过期,后台每 10 秒(1/3 周期)续期,业务没完锁不掉;服务宕机续期停止,锁到期自动释放。我们的支付、开票、对账都在用。

19. 限流有哪几种算法?项目里怎么用的?

固定窗口(有临界问题)、滑动窗口(平滑)、漏桶(恒定速率)、令牌桶(允许突发)。 我们是在对外接口上用基于注解的限流,底层 Redis + Lua 滑动窗口,key 是「接口 + 维度(用户/IP)」。

20. 幂等有哪些实现方式?

唯一索引(最可靠,数据库兜底)、Token/防重表、状态机(只允许特定流转)、Redis setnx、乐观锁版本号。 我们的组合是「Redis 挡第一层 + 数据库唯一索引兜底」,因为 Redis 有丢数据和过期风险,不能单独依赖。

21. 分布式事务怎么处理?为什么不上 Seata?

我们的链路(设备上报 → 订单入库 → 通知业务)对强一致要求没那么高,更重要的是「最终一致 + 不丢不重」,所以选 Outbox + 定时补偿。 Seata 这类强一致方案会让链路变重、性能下降、排查复杂,我们的量级下不划算。

存储 ​

22. MySQL 索引原理?什么情况会失效?

InnoDB 是 B+ 树,非叶子节点只存索引,树矮、IO 少;叶子节点双向链表相连,适合范围查询。 失效场景:索引列上做函数/运算、隐式类型转换、like '%xx' 左模糊、违反最左前缀、or 连接非索引列、优化器判断全表更快。

23. 事务隔离级别和 MVCC?

读未提交 / 读已提交 / 可重复读(MySQL 默认)/ 串行化。 MVCC = undo log 版本链 + ReadView:RC 每次查询生成新 ReadView,RR 事务开始生成一次。RR 下的当前读幻读靠间隙锁解决。

24. Redis 缓存穿透/击穿/雪崩?

穿透(查不存在的数据):布隆过滤器或缓存空值;击穿(热点 key 过期):互斥锁重建或逻辑过期;雪崩(大批 key 同时过期 / Redis 挂了):过期时间加随机、多级缓存、集群部署。

25. InfluxDB 为什么适合时序数据?

按时间组织的 TSM 存储 + tag 倒排索引,对「按设备、按时间范围聚合」做了针对性优化,写入是批量追加。时序数据量大且只追加,用 MySQL 的 B+ 树反而浪费。

26. ClickHouse 为什么快?和 MySQL 怎么分工?

列式存储(只读用到的列)、按列压缩(同类型压缩率极高)、向量化执行、多线程并行、稀疏索引。 我们做读写分离:业务增删改走 MySQL,经营看板的大范围聚合走 ClickHouse,靠同步任务推数据过去。

业务架构 ​

27. Outbox 模式?为什么不用事务消息?

解决「本地事务提交了但消息没发出去」:事件先落 outbox 表(PENDING),投递组件负责发送,成功改 SUCCESS、发送中标 SENDING、失败记 FAILED 退避重试,定时任务扫描超时的 PENDING/SENDING 兜底,每条带 eventId,消费端去重。 不用 RocketMQ 事务消息是因为我们栈里是 Kafka,它没有开箱即用的回查机制,自己实现 Outbox 更可控,而且这套逻辑还能复用到「内存队列 / Kafka / 单体」三种部署形态。

28. 支付通道工厂 + 回调责任链为什么要这么设计?

三个渠道如果每个业务点都写 if-else,加一个渠道要改所有地方。 工厂解决下单侧(统一接口,按渠道类型返回实现);责任链解决回调侧(验签解析 → 参数校验/幂等 → 改状态 → 通知/记账),新增渠道只加一套实现 + 处理器,主流程不动。

29. 策略 + 模板方法用在多省份适配,怎么避免过度设计?

判断标准是「差异是否成体系」。我们是先按两个省份写了,发现除了少数字段转换和流程细节,其余(参数校验、加解密、响应封装)几乎完全一样,这时候才抽基类 + 模板方法。 如果当时只有一两个省份且差异很大,我不会提前抽象——过早抽象比重复代码更贵。

30. 订单状态机怎么设计?

状态:下发中、启动中、充电中、停止中、完成、失败,流转由协议事件驱动,每次变更记录时间和来源。 关键在异常兜底:定时任务扫卡在中间态超时的订单,主动查下游真实状态修正;金额和电量做交叉校验,对不上就标异常告警。

31. 分布式定时任务怎么保证不重复执行?

用 SchedulerX2 做分布式调度,靠一致性协议保证同一时刻只有一个实例触发;业务侧再加一层幂等(状态判断 + 唯一索引)双保险。长任务做分片,比如对账按日期 + 渠道分片并行。

运维 / JVM ​

32. Docker 镜像分层?怎么减小体积?

每条 Dockerfile 指令一层,层可复用,改一层只重建它之后的部分,所以「先拷 pom 下依赖,再拷源码打包」能让依赖层被缓存。 减体积:多阶段构建(构建用 maven,运行用 jre)、alpine/distroless 基础镜像、.dockerignore、合并 RUN。

33. JVM 内存结构和 GC?

线程私有:程序计数器、虚拟机栈、本地方法栈;共享:堆、方法区(元空间)。 对象优先在 Eden 分配,Minor GC 后活下来的进 Survivor 涨年龄,到阈值晋升老年代。我们服务用的 G1,关注停顿时长;排查看 GC 日志和堆 dump,工具用 Arthas、MAT。