互联网大厂Java面试实录:谢飞机大战面试官——电商系统架构篇
互联网大厂Java面试实录:谢飞机大战面试官——电商系统架构篇
面试官:严肃脸,技术深度拉满 谢飞机:时而灵光乍现,时而水货附体 场景:某互联网大厂,Java高级工程师岗位,电商业务线
前情提要
谢飞机,江湖人称"飞哥",五年Java开发经验,简历上写着"精通微服务、高并发、分布式",实际上...emmm,懂的都懂。今天他来到了某一线互联网大厂面试Java高级工程师岗位,面试官是传说中的"灭绝师太"——技术VP老张,人称"张怼怼"。
面试间里,老张推了推眼镜,目光如刀。
老张:谢飞机是吧?坐。我们直接开始技术面。
谢飞机(擦了擦汗):好的张哥,您问!
第一轮:Java核心与JVM · 电商商品系统
业务场景:设计一个电商商品详情页系统,需要考虑缓存、并发、性能优化
问题1:你说说Java8和Java11在内存管理上有啥区别?别背书,结合业务讲。
谢飞机:(思考片刻)Java8是永久代(PermGen),Java8之后是元空间(Metaspace)...嗯...8的永久代在堆内存里,容量有限,容易OOM。11的元空间在本地内存,默认不受限制...还有就是,Java11有一些GC优化,比如ZGC的引入。
老张:嗯,基本概念对了。那结合电商场景,你负责的商品详情页,大量SKU信息、图片URL、促销活动数据,怎么设计内存管理?
谢飞机:这个...可以设个最大元空间大小,-XX:MaxMetaspaceSize,防止无限增长。然后商品信息用本地缓存,用Caffeine或者Guava Cache,设置过期时间和最大条目数,避免堆内存被打满。
老张(微微点头):思路可以。那你说说,商品详情页QPS突然飙升,比如双11秒杀,JVM层面你怎么调优?
谢飞机:呃...调大堆内存...年轻代调大点...用G1GC?设置最大停顿时间...
老张:具体参数呢?结合场景说。
谢飞机(有点慌了):-Xms4g -Xmx4g -Xmn2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200...嗯大概这样。
老张:参数背得还行。但你没说为什么用G1,什么时候用CMS,更没说你打算怎么监控GC。下一个问题。
问题2:HashMap在多线程环境下会出现什么问题?如何解决?ConcurrentHashMap的底层原理是什么?在电商购物车场景中怎么用?
谢飞机:这个我会!HashMap在多线程put时会导致死循环,Java8之前是头插法,扩容时可能形成环形链表。Java8改成了尾插法,避免了死循环,但依然有数据丢失、size不准确的问题。所以并发场景用ConcurrentHashMap!
ConcurrentHashMap底层...Java7是Segment分段锁,默认16个Segment,每个Segment是一个小HashMap。Java8改成了CAS + synchronized,对每个数组节点加锁,粒度更细了。
老张(眼前一亮):不错。那说说电商购物车——用户有购物车,里面有商品列表、数量、价格,还要支持多人同时操作,你怎么设计数据结构?
谢飞机:购物车用ConcurrentHashMap存储,key是商品SKU ID,value是购物车条目对象(包含商品ID、数量、价格快照)。用户维度的话,可以用一个Map<String, ConcurrentHashMap<String, CartItem>>,外层key是用户ID。
public class ShoppingCart {
// userId -> (skuId -> cartItem)
private ConcurrentHashMap<String, ConcurrentHashMap<String, CartItem>> userCarts;
public void addItem(String userId, String skuId, int quantity) {
userCarts.computeIfAbsent(userId, k -> new ConcurrentHashMap<>())
.merge(skuId, new CartItem(skuId, quantity),
(oldVal, newVal) -> {
oldVal.setQuantity(oldVal.getQuantity() + newVal.getQuantity());
return oldVal;
});
}
}
老张:嗯,computeIfAbsent和merge用的不错,知道CAS操作。那有个问题,价格快照怎么处理?用户加购时是100块,过会儿降价到80了,要不要给用户退差价?
谢飞机(挠头):这个...价格是在下单时根据SKU实时查询的,购物车里的价格只是个展示参考。真正算钱在订单系统,用Redis缓存最新价格,下单时Redis+DB双重校验,防止价格被篡改。
老张:还行,算你有基本电商认知。
问题3:电商系统中如何设计一个高可用的分布式锁?Redis分布式锁有什么坑?怎么避免?
谢飞机:嗯...Redis分布式锁用SETNX + EXPIRE...但可能会死锁,如果SETNX成功了EXPIRE没执行的话。所以要用SET key value NX EX 30这种原子命令。
但还有个问题,如果锁过期了业务还没执行完,A线程锁过期了,B线程拿到锁,A执行完了释放了B的锁。解决方案是value用唯一ID(比如UUID+线程ID),释放锁时用Lua脚本检查是不是自己的锁再删除。
// 加锁
String lockKey = "lock:order:" + orderId;
String requestId = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
// 释放锁 - Lua脚本保证原子性
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
Arrays.asList(lockKey), requestId);
老张:那如果业务执行超过30秒呢?锁自动释放了怎么办?
谢飞机:这个...用Redisson!它有个Watchdog机制,每10秒自动续期一次,只要业务没执行完锁就不会过期。还可以用Redlock算法实现高可用的分布式锁。
老张:Redlock有什么问题?Martin Kleppmann那篇文章看过吗?
谢飞机(额头冒汗):呃...看过一点...Redlock依赖于时钟同步,如果节点时钟发生跳跃会导致问题...还有GC pause可能导致锁的竞争判断出错...所以有些场景更适合用Zookeeper的临时顺序节点做锁,CP模型更可靠...
老张:行,Redlock的争议这个问题暂且放过。但至少你知道什么时候用Redis锁,什么时候用ZK锁。
问题4(追问):电商下单扣库存,并发扣减怎么保证不超卖?说具体方案。
谢飞机:这个几种方案:
- MySQL乐观锁:update sku set stock = stock - #{num} where sku_id = ? and stock >= #{num}
- Redis原子扣减:lua脚本或decrBy命令,扣减前检查库存
- 消息队列串行化:将扣库存请求放入MQ,单线程消费
最常用的是Redis缓存库存+异步DB持久化,Redis做预扣,MQ异步同步到DB。
-- Redis Lua脚本原子扣库存
local stock = redis.call('get', KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
return -1
end
redis.call('decrby', KEYS[1], ARGV[1])
return redis.call('get', KEYS[1])
老张:嗯。但你考虑过Redis在极端情况下丢数据吗?如果Redis宕机,内存里的库存数据丢了,怎么办?
谢飞机:可以开启AOF持久化,每秒钟fsync一次。再配合数据库兜底,如果Redis库存数据和DB不一致,有定时任务做校对恢复。还有...预热阶段从DB加载库存到Redis,设置过期时间,到期后重新加载。
老张:不错,这个回答有深度了。
问题5:Java SPI机制是什么?在Jakarta EE和Spring中怎么用的?
谢飞机:SPI就是Service Provider Interface,服务发现机制。在META-INF/services/目录下放接口全限定名的文件,文件内容是实现类的全限定名。ServiceLoader加载。
JDBC驱动加载就是SPI实现的,MySQL的com.mysql.cj.jdbc.Driver通过SPI注册到DriverManager。
Jakarta EE(原来是Java EE)里用的更多,比如Bean Validation、JAX-RS等规范都有SPI扩展点。Spring Boot的spring.factories和Spring 3.4+的spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports也是类似的SPI思想。
老张:手写过SPI扩展吗?
谢飞机(心虚):呃...看过源码,自己没写过...但原理知道。
老张:也没让你手写,知道原理就行。第一轮结束,问你第二轮。
第二轮:Spring全家桶与微服务 · 订单与支付系统
业务场景:分布式订单系统、支付回调、分布式事务
问题1:Spring Boot自动配置原理是什么?手写一个starter你该怎么设计?
谢飞机:Spring Boot启动时,@SpringBootApplication包含@EnableAutoConfiguration,它通过@Import(AutoConfigurationImportSelector.class)导入配置。
AutoConfigurationImportSelector会加载META-INF/spring.factories或spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里的所有自动配置类。每个配置类上有@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,满足条件才生效。
手写starter的话:
- 创建一个autoconfigure模块,定义配置类
- 创建starter模块,引入autoconfigure
- 在autoconfigure的spring.factories里注册
- 配置属性用@ConfigurationProperties绑定
// 自定义Starter示例
@Configuration
@ConditionalOnClass(RedisClient.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public RedisClient redisClient(RedisProperties properties) {
return new RedisClient(properties.getHost(), properties.getPort());
}
}
老张:那Spring Boot 3.x相比2.x有什么核心变化?你项目迁移时遇到什么问题?
谢飞机(擦了擦汗):Spring Boot 3基于Spring Framework 6,最低要求Java 17。最大的变化是...Jakarta EE替换Java EE,javax.*包变成了jakarta.*包。还有AOT编译支持,为GraalVM原生镜像铺路。
迁移问题的话...主要是第三方组件的兼容性,有些老版本的MyBatis、Shiro不支持Jakarta命名空间,得升级版本。还有就是移除了Spring.factories的自动配置注册方式...
老张:只说对一部分。Spring Boot 3.0移除了spring.factories的自动配置支持,3.4又加回来了但推荐新方式。其次,Spring 6的声明式事务底层从AspectJ换成了...算了,这个后面再说。
问题2:订单系统的状态机怎么设计?从创建到完成经历哪些状态?怎么防止状态跳转错误?
谢飞机:订单状态一般有:待支付→已支付→已发货→已收货→已完成,还有取消、退款等分支。
用Spring Statemachine或者自己实现状态机模式。最简单是用枚举+Map定义状态流转规则:
public enum OrderStatus {
PENDING_PAYMENT, // 待支付
PAID, // 已支付
SHIPPED, // 已发货
DELIVERED, // 已收货
COMPLETED, // 已完成
CANCELLED, // 已取消
REFUNDING, // 退款中
REFUNDED; // 已退款
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = new HashMap<>();
static {
TRANSITIONS.put(PENDING_PAYMENT, Set.of(PAID, CANCELLED));
TRANSITIONS.put(PAID, Set.of(SHIPPED, REFUNDING));
TRANSITIONS.put(SHIPPED, Set.of(DELIVERED));
TRANSITIONS.put(DELIVERED, Set.of(COMPLETED, REFUNDING));
// ...
}
public boolean canTransitionTo(OrderStatus target) {
return TRANSITIONS.getOrDefault(this, Collections.emptySet()).contains(target);
}
}
然后结合数据库乐观锁,更新时校验当前状态:
@Update("UPDATE orders SET status = #{newStatus} WHERE order_id = #{orderId} AND status = #{oldStatus}")
int updateStatus(String orderId, OrderStatus oldStatus, OrderStatus newStatus);
返回0表示状态不对,事务回滚。
老张:很好,这个回答非常扎实。那支付回调怎么处理?支付宝/微信回调到了,怎么保证幂等?
谢飞机:支付回调必须保证幂等!关键点:
- 回调处理使用分布式锁,同一个订单ID加锁
- 数据库唯一约束:支付流水号(transaction_id)做唯一索引
- 状态机前置校验:只有待支付状态才能处理支付成功回调
- 回调结果异步确认:收到回调后先返回SUCCESS给支付平台,业务异步处理
// 幂等处理核心逻辑
@Transactional
public void handlePaymentCallback(PaymentCallback callback) {
String lockKey = "lock:payment:" + callback.getOrderId();
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
// 1. 检查流水号是否已处理
if (paymentRecordDao.existsByTransactionId(callback.getTransactionId())) {
return; // 已处理过,直接返回
}
// 2. 校验订单状态
Order order = orderDao.selectById(callback.getOrderId());
if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
throw new BusinessException("订单状态异常");
}
// 3. 更新订单状态 + 记录支付流水(同一个事务)
orderDao.updateStatus(callback.getOrderId(),
OrderStatus.PENDING_PAYMENT, OrderStatus.PAID);
paymentRecordDao.insert(callback.toPaymentRecord());
}
} finally {
lock.unlock();
}
}
老张:不错,知道分布式锁+事务+唯一约束三板斧。
问题3:分布式事务你们怎么做的?Seata AT模式原理是什么?有什么局限性?
谢飞机:我们用的是Seata AT模式。原理是:
- 一阶段:业务SQL执行前,Seata拦截SQL,解析SQL语义,生成"前镜像"(数据修改前快照)和"后镜像"(数据修改后快照),然后在业务表所在的库创建undo_log表记录镜像
- 二阶段提交:全部RM成功,删除undo_log
- 二阶段回滚:有RM失败,根据undo_log的前镜像生成反向SQL恢复数据
局限嘛...
- 不支持所有SQL,比如一些复杂JOIN、批量操作
- 事务隔离级别是读未提交(脏读),因为一阶段就提交了本地事务
- 性能开销大,每条SQL都要解析和生成镜像
- 不适用于长事务场景
老张:如果不用Seata,你有其他分布式事务方案吗?比如TCC、SAGA?
谢飞机:TCC的话,Try-Confirm-Cancel模式,每个服务要实现这三个接口。Try阶段预留资源,Confirm执行,Cancel回滚。比如下单场景:
- Try:冻结库存、冻结余额
- Confirm:扣减库存、扣减余额
- Cancel:释放冻结库存、释放冻结余额
SAGA适用于长事务,每个步骤有补偿操作,比如旅游订单:订机票→订酒店→租车,每个步骤如果失败就执行补偿。
但说实话...我们业务对一致性要求没到那个级别,大部分场景用本地消息表+MQ最终一致性就搞定了。
老张:你很实在。确实,90%的场景用消息最终一致性就够了,别为了分布式事务而分布式事务。
问题4:OpenFeign和Dubbo的区别是什么?在电商系统中服务间调用怎么选型?
谢飞机:OpenFeign是声明式HTTP客户端,基于RESTful API,和Spring Cloud深度集成。Dubbo是RPC框架,基于TCP长连接,性能更高。
电商场景的话:
- 内部高并发调用(如订单→库存):Dubbo,性能好,可做服务治理
- 对外暴露API(如BFF层对前端):OpenFeign,RESTful更标准
- 异构系统集成(如对接第三方物流):OpenFeign
// OpenFeign声明式调用
@FeignClient(name = "inventory-service", path = "/api/inventory")
public interface InventoryClient {
@PostMapping("/deduct")
Result<Boolean> deductStock(@RequestBody DeductRequest request);
@GetMapping("/query/{skuId}")
Result<StockVO> queryStock(@PathVariable("skuId") String skuId);
}
老张:Dubbo的服务发现和负载均衡底层怎么实现的?
谢飞机:Dubbo服务发现支持多种注册中心,Zookeeper、Nacos、Consul。负载均衡有随机、轮询、最少活跃调用等策略。还可以做权重配置,给性能好的机器分配更多流量。
如果用了gRPC的话,基于HTTP/2,支持双向流,比Dubbo的序列化更灵活...不过我们没用过。
老张:行了,第二轮结束。来第三轮。
第三轮:高并发架构与数据库 · 促销秒杀系统
业务场景:双11秒杀、热点商品缓存、数据库优化
问题1:设计一个秒杀系统,应该从哪些层面做流量控制?从网关到应用的完整链路讲一下。
谢飞机(深吸一口气):秒杀系统核心是"削峰填谷",从入口到最终落单做全链路保护:
- CDN层:静态资源(图片、HTML)走CDN,不要打到应用服务器
- Nginx/Gateway层:
- 限流:Nginx lua限流、Spring Cloud Gateway的RequestRateLimiter
- 拦截非法请求:校验Token、User-Agent
- 灰度分流:部分流量导到新版本
- 业务层:
- 前端:限时按钮置灰、答题验证、随机延迟
- 后端:Redis预减库存、请求队列化
- 消息队列:MQ异步削峰,下单请求先进MQ再慢慢消费
- 数据库层:
- 数据库乐观锁扣库存
- 读写分离,读从库、写主库
// Gateway层限流 - Token Bucket
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("seckill_route", r -> r.path("/api/seckill/**")
.filters(f -> f.requestRateLimiter(config ->
config.setRateLimiter(redisRateLimiter())
.setKeyResolver(userKeyResolver())))
.uri("lb://seckill-service"))
.build();
}
// RedisRateLimiter默认实现,基于令牌桶
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(100, 200); // 每秒100个令牌,突发200个
}
老张:很全面。那MQ削峰后,消费端怎么保证不被打垮?消息积压了怎么办?
谢飞机:消费端要做好几点:
- 批量消费:一次拉取一批消息,批量处理
- 动态线程池:根据积压量动态调整消费线程数
- 降级:如果积压超过阈值,拒绝新请求,返回"拥挤请稍候"
- 消息优先级:秒杀消息优先处理
积压了的话...提高分区数,增加消费者实例;或者临时写个脚本批量处理积压消息。
老张:嗯,那你觉得单机Redis做秒杀库存够用吗?热Key问题怎么解决?
谢飞机:单机Redis肯定不行,双11流量会把Redis打爆。热Key问题的话:
- 本地缓存+Redis两级缓存:热点商品数据在JVM本地也缓存一份,减少Redis压力
- Key打散:把同一个商品的库存分到多个Key上,如stock:1001:0~stock:1001:9,请求随机访问
- 读写分离:Redis Cluster,主节点写,从节点读
// 本地缓存 + Redis 两级缓存
public class StockCacheManager {
private Cache<String, Integer> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
public Integer getStock(String skuId) {
// 1. 先查本地缓存
Integer stock = localCache.getIfPresent(skuId);
if (stock != null) return stock;
// 2. 本地没有查Redis
stock = redisTemplate.opsForValue()
.get("stock:" + skuId, Integer.class);
if (stock != null) {
localCache.put(skuId, stock);
}
return stock;
}
}
老张:可以。但你说本地缓存,不同节点的本地缓存数据不一致怎么办?
谢飞机:用Redis的Pub/Sub或者消息广播,某个节点扣减库存后通知其他节点失效本地缓存。不过秒杀场景几秒就结束了,短暂的不一致可以接受...5秒过期时间,过期重新加载。
老张(点头):秒杀场景确实可以牺牲强一致性换取性能。
问题2:MySQL的InnoDB索引结构是什么样的?B+树相比B树优势在哪?在订单查询场景中如何设计索引?
谢飞机:InnoDB用B+树做索引。B+树和B树的区别:
- B+树只有叶子节点存数据,B树所有节点都存数据
- B+树叶子节点有链表指针,范围查询快
- B+树非叶子节点只存索引,可以存更多索引条目,树更矮
优势:
- 磁盘IO少:树更矮,查询路径更短
- 范围查询快:叶子节点双向链表,直接遍历
- 排序好:叶子节点天然有序
订单查询索引设计:
-- 复合索引,覆盖最常见的查询场景
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);
-- 商家查订单
ALTER TABLE orders ADD INDEX idx_merchant_status (merchant_id, status);
-- 订单号查询(唯一)
ALTER TABLE orders ADD UNIQUE INDEX idx_order_no (order_no);
-- 时间范围查询,如查询某天的订单
ALTER TABLE orders ADD INDEX idx_create_time (create_time);
老张:那我有个场景——SELECT * FROM orders WHERE user_id = ? ORDER BY create_time DESC LIMIT 10,你这个索引能走到吗?文件排序还是索引排序?
谢飞机:能走到!因为索引是(user_id, status, create_time),user_id等值匹配后,create_time天然有序,是索引排序(Using Index),不需要文件排序(Using filesort)。
但如果where条件是user_id = ? AND status IN (?),那create_time的排序就失效了,因为status用了IN查询导致索引跳变。
老张:非常好!细节把握得很到位。
问题3:慢SQL你怎么排查和优化?说具体工具和方法。
谢飞机(来劲了):慢SQL排查三板斧:
- 开启慢查询日志:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒的记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
- 用EXPLAIN分析执行计划:
EXPLAIN SELECT * FROM orders o LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.create_time > '2024-01-01' AND o.status = 0;
关注:type(ALL→全表扫描)、rows(扫描行数)、Extra(Using filesort等)
- Profile分析耗时:
SET profiling = 1;
-- 执行SQL
SHOW PROFILES;
SHOW PROFILE CPU, BLOCK IO FOR QUERY 1;
优化方法:
- 加合适的索引(复合索引、覆盖索引)
- 避免SELECT *,只查需要的字段
- 分页优化:延迟关联、子查询优化
- 大表分库分表:ShardingSphere、MyCat
老张:分库分表后,跨分片的分页查询和聚合查询怎么处理?
谢飞机(额头冒汗):这个...有两种方案:
- 全局视野法:每个分片都查,应用层聚合排序。比如每页20条,如果有5个分片,每片查20条,合并后取前20条。但翻页越深性能越差。
- 业务折中法:限定查前N页,或者用"上一页最后一条的ID"替代OFFSET分页,每次只查大于某个ID的数据。
实际项目中...大部分场景用Elasticsearch做搜索引擎,MySQL只做事务性存储。ES天然支持分布式搜索聚合。
老张:ES方案确实更常见。那你用过ShardingSphere的分布式事务吗?
谢飞机:呃...只是在项目里配置过,没深入研究源码。ShardingSphere的分布式事务支持本地事务、XA(Atomikos)、Seata、BASE柔性事务...
老张(抬手打断):不用说了,看你这表情就知道你源码没看过。行吧,第三轮结束。
问题4:你用过Resilience4j吗?相比Hystrix有什么优势?结合电商场景讲熔断降级。
谢飞机:用过!Resilience4j是Netflix Hystrix停更后的替代品。优势:
- 轻量级:不依赖其他框架,Hystrix依赖Archaius
- 模块化:熔断器、限流器、重试、隔离、超时都是独立模块
- 支持响应式:对WebFlux友好
- Java8函数式:用Supplier、Consumer等
电商场景:
// 商品推荐服务熔断降级
@Bean
public CircuitBreaker recommendCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50%失败率触发熔断
.waitDurationInOpenState(Duration.ofSeconds(30)) // 30秒后尝试半开
.slidingWindowSize(100) // 滑动窗口大小
.build();
return CircuitBreaker.of("recommendService", config);
}
// 使用
public List<Product> getRecommendProducts(String userId) {
Supplier<List<Product>> supplier = CircuitBreaker.decorateSupplier(
circuitBreaker,
() -> recommendClient.getRecommend(userId)
);
// 降级方案:返回热门商品
return Try.ofSupplier(supplier)
.recover(throwable -> hotProductService.getHotProducts(10))
.get();
}
老张:熔断器有几种状态?半开状态是怎么工作的?
谢飞机:三种状态:CLOSED(关闭)、OPEN(打开)、HALF_OPEN(半开)。
- CLOSED:正常状态,请求正常通过
- OPEN:熔断打开,请求直接降级,不调用真实服务
- HALF_OPEN:过了一段时间(waitDurationInOpenState),允许少量请求通过测试,如果成功就关闭熔断器,失败就继续保持打开
半开状态默认允许一次请求通过测试,成功则关闭,失败则重新打开。这个数量可以通过setPermittedNumberOfCallsInHalfOpenState配置。
问题5(终极问题):如果让你从零搭建一个高可用、高并发的电商系统后端架构,你会怎么设计?画出核心架构图,讲清楚每一层的职责和选型理由。
谢飞机(吸了一口气,感觉这是送命题也是送分题):
老张:怎么,不会了?
谢飞机:会!我说说我的方案:
核心架构设计
整体分层
┌─────────────────────────────────────────────────────┐
│ 🏢 API Gateway │
│ Spring Cloud Gateway + Nginx + Sentinel │
│ 限流 · 鉴权 · 路由 · 日志 · 灰度 │
├─────────────────────────────────────────────────────┤
│ 🎯 BFF (Backend For Frontend) │
│ 聚合服务:Web端、App端各一套BFF │
├────────────────────┬────────────────────────────────┤
│ 🏪 业务中台 │ 📊 数据平台 │
│ │ │
│ 用户服务 │ ELK (日志) │
│ 商品服务 │ Prometheus+Grafana (监控) │
│ 订单服务 │ SkyWalking (链路追踪) │
│ 支付服务 │ Flink (实时计算) │
│ 库存服务 │ Spark (离线分析) │
│ 促销服务 │ │
│ 物流服务 │ │
├────────────────────┴────────────────────────────────┤
│ 🔗 中间件层 │
│ Kafka(消息) · Redis(缓存) · Elasticsearch(搜索) │
│ Seata(分布式事务) · XXL-Job(定时任务) │
├─────────────────────────────────────────────────────┤
│ 💾 基础设施层 │
│ MySQL(主从+分片) · MongoDB · TiDB · OSS对象存储 │
└─────────────────────────────────────────────────────┘
核心技术选型理由
| 层次 | 组件 | 选型理由 | |------|------|----------| | 网关 | Spring Cloud Gateway | 响应式、性能好、集成Sentinel | | 注册中心 | Nacos | 支持AP/CP切换、配置中心一体化 | | RPC | Dubbo | 高性能、服务治理完善 | | 配置中心 | Nacos/Apollo | 实时推送、灰度发布 | | 消息队列 | Kafka | 高吞吐、持久化、分区消费 | | 缓存 | Redis Cluster | 高可用、自动分片 | | 数据库 | MySQL + ShardingSphere | 分库分表、读写分离 | | 搜索引擎 | Elasticsearch | 商品搜索、订单查询 | | 链路追踪 | SkyWalking | 无侵入、APM监控 | | 监控 | Prometheus + Grafana | 开源生态好、告警完善 | | 容器化 | Kubernetes | 弹性伸缩、服务编排 | | CI/CD | Jenkins + GitLab CI | 自动化构建部署 |
关键设计策略
- 高可用:服务多副本、异地多活、熔断降级
- 高性能:CDN加速、多级缓存、读写分离
- 高扩展:微服务拆分、水平扩展、无状态设计
- 一致性:最终一致性为主、强一致性为辅
- 安全:HTTPS、JWT鉴权、数据加密、防刷防爬
老张(沉默片刻,眼神中带着一丝意外):好家伙,这一套还真让你给说明白了。虽然有些细节你前面回答得磕磕绊绊,但整体架构设计能力确实不错。
谢飞机(松了口气):谢谢张哥!
老张:好吧,今天的面试就到这。你的技术广度不错,深度在某些方面还可以,但JVM调优、分布式事务源码这些还需要加强。我们的面试结果会在三个工作日内通知你。
谢飞机(忐忑):那张哥,我大概...有戏吗?
老张(面无表情):回家等通知吧。
谢飞机(内心OS:完蛋,又是"回家等通知"……)
🔥 面试题深度解析(小白必看)
第一轮:Java核心与JVM
Q1: Java8 vs Java11 内存管理
技术要点:
- Java8的永久代(PermGen)在堆内存中,默认大小有限(-XX:MaxPermSize),容易OOM
- Java8+的元空间(Metaspace)在本地内存(Native Memory),默认无上限,避免永久代OOM
- Java11引入了ZGC(可伸缩低延迟垃圾收集器),停顿时间不超过10ms,不受堆大小影响
- Java11引入了Epsilon GC(无操作GC),用于性能测试和短暂任务
电商场景应用:
- 商品详情页使用Caffeine作为本地缓存,设置maximumSize和expireAfterWrite,防止堆内存打满
- 元空间设置上限 -XX:MaxMetaspaceSize=256m,防止类加载过多导致内存泄漏
- 双11大促时使用G1GC,设置-XX:MaxGCPauseMillis=100,控制GC停顿时间
- 使用JMX、Micrometer + Grafana监控GC频率和停顿时间
Q2: HashMap与ConcurrentHashMap
技术要点:
- HashMap Java7头插法→Java8尾插法:解决死循环问题
- ConcurrentHashMap Java7分段锁→Java8 CAS+synchronized:锁粒度更细
- computeIfAbsent、merge等原子方法的使用
- ConcurrentHashMap的size()方法:先无锁求和,有修改则加锁重试
电商场景应用:
- 购物车使用ConcurrentHashMap<String, CartItem>,key为SKU ID
- 用户维度用ConcurrentHashMap<String, ConcurrentHashMap<String, CartItem>>
- 加购操作使用merge方法保证原子性
- 价格在购物车中为快照,下单时实时查询Redis最新价格
Q3: 分布式锁
技术要点:
- Redis SETNX + EXPIRE → SET key value NX EX seconds(原子操作)
- 唯一Value + Lua脚本释放锁:get和del原子操作
- Watchdog自动续期机制(Redisson实现)
- Redlock算法:向大多数Redis节点申请锁,过半成功则加锁成功
- Redlock争议:依赖时钟同步、GC pause导致判断错误
电商场景应用:
- 下单锁:lock:order:{orderId},防止重复下单
- 库存锁:lock:stock:{skuId},保证扣库存原子性
- 支付锁:lock:payment:{orderId},保证支付回调幂等
Q4: 库存扣减方案
技术要点:
- 乐观锁:UPDATE stock SET version=version+1 WHERE sku_id=? AND version=?
- 原子扣减:UPDATE sku SET stock = stock - ? WHERE sku_id = ? AND stock >= ?
- Redis Lua脚本:检查+扣减原子执行
- 异步同步:Redis预扣→MQ同步→DB落盘,定时校对
电商场景应用:
- 秒杀场景Redis预扣库存,承受高并发
- 普通下单直接操作DB乐观锁
- Redis宕机兜底:AOF持久化 + DB校对定时任务
第二轮:Spring全家桶与微服务
Q1: Spring Boot自动配置原理
技术要点:
- @EnableAutoConfiguration → @Import(AutoConfigurationImportSelector) → 加载META-INF/spring.factories
- @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty等条件注解
- Spring Boot 3.0移除spring.factories自动配置,3.4重新支持但推荐AutoConfiguration.imports
- Spring Boot 3.x:Java 17+、Jakarta EE、AOT编译、GraalVM支持
Q2: 订单状态机
技术要点:
- 状态模式:定义状态流转规则,每个状态有明确的可达状态集合
- 数据库乐观锁校验状态:UPDATE ... WHERE status = ?
- Spring Statemachine框架:状态、事件、行为、守卫完整实现
- Saga模式:长事务场景的状态补偿
电商场景应用:
- 订单状态流转:待支付→已支付→已发货→已收货→已完成
- 逆向流程:已支付→退款中→已退款
- 状态校验+乐观锁保证数据一致性
Q3: 支付回调幂等
技术要点:
- 分布式锁:同一订单ID加锁,防止并发
- 唯一约束:支付流水号transaction_id做唯一索引
- 状态校验:只有待支付状态才能处理支付成功
- 幂等表:记录已处理的回调流水号
Q4: Seata AT模式与分布式事务
技术要点:
- Seata AT:一阶段准备(前镜像+后镜像+undo_log),二阶段提交/回滚
- TCC:Try预留资源、Confirm确认、Cancel取消
- SAGA:步骤+补偿操作,适合长事务
- 本地消息表:业务操作+消息入库在同一事务,异步消费
- 最终一致性:MQ可靠消息+定时补偿
电商场景应用:
- 下单场景:订单服务+库存服务+积分服务,使用Seata AT
- 支付场景:使用TCC,Try冻结金额、Confirm扣款、Cancel释放
- 非核心链路(如发短信):使用MQ异步最终一致性
第三轮:高并发架构与数据库
Q1: 秒杀系统设计
技术要点:
- 全链路保护:CDN→Nginx/Gateway→业务层→数据库
- 限流算法:令牌桶(Token Bucket)、漏桶(Leaky Bucket)、滑动窗口
- Redis预扣库存 + Lua脚本
- MQ削峰:生产者快速返回,消费者异步处理
- 热Key处理:本地缓存+Redis两级、Key打散、读写分离
Q2: MySQL B+树索引
技术要点:
- B+树特点:非叶子节点只存索引,叶子节点存数据+双向链表
- 优势:磁盘IO少、范围查询快、天然排序
- 复合索引最左前缀原则
- 覆盖索引:查询字段都在索引中,避免回表
- 索引下推(ICP):在存储引擎层过滤,减少回表次数
电商场景应用:
- 订单查询:复合索引(user_id, status, create_time)
- 商家查询:复合索引(merchant_id, status)
- 时间范围查询:单列索引(create_time)
- 避免SELECT *,使用覆盖索引
Q3: 慢SQL排查
技术要点:
- 慢查询日志:long_query_time慢查询阈值
- EXPLAIN:type(ALL/INDEX/RANGE/REF/CONST)、rows、Extra
- Profile:CPU、Block IO耗时分析
- 优化策略:加索引、覆盖索引、小表驱动大表、分页优化
Q4: Resilience4j熔断降级
技术要点:
- 三种状态:CLOSED→OPEN→HALF_OPEN
- 滑动窗口:统计失败率/慢调用率
- 隔离模式:线程池隔离、信号量隔离
- 重试+超时+限流+熔断组合使用
电商场景应用:
- 商品推荐服务熔断:失败率>50%熔断,30秒后尝试半开
- 降级方案:返回热销商品缓存数据
- 第三方物流查询:设置超时500ms,超时降级返回缓存数据
💡 写在最后
谢飞机虽然有些地方回答得不够深入,但整体架构思维和技术广度还是可圈可点的。作为面试官,老张关注的点其实很明确:
- 知其然更知其所以然:不仅要会用,还要理解原理
- 业务场景结合:技术脱离业务就是空中楼阁
- 异常边界处理:99%的bug都出在边界条件上
- 架构设计能力:从点→线→面的全局视角
如果你是谢飞机,这些题你能答上来几道?欢迎在评论区分享你的面试经历!
如果这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多Java同学看到!
更多推荐




所有评论(0)