公司背景

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 记录锁状态。使用 monitorentermonitorexit 字节码指令。

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 排查、设计模式应用场景)。

跨境支付业务对分布式一致性和幂等性要求高,所以一面就问了幂等性——这不是八股文,是业务刚需。