公司背景
xTransfer:跨境支付收款平台,类似跨境支付宝。业务涉及多国渠道对接、国际化 USDT 支付、B2B 收款。是典型的分布式高并发场景,面试问题侧重点也在分布式和高可用。
一面
如何保证幂等性
幂等 = 同一个操作执行多次的结果和执行一次一样。在支付场景中是底线级要求——重复扣款是不被接受的。
实际方案:
// 1. 数据库唯一索引 —— 最可靠
// insert into orders(id, amount) values(?, ?)
// 重复插入 → DuplicateKeyException,直接返回已有结果
// 2. Token 机制 —— 适合复杂操作
// 提交表单前先获取 token,携带 token 提交
String token = redis.setnx("idempotent:" + orderId, "1", 300);
if (token == null) return "重复提交";
try {
// 业务处理
} finally {
// token 过期自动清理,或者处理完后删除
}
// 3. 状态机 —— 利用数据库行锁
// UPDATE orders SET status='PAID' WHERE id=? AND status='UNPAID'
// 第二次执行 affected_rows=0,直接返回
幂等 key 设计:业务类型:业务ID:操作时间窗口,例如 payment:order_12345:20230101
TraceId 如何传递
分布式链路追踪的核心——一个请求从网关到微服务 A 到微服务 B 到数据库,需要一条串联所有日志的 ID。
入口: 网关/MDC Filter 生成 traceId
request.setAttribute("traceId", UUID.randomUUID().toString());
跨线程: MDC (Mapped Diagnostic Context)
MDC.put("traceId", traceId); // 存在 ThreadLocal 中
跨服务: RPC 框架的 Filter 机制
- Dubbo: RpcContext 附件传递
- gRPC: Metadata (HTTP Header)
- Feign: RequestInterceptor 注入 Header
- MQ: 消息 Header 中携带
日志输出:
[traceId=abc123][spanId=1] 用户登录成功
[traceId=abc123][spanId=2] 查询用户信息耗时 15ms
关键点:线程池切换时 MDC 会丢失——因为 MDC 基于 ThreadLocal。解决方式是用装饰器在线程切换时手动传递:
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
try {
MDC.setContextMap(context);
runnable.run();
} finally {
MDC.clear();
}
};
}
}
RPC 注册和发现流程
┌──────────────┐
┌──────▶│ 注册中心 │◀──────┐
│ │ (Nacos/ZK) │ │
│ └──────────────┘ │
① 启动时注册 │ │ ① 启动时注册
(IP:Port) ② 拉取列表 │ ② 拉取列表 (IP:Port)
│ │ │ │
┌─────┴──┐ │ │ ┌───┴─────┐
│Provider│◀───┘ └──▶│ Consumer │
└────────┘ └──────────┘
③ RPC 调用
心跳 & 下线:
- Provider 定时发送心跳到注册中心
- 心跳超时 → 注册中心标记 Provider 为不健康
- Provider 优雅停机 → 主动从注册中心摘除 → 等待处理完现有请求 → 关闭
- Consumer 通过订阅机制感知 Provider 列表变化
线程池核心参数
ThreadPoolExecutor 的 7 个参数:
corePoolSize 核心线程数 —— 池中保留的线程数(即使空闲也不回收)
maximumPoolSize 最大线程数 —— 池中最多能容纳的线程数
keepAliveTime 空闲线程存活时间 —— 非核心线程空闲多久后回收
unit 时间单位
workQueue 工作队列 —— 核心线程忙时,任务进队列
threadFactory 线程工厂 —— 自定义线程前缀
handler 拒绝策略 —— 队列满 + 线程满时的处理方式
任务执行流程:
新任务 → 核心线程有空闲? → YES → 执行
│
NO
▼
队列有空位? → YES → 入队等待
│
NO
▼
线程数 < max? → YES → 创建新线程执行
│
NO
▼
执行拒绝策略:
AbortPolicy → 抛异常(默认)
CallerRunsPolicy → 调用者线程执行
DiscardPolicy → 静默丢弃
DiscardOldestPolicy → 丢弃队首,重试
二面
Dubbo 核心抽象与调用流程
Dubbo 核心接口:
Invoker: 可执行的对象(Provider 端的服务实现 / Consumer 端的远程代理)
Invocation: 一次 RPC 调用的参数封装
Protocol: 协议的抽象(dubbo:// / rest:// / gRPC://)
Exporter: Provider 端暴露服务的对象
Filter: 拦截器链(类似 Servlet Filter)
Consumer 调用 Provider 流程:
Consumer:
Proxy → Cluster (容错) → Directory (路由/负载均衡) → Filter Chain → Protocol → Transport → Client
│
TCP 通信 │
│
Provider:
Server → Transport → Protocol → Filter Chain → Invoker → 业务实现
String vs StringBuilder vs StringBuffer
| 类型 | 可变性 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|---|
| String | 不可变 | 是(final class) | 拼接时每拼一次生成一个新对象 | 少量拼接、作为 key |
| StringBuilder | 可变 | 否 | 快 | 单线程拼接(99% 的日常开发) |
| StringBuffer | 可变 | 是(synchronized) | 慢 | 多线程拼接(极少用) |
String + String 底层实现:
String s = "a" + "b" + "c";
// 编译后:String s = "abc"; (编译器常量折叠)
String s = a + b;
// 编译后:new StringBuilder().append(a).append(b).toString();
// JDK 9+: 使用 StringConcatFactory + invokedynamic
synchronized 如何保证线程安全
JVM 层面:对象头中的 Mark Word 记录锁状态。使用 monitorenter 和 monitorexit 字节码指令。
JDK 1.6 之后的锁升级:
偏向锁 → 轻量级锁(CAS) → 重量级锁(OS mutex)
│ │ │
一个线程 少量竞争 大量竞争
偏向锁: 在 Mark Word 中记录线程 ID,下次同一线程进入直接放行
轻量级锁: CAS 在 Mark Word 和线程栈之间拷贝 Lock Record
重量级锁: 向 OS 申请互斥量,未获得锁的线程进入阻塞队列
Spring 容器启动流程
1. 加载配置(XML / @Configuration / 包扫描)
↓
2. BeanDefinition 注册
- 解析生成 BeanDefinition 对象
- 注册到 BeanDefinitionRegistry
↓
3. BeanFactoryPostProcessor 执行
- 对 BeanDefinition 做后置处理(如 Placeholder 替换 ${...})
↓
4. Bean 实例化
- 反射创建对象(此时属性未赋值)
↓
5. Bean 属性填充
- @Autowired / @Value 注入
↓
6. BeanPostProcessor 前置处理
↓
7. InitializingBean 的 afterPropertiesSet()
↓
8. BeanPostProcessor 后置处理(AOP 代理生成在这一步)
↓
9. Bean 就绪
三面(HR + 主管)
OOM 排查方法
现象: 应用卡死 / 频繁 Full GC / OOM 异常日志
排查流程:
1. 看日志
- grep "OutOfMemoryError" app.log
- 确认是哪种 OOM(heap / Metaspace / Direct buffer)
2. 保留现场
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dump/
3. 分析 dump
- MAT (Memory Analyzer Tool): 看占用最大的对象
- jmap -histo:live <pid> | head -20 ← 快速看 Top 20 类
- jcmd <pid> GC.heap_dump /tmp/heap.hprof
4. 通过日志定位
- 每分钟打印一次 jstat -gcutil,看各区使用率趋势
- Full GC 频率从 1次/分钟 涨到 10次/分钟 → 内存泄漏
5. 措施
- 泄漏: 找出大对象 → GC Root 路径 → 修复代码
- 不够: 加大 -Xmx / 加机器
OOM 可能出现的分区
堆 OOM:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
对策: 增大 Xmx / dump 分析 / 修复内存泄漏
元空间 OOM:
OOM: Metaspace
对策: 增大 MaxMetaspaceSize / 减少动态类生成
栈 OOM:
StackOverflowError: 递归太深
OOM: unable to create new native thread: 线程数太多
对策: -Xss 调整栈大小 / 线程池限制最大线程数
直接内存 OOM:
OOM: Direct buffer memory
对策: -XX:MaxDirectMemorySize / 复用 ByteBuffer
设计模式
在 Spring 框架中的体现:
| 模式 | Spring 中的体现 |
|---|---|
| 单例 | Bean 默认 scope = singleton |
| 工厂 | BeanFactory / ApplicationContext |
| 代理 | AOP(JDK 动态代理 / CGLIB) |
| 模板方法 | JdbcTemplate、RestTemplate |
| 观察者 | ApplicationListener / @EventListener |
| 策略 | Resource 接口的不同实现 |
| 责任链 | Filter Chain、Interceptor Chain |
Provider 宕机,Consumer 如何及时发现
几种机制各有时延:
1. 注册中心心跳检测 → 标记下线 → 通知 Consumer
延迟: 心跳间隔 + 容错次数 (通常 15-30 秒)
2. Consumer 端 RPC 调用超时
延迟: timeout 时间 (通常 1-5 秒)
触发: 标记节点为不健康,暂时从路由列表移除
3. 主动摘除
Provider 主动发送下线通知到注册中心
延迟: 几秒
总结
xTransfer 面试的特点:一面偏分布式基础(幂等、TraceId、RPC),二面偏框架深度(Dubbo 源码级理解、Spring 容器),三面偏排查和设计能力(OOM 排查、设计模式应用场景)。
跨境支付业务对分布式一致性和幂等性要求高,所以一面就问了幂等性——这不是八股文,是业务刚需。
☕ 如果这篇文章对你有帮助
欢迎请我喝杯咖啡支持一下
评论