Java大厂面试系列 - 电商支付系统架构篇
🎭场景设定:某互联网大厂电商事业部,Java高级工程师岗位面试 👔面试官:张总监(10年架构经验,严肃认真) 😅候选人:谢飞机(自称全栈高手,简历写得很华丽)
第一轮面试:基础功底摸底
张总监(翻看简历):谢飞机是吧?简历上写着精通Spring全家桶,还有高并发电商项目经验?
谢飞机(挺胸):是的张总!我之前在某电商公司做过秒杀系统,日活百万那种!
张总监:好,那我们从基础开始。你说说Spring Boot的自动配置原理是什么?@SpringBootApplication注解背后做了哪些事情?
谢飞机:这个我知道!@SpringBootApplication是个组合注解,里面包含了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。自动配置就是通过spring.factories文件加载配置类,然后用条件注解判断是否生效。
张总监(点头):不错,基本原理说清楚了。那我问你,Spring Boot 2.7版本之后,自动配置的加载方式有什么变化?
谢飞机:呃...这个...应该是有变化的,好像改了文件位置?具体记不太清了...
张总监:行,那我换个问题。在我们的电商系统中,订单服务需要调用库存服务和用户服务,你会用什么方式实现服务间调用?OpenFeign的底层原理是什么?
谢飞机:用OpenFeign啊!声明式调用,写个接口加个注解就行。底层原理嘛...是基于动态代理,把接口方法转成HTTP请求...
张总监:动态代理只是其中一层。Feign Client的创建流程你清楚吗?它和Ribbon是怎么配合做负载均衡的?
谢飞机:这个...我知道能负载均衡,具体实现细节...我主要关注业务代码,底层没怎么看...
张总监(记笔记):好,最后一个问题。你在秒杀系统中是怎么做库存扣减的?数据库层面如何保证超卖问题?
谢飞机:先查库存,库存够就减一,然后创建订单。用数据库乐观锁,version字段控制!
张总监:如果并发量很大,单纯用数据库乐观锁会有什么问题?你有没有考虑过用Redis预扣减?
谢飞机:Redis...用过,就是用decr原子操作,库存预热到Redis,扣减成功再写数据库...大概这么个流程。
第二轮面试:架构设计深挖
张总监:刚才聊到Redis,我想深入了解一下。在支付场景中,用户发起支付请求后,如果网络超时导致用户重复点击,你怎么保证不会重复扣款?说说幂等性设计。
谢飞机:生成支付流水号,用唯一索引控制,重复请求直接返回之前的结果!
张总监:如果用分布式锁来实现,Redisson的看门狗机制了解吗?锁的续期是怎么做的?
谢飞机:看门狗...就是会自动续期吧?默认30秒,每10秒续一次?应该是这样...
张总监:那Redis集群模式下,分布式锁可能有什么问题?Redisson是怎么解决的?
谢飞机(冒汗):集群模式...主从切换的时候可能会丢锁?RedLock算法...大概就是向多个节点加锁,超过半数成功才算成功?
张总监:嗯,说对了一部分。那我再问你,我们的支付系统需要对接多个支付渠道(微信、支付宝、银联),你会怎么设计支付路由策略?用什么设计模式?
谢飞机:用策略模式!定义统一的支付接口,每个渠道实现一下,然后...可以用工厂模式根据参数创建具体的支付实现...
张总监:不错。那在支付回调处理中,如果回调消息乱序到达(比如退款回调比支付成功回调先到),你怎么处理?
谢飞机:这个...用状态机?支付状态要有流转顺序,不符合状态机的回调就丢弃?
张总监:基本思路对了。那我问个具体的技术问题,Spring的状态机框架用过吗?怎么集成到项目中的?
谢飞机:没...没用过Spring StateMachine,我们是自己写的状态枚举加校验...
第三轮面试:生产实战拷问
张总监:最后问几个生产环境的问题。线上支付系统出现大面积超时,你的排查思路是什么?会用到哪些监控工具?
谢飞机:先看监控大盘!用Prometheus和Grafana看系统指标,然后看ELK日志...如果是数据库慢,看慢查询日志;如果是GC问题,看GC日志...
张总监:那如果用Arthas进行在线诊断,怎么快速定位到慢方法?Thread命令和Trace命令分别怎么用?
谢飞机:Arthas...用过一点。thread -n 3看最忙的线程,trace命令跟踪方法耗时...
张总监:好。最后一个问题,我们的支付系统每天有上亿笔交易流水,数据库单表已经扛不住了,你会怎么设计分库分表方案?用什么中间件?
谢飞机:用ShardingSphere!按用户ID分片,保证同一个用户的订单在同一个分库分表中...这样关联查询方便。
张总监:分片键选用户ID确实不错。那分库分表之后,全局唯一的订单号怎么生成?雪花算法的原理你清楚吗?
谢飞机:雪花算法...就是时间戳+机器ID+序列号,64位长整型。具体怎么实现...我只知道用现成的...
张总监(合上笔记本):好的,谢飞机,今天的面试就到这里。你对一些基础概念有了解,但深度还需要加强,特别是源码层面和生产实战经验。回去等通知吧,有消息HR会联系你。
谢飞机:好的张总!我回去一定好好补补源码!等您消息!
📚 面试题目详细解析
第一轮答案详解
1. Spring Boot自动配置原理详解
业务场景:在电商系统中,我们引入Spring Boot后,只需要少量配置就能快速启动应用,这是因为Spring Boot提供了大量自动配置。
技术原理:
@SpringBootApplication是组合注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { ... }) public @interface SpringBootApplication { }自动配置核心流程:
// 1. @EnableAutoConfiguration 通过 @Import 导入配置 @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { } // 2. AutoConfigurationImportSelector 加载配置 public class AutoConfigurationImportSelector implements DeferredImportSelector { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { // 获取所有自动配置类 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); return configurations.toArray(new String[0]); } }Spring Boot 2.7+的重大变化:
# 旧版本(2.7之前):加载 spring.factories META-INF/spring.factories # 新版本(2.7+):改为加载新文件 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports配置类的条件注解:
@AutoConfiguration @ConditionalOnClass(DataSource.class) // 类路径存在DataSource才生效 @ConditionalOnMissingBean(DataSource.class) // 容器中没有DataSource才生效 @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { @Bean @ConditionalOnMissingBean public DataSource dataSource(DataSourceProperties properties) { // 自动配置数据源 return DataSourceBuilder.create() .url(properties.getUrl()) .username(properties.getUsername()) .password(properties.getPassword()) .build(); } }2. OpenFeign调用原理与负载均衡
业务场景:在电商系统中,订单服务需要调用库存服务扣减库存、调用用户服务查询用户信息,OpenFeign让这些远程调用像本地方法一样简单。
使用示例:
// 1. 定义Feign客户端接口 @FeignClient(name = "inventory-service", fallback = InventoryFallback.class) public interface InventoryClient { @PostMapping("/api/inventory/deduct") Result<Boolean> deductInventory(@RequestBody InventoryDTO dto); @GetMapping("/api/inventory/{skuId}") Result<InventoryVO> getInventory(@PathVariable("skuId") Long skuId); } // 2. 降级处理 @Component public class InventoryFallback implements InventoryClient { @Override public Result<Boolean> deductInventory(InventoryDTO dto) { return Result.fail("库存服务降级"); } @Override public Result<InventoryVO> getInventory(Long skuId) { return Result.fail("库存服务降级"); } } // 3. 在订单服务中使用 @Service public class OrderService { @Autowired private InventoryClient inventoryClient; public Result createOrder(OrderDTO orderDTO) { // 调用库存服务,就像调用本地方法 Result<Boolean> result = inventoryClient.deductInventory( new InventoryDTO(orderDTO.getSkuId(), orderDTO.getQuantity()) ); if (!result.isSuccess()) { return Result.fail("库存扣减失败"); } // 创建订单逻辑... return Result.success(orderId); } }OpenFeign创建流程:
1. @EnableFeignClients 扫描带有 @FeignClient 的接口 2. FeignClientFactoryBean 为每个接口创建代理对象 3. 调用时动态代理拦截方法调用 4. Contract 解析方法上的注解(@GetMapping等) 5. RequestTemplate 构建HTTP请求模板 6. Client.execute() 发送HTTP请求(默认使用HttpURLConnection) 7. 与Ribbon集成,实现负载均衡3. 秒杀系统的库存扣减方案
业务场景:秒杀活动商品库存100件,但可能有10万用户同时抢购,需要保证不会超卖。
Redis预扣减方案:
@Service public class SeckillService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private RabbitTemplate rabbitTemplate; // 秒杀接口 public Result seckill(Long userId, Long activityId) { String lockKey = "seckill:lock:" + activityId; String stockKey = "seckill:stock:" + activityId; // 1. 使用分布式锁防止同一用户重复秒杀 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey + ":" + userId, "1", 10, TimeUnit.SECONDS); if (!locked) { return Result.fail("请勿重复提交"); } try { // 2. Redis原子扣减库存 Long stock = redisTemplate.opsForValue().decrement(stockKey); if (stock < 0) { // 3. 库存不足,恢复库存 redisTemplate.opsForValue().increment(stockKey); return Result.fail("已被抢光"); } // 4. 发送MQ消息,异步创建订单 SeckillMessage message = new SeckillMessage(userId, activityId); rabbitTemplate.convertAndSend("seckill.exchange", "seckill.order", message); return Result.success("秒杀成功,订单创建中"); } finally { redisTemplate.delete(lockKey + ":" + userId); } } }最终方案对比:
| 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 数据库乐观锁 | 实现简单 | 性能差 | 低并发 | | Redis预扣减 | 高性能 | 需要额外维护Redis | 高并发秒杀 | | 分段锁 | 并行度高 | 实现复杂 | 超高并发 |
第二轮答案详解
4. 支付系统幂等性设计
业务场景:用户在支付页面点击"确认支付",由于网络抖动,用户多次点击,系统不能重复扣款。
基于支付流水号的幂等方案:
@Service public class PaymentService { @Autowired private PaymentOrderMapper paymentOrderMapper; @Transactional public Result<PaymentResult> createPayment(PaymentRequest request) { // 1. 生成全局唯一支付流水号 String payNo = generatePayNo(request); // 2. 先查询是否已存在该流水号 PaymentOrder existing = paymentOrderMapper.selectByPayNo(payNo); if (existing != null) { // 已存在,直接返回之前的结果 return Result.success(existing.getPaymentResult()); } // 3. 创建支付订单(唯一索引保证幂等) PaymentOrder paymentOrder = new PaymentOrder(); paymentOrder.setPayNo(payNo); paymentOrder.setOrderNo(request.getOrderNo()); paymentOrder.setAmount(request.getAmount()); paymentOrder.setStatus(PayStatus.INIT); try { paymentOrderMapper.insert(paymentOrder); } catch (DuplicateKeyException e) { // 唯一索引冲突,并发场景下另一个请求已创建 existing = paymentOrderMapper.selectByPayNo(payNo); return Result.success(existing.getPaymentResult()); } // 4. 调用支付渠道 PaymentResult result = callPaymentChannel(paymentOrder); // 5. 更新支付结果 paymentOrder.setStatus(result.getStatus()); paymentOrder.setResult(result); paymentOrderMapper.updateById(paymentOrder); return Result.success(result); } }5. Redisson分布式锁与看门狗机制
业务场景:订单超时未支付需要取消,多个实例同时执行定时任务,需要分布式锁保证同一订单只被处理一次。
看门狗(WatchDog)机制原理:
// RedissonLock核心代码 public class RedissonLock implements RLock { // 看门狗默认超时时间:30秒 private long lockWatchdogTimeout = 30 * 1000; private void renewExpiration() { // 开启定时任务续期 Timeout task = commandExecutor.getConnectionManager() .newTimeout(new TimerTask() { @Override public void run(Timeout timeout) { // 每10秒(lockWatchdogTimeout / 3)续期一次 renewExpirationAsync(threadId); // 递归调用,持续续期 renewExpiration(); } }, lockWatchdogTimeout / 3, TimeUnit.MILLISECONDS); } }6. 支付路由策略设计
业务场景:电商平台需要对接微信、支付宝、银联等多个支付渠道,需要根据金额、用户、优惠活动等因素智能选择支付渠道。
策略模式实现:
// 1. 定义支付策略接口 public interface PaymentStrategy { // 支付渠道 PaymentChannel getChannel(); // 是否支持该支付请求 boolean support(PaymentContext context); // 执行支付 PaymentResult pay(PaymentOrder order); } // 2. 支付路由器 @Component public class PaymentRouter { @Autowired private List<PaymentStrategy> strategies; public PaymentStrategy route(PaymentContext context) { // 1. 根据用户指定的渠道 if (context.getChannel() != null) { return strategies.stream() .filter(s -> s.getChannel() == context.getChannel()) .filter(s -> s.support(context)) .findFirst() .orElseThrow(() -> new BizException("指定渠道不支持")); } // 2. 默认选择最优渠道 return strategies.stream() .filter(s -> s.support(context)) .min(Comparator.comparing(s -> s.getFee(context.getAmount()))) .orElseThrow(() -> new BizException("无可用支付渠道")); } }第三轮答案详解
7. 线上支付超时排查与Arthas诊断
Arthas常用命令:
# 1. 查看最忙的线程(CPU占用最高) thread -n 3 # 2. 查看阻塞线程 thread -b # 3. 方法耗时追踪(trace命令) trace com.example.PaymentService createPayment -n 5 --skipJDKMethod false # 4. 方法入参出参监控(watch命令) watch com.example.PaymentService createPayment '{params, returnObj}' -x 3 # 5. 火焰图分析(找出性能瓶颈) profiler start profiler stop --format html8. 分库分表方案与雪花算法
雪花算法(Snowflake)原理:
64位ID结构: ┌─────────────────────────────────────────────────────────────────┐ │ 0 │ 00000000 00000000 00000000 00000000 00000000 0 │ 00000 00000 │ 000000000000 │ │ │ 41位时间戳 │ 5位数据中心 │ 5位机器ID │ 12位序列号 │ └─────────────────────────────────────────────────────────────────┘ 各部分含义: - 1位符号位:始终为0 - 41位时间戳:毫秒级,可使用约69年 - 5位数据中心:支持32个数据中心 - 5位机器ID:每个数据中心支持32台机器 - 12位序列号:每毫秒支持4096个ID分布式ID方案对比:
| 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | UUID | 简单、无依赖 | 无序、太长 | 日志追踪等 | | 数据库自增 | 有序、简单 | 性能瓶颈、单点故障 | 低并发 | | Redis自增 | 高性能 | 需要额外维护Redis | 中高并发 | | 雪花算法 | 高性能、有序、全局唯一 | 依赖时钟、需要规划机器ID | 高并发生产环境 |
📝 面试总结
学习建议
- 深入源码:不能只停留在会用的层面,要理解框架的设计思想
- 实战积累:多参与生产环境问题排查,积累实战经验
- 系统学习:建立完整的知识体系,不能只学碎片化知识
- 关注新技术:保持学习热情,跟上技术发展趋势
💡面试经验:大厂面试不仅考察基础知识,更看重解决问题的思路和深度。简历上写的技术栈,一定要做到真正掌握,而不是停留在"用过"的层面。
本文由Java面试宝典整理发布,欢迎关注获取更多面试干货!🚀