1.java后端基础(20道)
1.1 jvm内存结构
JVM 分为堆、栈、方法区、程序计数器;对象主要在堆中分配,GC 负责内存回收。
JVM 我重点关注的是堆和GC行为。
堆分新生代和老年代,新生代对象创建频繁,如果GC不合理会直接影响系统延迟。
在交易所撮合和行情服务这种长时间运行、高并发、低延迟的系统里,我们更关注STW时间是否可控,所以生产环境偏向G1。
同时会避免在撮合主路径创建大对象,减少晋升老年代概率,防止FullGC抖动。
tips:SYW是指在垃圾回收时暂停所有业务线程的时间,会直接导致系统卡顿,高并发和低延迟系统必须重点控制
1.2 G1为什么适合交易系统?ZGC不行吗?
G1最大优势是可预测停顿时间。
交易系统对 TPS 不如对 P99 延迟敏感,一次 Full GC 卡 1 秒比 QPS 下降更致命。
G1把堆拆成 Region,可以并行回收、按收益优先回收垃圾多的 Region。
在撮合和 WebSocket 推送这种实时系统里,这种设计非常重要
ZGC 追求极致低停顿,通过并发压缩实现近乎无感 GC,但其 CPU 开销和工程成熟度仍在验证阶段;而 G1 在低延迟与稳定性之间取得了最佳平衡,是当前交易系统最稳妥的默认选择
1.3 对象什么时候进入老年代?
常见是对象多次 Minor GC 存活,或者大对象直接进入老年代。
在我们系统里,像订单快照、行情聚合对象,如果设计不当就可能变成“隐形大对象”。
所以会通过对象复用、拆字段、减少生命周期来控制老年代压力
tips:超过 1MB 的对象,基本就算大对象,几十 KB 起就要警惕(在高频创建场景);在G1模式下,对象大小 ≥ 一个 G1 Region 的 50%,就会被认为是大对象会直接进入老年代;
1.4 synchronized 在JVM中是怎么实现的?
synchronized 基于对象头和Monitor实现,经历偏向锁、轻量级锁到重量级锁的升级。
在低竞争场景成本很低,但高竞争会退化成 OS Mutex(内核态互斥锁)。
所以在撮合这种热点逻辑里,我们不依赖 synchronized,而是直接用单线程模型保证一致性
1.5 volatile 能解决什么,不能解决什么?
volatile 只能保证可见性和一定的有序性,不能保证复合操作的原子性。
我一般只用它做状态标记,比如服务是否可用、是否需要降级。
涉及资金、订单这种强一致场景,绝不会只靠 volatile
1.6 HashMap 为什么线程不安全?
核心问题在并发扩容和链表/红黑树转换过程中可能产生死循环或数据覆盖。
所以在并发环境中统一使用 ConcurrentHashMap,避免隐式风险
1.7 ConcurrentHashMap 为什么快?
JDK8 后是 CAS + synchronized,锁粒度在桶级别。
读操作无锁,写冲突才加锁,非常适合高并发读多写少场景,比如行情缓存
1.8 ThreadLocal 的使用场景和坑?
用于 traceId、用户上下文。
最大的坑是线程池 + ThreadLocal,如果不remove 会导致上下文串线甚至内存泄漏。
我们线上排查过类似问题,所以现在是强制规范
1.9 equals / hashCode 为什么必须一起重写?
Hash 容器依赖 hashCode 定位桶,equals 决定是否相等。
在订单、业务 Key 场景如果不一致,会导致数据重复或查不到
1.10 igDecimal 在金融系统中的使用原则?
所有资金、价格、数量统一 BigDecimal。
明确scale和roundingMode,避免隐式精度丢失。
撮合、结算、链上金额转换都严格控制。
1.11 Java 中的内存泄漏常见场景?你怎么排查?
Java 的内存泄漏本质不是“内存回收不了”,而是对象不再使用但仍被引用。
常见场景包括:
静态集合长期持有对象
ThreadLocal 在线程池中未清理
Listener / 回调未反注册
我们线上主要关注 ThreadLocal + 线程池 和 缓存无限增长 两类问题。
排查时一般先看 GC 日志是否频繁 Full GC,再通过 MAT / JProfiler 分析堆中大对象和引用链。
1.12 Java 中的深拷贝和浅拷贝有什么区别?你在哪用过?
在交易系统里,快照语义非常重要。
比如撮合过程中,如果直接把订单对象引用传给下游,很容易被并发修改。
所以在订单快照、撮合事件中,我们会用深拷贝或不可变对象,确保下游消费到的是一致状态
1.13 Java 为什么不推荐 finalize?你们怎么做资源释放?
finalize 执行时机不确定,会增加GC负担,JDK 9 之后也不推荐使用。
我们在项目里统一使用:
try-with-resources
明确的close生命周期
像MQ Producer、Netty Channel 都必须在明确时机释放,避免依赖GC
1.14 Java 中异常体系你是怎么设计的?
我们业务中更偏向 运行时异常 + 业务错误码。
Checked Exception 在复杂链路中会污染方法签名,不利于扩展。
比如撮合失败、余额不足,本质是业务异常,不是系统异常。
所以我们统一封装业务异常,结合错误码向上透传
1.15 Java 序列化有哪些问题?你们是怎么做的?
Java 原生序列化有三个问题:
1️⃣ 性能差
2️⃣ 体积大
3️⃣ 存在反序列化安全风险
在 MQ、RPC 场景中,我们统一使用 JSON / Protobuf,
严禁 Serializable直接上生产链路
1.16 Java 中的不可变对象有什么好处?
不可变对象在并发场景下非常有价值。
在行情、配置、交易对参数等场景,我们更倾向不可变对象:
无需加锁
天然线程安全
减少并发 Bug
复杂系统里,减少状态变化本身就是优化
1.17 Java 中的反射为什么慢?你们怎么用?
反射慢主要因为:
无法内联
有安全检查
我们只在测试工具、框架层使用反射,比如京东项目里的单测辅助工具。
撮合、交易核心路径 禁止反射
1.18 Java 的类加载机制?双亲委派的意义?
安全、避免重复加载,双亲委派保证核心类不会被篡改。
在金融和交易系统里,这是安全基线。
即使自定义 ClassLoader,也不会破坏 JDK 核心类的加载顺序
1.19 Java 中的 SPI 机制了解吗?适合用在什么地方?
服务发现、解耦实现
SPI 适合低频、非核心路径的可插拔能力,比如策略扩展、插件化组件。
在交易核心链路不适合,因为类加载和反射开销不可控
1.20 JIT 编译对高并发系统的影响?
JIT会把热点方法编译成本地代码。
对撮合、行情这种长期运行的服务非常有利。
这也是为什么我们更倾向 常驻服务 + 稳定流量,而不是频繁重启
2.并发 / 中间件 / 分布式(20题)
2.1 你在系统中是如何设计线程模型的?
我们的原则是:线程模型必须贴合业务一致性要求。
比如撮合引擎是单交易对单线程,保证强一致性;
行情推送、MQ 消费属于 IO 或计算型任务,采用线程池并行处理。
通过这种方式,把“必须串行”的地方和“可以并发”的地方彻底分离
2.2 为什么撮合引擎要用单线程?不会成为瓶颈吗?
撮合的瓶颈不是 CPU,而是一致性复杂度。单线程可以避免锁竞争、死锁、ABA 等问题。
扩展性靠交易对维度拆分,不是靠堆线程。这在真实交易所是非常成熟的设计
2.3 线程池参数你是如何设置的?
我们不会套公式,而是按场景来:
IO 型:线程数 > CPU
计算型:线程数 ≈ CPU
所有线程池统一要求:
有界队列
明确拒绝策略
可观测(队列长度、活跃线程)
防止流量突刺直接拖垮JVM
2.4 线程池队列满了怎么办?
队列满本质是系统过载信号,我们不会无脑 CallerRuns。
对交易类请求直接失败并告警;
对非核心任务(报表、统计)可以降级或丢弃。核心原则是:保护核心链路
2.5 Redis 在你们系统中主要承担什么角色?
Redis 在我们系统中不是“万能存储”,而是三类角色:
1️⃣ 加速读(行情、配置)
2️⃣ 分布式锁(支付、防重复)
3️⃣ 排序结构(排行榜)
所有最终状态一定落 MySQL,Redis挂了可以降级,但交易不能错
2.6 你是如何实现分布式锁的?有哪些坑?
分布式锁最基本是 SET NX + 过期时间。
关键不是“能不能加锁”,而是锁失效怎么办。
所以我们用 Redisson 的看门狗机制,并且锁只用于短事务,避免长时间占用。在资金、撮合核心路径尽量避免分布式锁
2.7 Redis 挂了会发生什么?你们怎么应对?
Redis 挂了后:缓存失效,锁不可用
我们的设计是:核心交易不依赖 Redis 最终状态
必要时直接走 DB,行情类服务可直接降级
Redis是加速器,不是地基
2.8 MQ在你们系统中解决什么问题?
MQ 主要解决三件事:
1️⃣ 撮合和结算解耦
2️⃣ 削峰填谷
3️⃣ 异步扩展能力
撮合只负责产生成交事件,后续资金、统计、风控通过 MQ 扩展,不影响撮合性能
2.9 如何保证MQ消息不丢?
我们从两端保证:
生产端:同步发送 + ACK
消费端:业务成功才提交 offset
失败进入重试或死信队列,任何“发出去就不管”的MQ都是不合格的设计
2.10 MQ重复消费你是怎么处理的?
MQ 语义是至少一次,所以业务层必须幂等
常见做法:
业务唯一key
DB唯一索引
Redis去重
京东和交易所场景下我都用过,尤其是在新老系统双写阶段
2.11 你如何保证分布式系统中的数据一致性?
我们不追求强一致覆盖全链路,而是分层处理一致性。
撮合、余额扣减属于强一致,必须串行;
统计、报表、风控是最终一致,通过MQ异步对齐。
通过这种方式,在保证正确性的同时不牺牲性能
2.12 你怎么看分布式事务?在项目中怎么用?
分布式事务在高并发系统里成本很高。
我们更倾向本地事务 + 消息最终一致性。
比如撮合落成交记录,本地事务成功后发MQ,下游失败可以重试或补偿,而不是阻塞主链路
2.13 你了解哪些分布式事务方案?为什么不用 2PC?
2PC、TCC、Saga
2PC 阻塞严重,不适合高并发交易系统。
TCC 侵入性强,开发成本高。
我们实际使用的是 Saga / 事件驱动补偿模型,允许短时间不一致,但必须最终可恢复
2.14 如何保证 MQ 消息的顺序性?
顺序是业务维度问题。
我们以订单号/交易对作为分区 key,同一key的消息进同一分区、同一消费者。
顺序只保证在“需要顺序”的粒度上
2.15 消息堆积你是怎么处理的?
首先区分是生产快还是消费慢。
消费慢:
临时扩容消费者
提升单批处理能力
生产快:限流/削峰
京东订单消息和交易所行情高峰期都遇到过这种问题
2.16 延迟消息你用过吗?适合什么场景?
延迟消息适合状态超时检查
比如:
订单未支付自动关闭
链上充值长时间未确认
相比定时任务,延迟消息更精确、更易扩展
2.17 幂等设计你是如何系统性考虑的?
幂等不是“补丁”,而是设计前置条件。
每个业务操作必须有:
唯一业务 ID
明确的状态机
重复请求只可能:
返回相同结果
被拒绝
在支付、下单、结算里尤为关键
2.18 分布式ID你用过哪些方案?
UUID、Snowflake
高并发系统我们不用UUID,索引不友好。
更偏向Snowflake类方案,保证趋势递增,利于DB索引和分页。
京东内部也有类似方案
2.19 配置中心你是怎么用的?
配置中心用于:
交易对参数
风控阈值
开关控制
所有配置必须支持:
热更新
回滚
避免频繁重启影响交易
2.20 注册中心挂了会怎么样?
注册中心不可用时,已建立的连接仍然可用。
所以关键是:
客户端本地缓存
服务列表容错
不能让注册中心成为单点
3 项目深挖(20题)
3.1 你们 CEX 系统的整体架构是怎样的?
整体是微服务架构,核心拆为四块:
Exchange:撮合引擎,强一致
Market:行情服务,高并发读
Wallet / Settlement:资金结算
Admin / Risk:风控与管理
撮合只产生成交事件,不直接改资金,通过MQ解耦结算,保证撮合低延迟
3.2 撮合引擎为什么一定要和资金解耦?
撮合是整个系统延迟最敏感的模块。
任何DB/RPC/链上操作都会放大尾延迟。
所以撮合只负责:
价格撮合
成交数量计算
成交结果通过 MQ 发送给结算系统,即使结算慢,也不会影响撮合吞吐
3.3 撮合引擎内部的数据结构是怎样的?
每个交易对维护独立订单簿:
买卖盘分别用红黑树
Key 是价格
Value 是同价位 FIFO 队列
这样既能快速找到最优价,又能保证价格优先、时间优先
3.4 市价单和限价单在实现上最大的区别?
限价单可能部分成交,剩余进入订单簿;市价单永远不入簿,只按当前深度吃单。
我们在市价单上做过优化,
利用红黑树减少无效遍历,将撮合耗时从15ms降到 2ms
3.5 高并发下如何保证撮合结果不乱、不重?
单交易对单线程撮合是关键。不同交易对并行,同一交易对严格串行。
这样可以:不加锁,不死锁,不ABA
一致性由线程模型保证,而不是靠锁
3.6 撮合服务如果宕机,怎么恢复?
快照 + 重放。
撮合本身是无状态的:
订单簿定期快照
成交事件持久化在 MQ
重启后:
加载快照
重放未处理的成交消息
不会丢单,也不会多成交
3.7 行情数据是如何产生和推送的?
撮合 → 行情聚合 → 推送
行情源头来自撮合成交事件。
Market 服务订阅成交流,
聚合盘口、K 线、Ticker。
WebSocket 推送采用:
新用户先快照
后续只推增量
控制延迟和带宽
3.8 WebSocket 如何防止慢客户端拖垮系统?
限流 + 主动断开
每个连接都有发送缓冲区上限。一旦积压:丢弃低优先级消息或直接断连
行情是“尽力而为”,不能影响整体系统稳定性
3.9 链上充值你们是怎么做的?
监听链上事件
使用 web3j:启动时扫历史区块补数据,实时订阅新区块,解析 ERC20 / ERC721 Transfer 日志,将链上状态同步到链下账户体系
3.10 链上回滚你们如何处理?
链上交易先标记 pending,达到N个确认数后才 final。
如果检测到 blockHash 不一致:回滚链下状态,重新扫描。所有充值都必须“可重放、可回滚”
3.11 NFT铸造流程你是怎么设计的?
链下发起,链上执行
铸造流程分三步:
1️⃣ 链下校验 & 业务锁
2️⃣ 调用链上合约
3️⃣ 监听事件确认结果
链上是最终状态,链下失败可通过事件重放恢复
3.12 NFT 支付重复下单如何避免?
支付前通过:
分布式锁,订单状态机
保证同一订单只能支付一次。即使 MQ / 回调重复,也不会多扣款
3.13 订单状态机你是怎么设计的?
订单状态只能单向流转:
CREATED → PAID → FINISHED / CLOSED
每次状态变更都校验当前状态,并通过乐观锁防并发写冲突
3.14 京东项目中你是如何落地 DDD 的?
按业务能力拆域:
行为域
订单域
佣金计算域
价保域
每个域内模型自洽,通过事件而不是直接调用交互
3.15 为什么要用策略模式?
佣金规则复杂且变化频繁。使用策略模式把规则拆散,新规则只新增实现,不改原逻辑。这对长期维护非常关键
3.16 京东迁移新老系统时如何做到 0 事故?
上游双写消息,新老系统并行消费。
消息携带唯一 ID,
下游做幂等处理。
确认新系统稳定后再切流
3.17 你们是如何保证单测覆盖率的?
工具 + 规范
我们补齐了私有方法测试工具,引入 JMockit 覆盖复杂分支。
两个人一个月把覆盖率提升到 87%,顺利通过合规审查
3.18 高并发下你们最怕哪类 Bug?
最怕的是:
重复扣款
多成交 / 少成交
所以所有资金、订单逻辑:
明确状态
明确幂等
明确补偿
3.19 你觉得你项目中最难的点是什么?
最难的是在 一致性、性能、可恢复性 之间取平衡。不是追求极致快,而是可控、可恢复
3.20 如果现在让你重构一次撮合系统,你会改什么?
核心思路不变:单交易对单线程,撮合与结算解耦
会补充更多:可观测性;自动降级;更完善的回放工具
4 系统设计 / 场景题(20题)
4.1 让你设计一个现货交易系统,你会怎么拆模块?
我会先按一致性和并发特性拆模块:
Exchange:撮合,强一致,单线程模型
Market:行情,高并发读
Wallet / Settlement:资金结算
Risk:限额、限频、风控
强一致模块尽量少,高并发模块通过缓存和异步扩展
4.2 为什么撮合一定不能直接操作资金?
撮合是延迟最敏感的模块。一旦引入 DB、RPC 或链上操作,尾延迟会被无限放大。
所以撮合只生成成交事实,资金变化通过MQ异步处理,即使结算慢,也不影响撮合吞吐
4.3 如果交易量突然暴涨 10 倍,你系统哪里最先扛不住
通常最先扛不住的是行情和推送链路,而不是撮合本身。
应对方式:
行情降级(减少频率、字段)
WebSocket 连接限流
核心交易优先级最高
交易系统一定要有“止损阀”
4.4 如何设计一个高可用的撮合服务?
撮合服务本身是无状态的:
订单簿内存
定期快照
成交事件持久化
任意节点宕机:
重启加载快照
重放成交事件
可用性靠可恢复性,不是堆机器
4.5 你如何设计订单系统的状态流转?
订单状态只能单向流转,
每次流转校验当前状态。
并发场景下结合:
乐观锁
版本号
防止重复扣款或乱序更新
4.6 Redis 在系统设计中你会放在哪一层?
Redis 只能作为加速层:
行情
配置
临时状态
所有最终状态必须落DB。任何“Redis就是主存”的设计都很危险
4.7 如果 Redis 整个集群不可用,系统如何自保
Redis 不可用时:
核心交易直接走DB
行情服务降级
排行榜等非核心功能关闭
系统设计必须区分:能慢 vs 不能错
4.8 MQ 堆积严重你会怎么处理?
扩容、限流
先判断原因:
消费慢 → 扩容消费者
生产快 → 限流 / 削峰
同时监控:积压长度,消费延迟
必须做到“可观测 + 可干预”。
4.9 如何设计一个幂等的下单接口?
下单必须携带clientOrderId。
服务端:
DB唯一索引
状态机校验
重复请求只能返回相同结果,绝不能生成新订单
4.10 如何设计一个安全的充值提现系统?
多重校验。
充值:
- 链上监听
- 确认数
提现:
- 多重风控
- 冷热钱包
资金系统必须:
- 可追溯
- 可回放
- 可审计
4.11 如何避免超卖或资产为负?
冻结、校验
下单前先冻结资产,撮合只使用可用余额。
所有扣减必须校验版本号,出现异常直接失败而不是修数据
4.12 高并发下如何设计分页和导出?
异步、分批
大数据量导出必须异步,分页查询并行化处理,中间结果落临时存储。
平安项目中把导出从3分钟优化到40秒
4.13 如何设计一个风控系统?
规则 + 实时
风控分两层:
实时风控:限额、限频
离线风控:行为分析
风控永远不能阻塞交易主链路
4.14 如何做系统限流?
令牌桶模型
不同接口不同限流策略:
下单:严格
查询:宽松
限流优先在入口做,防止请求打到核心服务
4.15 如何设计系统的灰度发布?
分批切流
灰度核心是:小流量验证 + 可回滚
京东项目中通过消息双写保证新老系统共存
4.16 系统日志你重点关注哪些?
可追踪,每笔交易必须能串起完整链路:
- 请求
- 撮合
- 成交
- 结算
没有可追踪日志的系统是不可运维的
4.17 如何设计系统的监控指标?
延迟、错误率
重点监控:
- P99 延迟
- 撮合耗时
- MQ 积压
监控是为了提前止损,不是事后复盘
4.18 如果发现成交价格异常波动,你会怎么排查?
数据回溯,第一时间冻结交易对。
回放撮合日志,检查是否异常订单或行情输入。
交易系统必须支持历史回放,且可复现
4.19 如何设计一个可回放的系统?
事件驱动。
核心状态由事件驱动。事件不可变、可持久化。
任何状态都能通过事件重建,这是金融系统的底线能力
4.20 你如何评价一个系统设计的好坏?
我会看三点:
1️⃣ 出问题能不能恢复
2️⃣ 压力下会不会失控
3️⃣ 复杂度是否可维护
好系统不是“从不出错”,而是出错不致命
5.Web3 / 区块链专项(20题)
5.1 你如何理解“链上最终一致性”和 Web2 的区别?
确认数 + 不可逆性
Web2 一致性靠事务和锁,链上一致性靠区块确认数。
链上交易不是“立即成功”,而是 pending → confirmed → final。
所以链下系统必须支持:回滚,重放,延迟确认
5.2 为什么监听链上事件不能只靠 WebSocket?
WS 只适合实时性,不适合完整性。
节点断线、重连都会丢事件。
正确方式是:
- 启动时扫历史区块
- 实时WS补充
两者结合才能保证不漏数据
5.3 ERC20 Transfer 事件你是怎么解析的?
日志解析
通过 web3j 解析 logs:
topics[0] 是 Transfer 签名
topics[1]/[2] 是 from / to
data 是 amount
解析后再映射到链下账户体系
5.4 USDC 为什么适合做结算币?
稳定、规范
USDC:法币锚定 + 小数位固定 + 审计透明
比ETH波动风险小,更适合做交易、预测市场结算
5.5 链上充值如何防止重复入账?
以 txHash + logIndex 作为唯一键。
DB 层做唯一索引。
即使事件重复扫描,也只入账一次
5.6 链上 Reorg 会对你系统产生什么影响?
状态回滚。
Reorg 会导致:
- 已确认交易消失
- 事件顺序变化
所以所有链上数据:
- 先标 pending
- N 个确认后才 final
并支持回滚重扫
5.7 提现为什么一定要走链下审批?
提现是资金外流的唯一通道。
必须:风控,人工或多签
不能完全自动化,否则一旦被攻击就是灾难。
5.8 冷热钱包你如何理解?
风险隔离
热钱包:小额 + 高频
冷钱包:大额 + 离线
热钱包余额不足时,由冷钱包定向补充。
5.9 智能合约升级你怎么看?
合约升级必须极度克制。
通常采用:Proxy模式 或新合约迁移
所有升级都要:
公告,时间锁,可审计
5.10 Solidity 中最容易出安全问题的点?
重入、精度
常见问题:重入攻击、精度溢出、授权滥用
所以写合约时:
Checks-Effects-Interactions、权限最小化
5.11 为什么很多逻辑不放在链上?
成本、效率
链上:成本高、延迟大;所以复杂业务逻辑放链下
链上只做:状态结算、关键校验
5.12 预测市场为什么适合用 USDC?
价格直观
预测市场的价格本身就是概率。
用稳定币结算,用户更容易理解赔率和收益
5.13 你如何设计链下 + 链上数据一致性?
事件驱动
链上是最终裁决者。
链下状态必须:
- 可回放
- 可对账
不一致时,以链上为准自动修正
5.14 为什么链下系统必须支持重扫区块?
恢复能力
因为节点、网络、程序都会出问题。
唯一可信的是链上历史。
能重扫,系统才算“活着”
5.15 Gas 费用波动你如何处理?
通过:
动态 Gas Price
延迟执行
在不影响用户体验的前提下控制成本
5.16 多链支持你会怎么设计?
抽象链适配层:
地址,事件,交易
业务逻辑不感知具体链,只关心标准接口
5.17 Web3 系统中最难排查的 Bug 是什么?
链上慢、异步、可回滚,Bug 往往不是“错”,而是“没等到”。
所以必须有完整状态机和超时机制
5.18 Web3 项目如何做风控?
链上 + 链下
链上防合约漏洞,链下防女巫、套利、刷子。
风控永远是多层的
5.19 Web3 后端和传统后端最大的不同?
Web3 后端面对的是:不可信输入、不可控延迟、不可回滚成本
所以设计上更保守、更强调恢复能力
5.20 你为什么适合做 Web3 后端?
我有传统金融级后端的工程经验,同时理解链上系统的不确定性。
能把Web2的稳定性和 Web3的开放性结合起来,这是我最大的优势。