1. 电商场景下的Java技术栈实战解析
最近帮一位准备大厂面试的朋友复盘电商项目的技术方案,发现很多候选人对"电商全技术栈"的理解停留在表面。实际上,大厂面试官更关注技术选型背后的业务适配性。以商品秒杀场景为例,单纯说"用Redis做缓存"远远不够,需要讲清楚为什么选择Redis而不是其他方案,以及如何解决随之而来的新问题。
电商系统的技术难点往往集中在高并发、数据一致性、系统扩展性三个维度。去年双十一某头部电商的订单创建峰值达到58.3万笔/秒,这种量级下任何技术细节的疏忽都会导致灾难性后果。下面结合我参与过的多个电商中台项目,拆解大厂面试中最常深挖的5个技术场景。
提示:大厂面试官通常会要求候选人用STAR法则(Situation-Task-Action-Result)描述技术方案,建议准备案例时按这个结构组织
1.1 商品详情页的极致优化方案
商品页是流量入口也是性能瓶颈点。某次压测中发现,当QPS超过2万时,传统方案会出现以下问题:
- 动态渲染导致TP99飙升到800ms+
- 数据库连接池耗尽
- 静态资源加载缓慢
我们最终采用的四级缓存架构:
- 客户端缓存:利用HTTP Cache-Control头设置max-age=300
- Nginx静态化:将商品基础信息生成HTML片段,更新时通过Purge指令清除
- Redis集群:存储商品实时数据(库存、价格),采用Hash结构节省内存
- 本地缓存:使用Caffeine缓存热点数据,设置软引用防止OOM
关键参数配置示例:
// Caffeine配置 LoadingCache<String, ItemDTO> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(5, TimeUnit.MINUTES) .softValues() .build(key -> itemService.getItem(key));避坑经验:
- 避免缓存穿透:对不存在的商品ID缓存空值,设置较短过期时间
- 缓存更新策略:采用Binlog监听+消息队列保证最终一致性
- 压测时发现:本地缓存大小超过JVM堆内存30%会导致频繁GC
1.2 分布式锁在库存扣减中的应用
超卖问题是电商系统的基本功。某次大促时因锁实现不当,导致100件商品卖出132单。经过复盘,总结出分布式锁的四个核心要点:
- 互斥性:使用Redis的SETNX命令实现
SET lock_key unique_value NX PX 30000 - 可重入性:通过ThreadLocal记录持有次数
- 自动续期:后台线程定期延长锁过期时间
- 容灾处理:引入Zookeeper作为备用锁服务
完整扣减流程:
public boolean deductStock(Long itemId, int num) { String lockKey = "stock_lock:" + itemId; String token = UUID.randomUUID().toString(); try { // 获取锁 while(!redisTemplate.opsForValue().setIfAbsent(lockKey, token, 30, TimeUnit.SECONDS)) { Thread.sleep(100); } // 查询库存 Integer stock = stockMapper.selectById(itemId); if (stock < num) return false; // 扣减库存 stockMapper.update(new LambdaUpdateWrapper<Stock>() .setSql("stock = stock - " + num) .eq(Stock::getItemId, itemId) .ge(Stock::getStock, num)); return true; } finally { // 释放锁 - 使用Lua脚本保证原子性 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), token); } }常见面试问题:
- 为什么不用数据库行锁?(答:在分布式系统无法满足需求)
- 如何解决锁过期但业务未执行完的问题?(答:守护线程定时续期)
- Redis主从切换会导致锁失效怎么处理?(答:RedLock算法或切换Zookeeper)
2. 微服务架构下的技术挑战
2.1 订单服务的分库分表实践
当订单表达到千万级时,我们采用基因法分库分表:
- 分片键:用户ID后4位 mod 16
- 基因扩散:将分片键冗余到订单ID中(雪花算法改造)
- 查询优化:建立用户ID到分片位置的映射表
分库分表后的查询示例:
// 根据订单ID路由 public Order getById(Long orderId) { int shardKey = (orderId >> 16) & 0xF; // 提取基因位 String dsName = "order_db_" + shardKey; DynamicDataSourceContextHolder.setDataSource(dsName); return orderMapper.selectById(orderId); }踩坑记录:
- 分布式事务:最终采用本地消息表+定时任务补偿
- 全局唯一ID:改造雪花算法,保留分片信息
- 跨库JOIN:通过数据冗余和异步加载解决
2.2 分布式事务的妥协艺术
电商场景下完全遵循ACID成本过高。我们的实践方案:
| 场景 | 方案 | 一致性级别 | 实现要点 |
|---|---|---|---|
| 创建订单 | Saga模式 | 最终一致 | 每个步骤提供补偿接口 |
| 支付回调 | 本地消息表 | 最终一致 | 幂等设计+异步对账 |
| 库存预占 | TCC(Try-Confirm-Cancel) | 强一致 | 预留资源+定时释放 |
| 物流状态更新 | 最大努力通知 | 弱一致 | 重试机制+人工干预通道 |
TCC模式示例代码:
// Try阶段 public boolean reserveStock(Long itemId, int num) { return stockMapper.update(new LambdaUpdateWrapper<Stock>() .setSql("reserved_stock = reserved_stock + " + num) .eq(Stock::getItemId, itemId) .ge(Stock::getAvailableStock, num)) > 0; } // Confirm阶段 public boolean confirmStock(Long itemId, int num) { return stockMapper.update(new LambdaUpdateWrapper<Stock>() .setSql("stock = stock - " + num + ", reserved_stock = reserved_stock - " + num) .eq(Stock::getItemId, itemId) .ge(Stock::getReservedStock, num)) > 0; } // Cancel阶段 public boolean cancelStock(Long itemId, int num) { return stockMapper.update(new LambdaUpdateWrapper<Stock>() .setSql("reserved_stock = reserved_stock - " + num) .eq(Stock::getItemId, itemId) .ge(Stock::getReservedStock, num)) > 0; }3. 高并发场景下的缓存设计
3.1 多级缓存架构实现
某促销活动期间,商品详情页QPS突破50万,我们设计的缓存方案:
- 客户端缓存:ETag协商缓存
- CDN边缘缓存:静态资源分发
- Nginx缓存:Lua脚本实现动态内容缓存
- Redis集群:一主多从+读写分离
- 本地缓存:Caffeine结合Spring Cache
缓存更新策略对比:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Cache Aside | 实现简单 | 存在不一致时间窗口 | 读多写少 |
| Write Through | 数据一致性高 | 写入性能较低 | 写多读少 |
| Write Behind | 写入性能极高 | 可能丢失更新 | 允许延迟更新的场景 |
3.2 热点Key发现与处理
通过监控发现,某爆款商品占用了Redis 30%的流量。解决方案:
- 实时监控:使用Redis的MONITOR命令采样分析
- 本地缓存:对热点Key进行客户端缓存
- 数据分片:将热点Key拆分为多个子Key
- 限流保护:对单个Key的访问进行令牌桶限流
热点Key处理代码示例:
// 结合Hystrix实现熔断 @HystrixCommand(fallbackMethod = "getItemFallback") public ItemDTO getItem(Long id) { // 先查本地缓存 ItemDTO item = localCache.get(id); if (item != null) return item; // 再查Redis String key = "item:" + id; item = redisTemplate.opsForValue().get(key); if (item == null) { // 查数据库并回填缓存 item = itemMapper.selectById(id); redisTemplate.opsForValue().set(key, item, 5, TimeUnit.MINUTES); } // 回填本地缓存 localCache.put(id, item); return item; } public ItemDTO getItemFallback(Long id) { // 降级策略:返回基础信息 return new ItemDTO(id, "默认商品", 0, 0); }4. 面试中的架构设计题
4.1 如何设计一个秒杀系统
这是大厂高频考题,完整方案应包括:
流量削峰:
- 答题验证码
- 异步排队(使用Kafka)
- 分层过滤(静态数据校验→分布式锁→库存检查)
库存预热:
- 提前将库存加载到Redis
- 采用分段扣减策略
- 设置售罄标记快速返回
熔断降级:
- 监控核心指标(QPS、成功率、RT)
- 设置多级阈值
- 自动切换降级策略
架构示意图:
用户端 → 接入层(Nginx限流) → 应用层(队列处理) → 服务层(库存扣减) → 数据层(Redis+MySQL)4.2 微服务治理要点
面试官常考察对微服务完整生命周期的理解:
服务通信:
- RESTful API设计规范
- gRPC性能优化
- 消息格式演进方案
服务发现:
- Nacos与Eureka对比
- 健康检查策略
- 元数据路由
容错处理:
- 熔断器模式
- 降级策略
- 重试机制
配置示例(Spring Cloud Alibaba):
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 metadata: version: v1 sentinel: transport: dashboard: localhost:8080 datasource: ds1: nacos: server-addr: 127.0.0.1:8848 dataId: ${spring.application.name}-flow-rules rule-type: flow5. 性能优化实战技巧
5.1 JVM层优化
某次Full GC频繁导致接口超时,通过以下步骤解决:
诊断工具:
- jstat -gcutil 监控内存使用
- Arthas的dashboard命令
- GC日志分析
关键参数:
-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 -XX:+HeapDumpOnOutOfMemoryError优化效果:
- GC时间从1.2s降至200ms
- 吞吐量提升40%
5.2 MySQL优化案例
慢查询日志中发现某个订单查询需要3秒,优化过程:
原始SQL:
SELECT * FROM orders WHERE user_id=123 AND status IN (1,2,3) ORDER BY create_time DESC LIMIT 10;优化措施:
- 建立复合索引(user_id, status, create_time)
- 使用覆盖索引
- 引入ES做复杂查询
优化后SQL:
SELECT id, order_no FROM orders WHERE user_id=123 AND status IN (1,2,3) ORDER BY create_time DESC LIMIT 10;
优化效果:执行时间从3000ms降至15ms
5.3 网络传输优化
图片加载耗时占页面总时间的60%,采取的优化手段:
图片处理:
- WebP格式转换
- 自适应分辨率
- 懒加载
HTTP协议:
- HTTP/2多路复用
- 开启Brotli压缩
- 合理设置缓存头
CDN策略:
- 边缘节点缓存
- 智能调度
- 预加载热点资源
配置示例(Nginx):
location ~* \.(jpg|png|webp)$ { expires 365d; add_header Cache-Control "public"; brotli_static on; gzip_static on; try_files $uri =404; }在实际面试中,除了展示技术方案的实现细节,更重要的是说明决策过程和权衡考量。比如选择Redis而不是其他方案时,要清楚Redis的单线程模型、持久化机制等特性如何与电商场景的需求相匹配。技术没有绝对的好坏,只有适合与否。