☰
微服务电商架构高频面试考点解析:从拆分到一致性
2026/10/8 15:07:33 网站建设 项目流程

最近几年互联网大厂的Java面试,问来问去都绕不开几个固定的主题。微服务架构怎么做服务拆分,电商下单这种高并发业务怎么保证数据一致性,Redis缓存穿透怎么办,消息队列怎么保证消息不丢,分布式锁到底该用Redis还是Zookeeper。很多同学背了不少八股文,一到面试官追问"为什么是这个方案""换个场景你还这么选吗",就卡壳了。

这篇文章围绕互联网大厂Java面试里微服务、电商场景下的高频问题,以面试问答的形式做一次完整拆解。内容覆盖服务拆分、注册中心选型、Feign调用、分布式事务、Redis缓存、消息队列、分布式锁,以及微服务架构里躲不掉的JVM、并发、Spring Boot这些必考题。适合正在准备跳槽的Java开发,工作了2到6年、想冲击大厂P6/P7的同学,也可以用这套思路自己复盘知识体系。

1. 先摸清套路:大厂面试官在微服务电商场景下到底想考察什么

1.1 从一条电商链路看懂核心考点

我在面试候选人的时候,很少上来直接问概念。通常会给一条业务链路,比如"用户浏览商品,加购物车,下单,支付,扣库存,发优惠券",然后让候选人讲这个流程在他设计的系统里是怎么跑通的。就这么一个开放题,基本能把一个人的水平摸个七八成。

原因很简单。这条链路表面是业务流程,底下全是技术考点:商品服务是高并发读场景,要考缓存设计;下单是写多读少的核心链路,要考分布式事务和幂等性;支付回调是典型的异步消息场景,要考消息队列的可靠性;扣库存同时要防超卖,要考分布式锁和乐观锁;整个链路跨多个服务,要考服务调用方式、超时重试、链路追踪;数据量大了以后,还要考虑分库分表和最终一致性。

所以你在准备面试的时候,不要死记硬背Redis的八股文,而是要想清楚:在这个电商场景里,Redis具体解决的是什么问题?当你把知识点挂到业务链路上,面试官追问起来你才有底气。

1.2 面试官的三连问:是什么、为什么、出了问题怎么办

大厂面试的节奏非常固定,基本是"三连问"递进的模式。

第一层问是什么。什么是微服务,什么是CAP理论,什么是缓存穿透。这一层考的是知识面,只要是认真准备过面试的同学,都能说个大概。

第二层问为什么。为什么这个场景要用Redis缓存而不是本地缓存,为什么注册中心选Nacos而不是Zookeeper,为什么分布式事务用消息最终一致性而不是2PC。这一层考的是理解深度,需要你明白每个方案的适用边界和代价。

第三层问出了问题怎么办。缓存和数据库不一致了怎么修复,消息重复消费怎么处理,订单服务挂了怎么保证库存不超卖。这一层考的是实战经验和排查能力,是最容易拉开差距的地方。

很多候选人挂在第二层和第三层,不是因为他不会,而是因为他从来没在真实场景里踩过坑。这一篇我会尽量把每个方案的取舍逻辑讲透,并给出常见故障的排查思路。

1.3 一个正统的微服务电商系统长什么样

先给个整体画面。一套典型的微服务电商系统,按领域模型拆分,通常会有这些服务:用户服务、商品服务、订单服务、库存服务、支付服务、营销优惠服务、购物车服务、搜索推荐服务、售后服务和消息通知服务。

入口一般是一层API网关,统一做鉴权、限流、路由转发,后面挂各业务服务。服务之间通过OpenFeign走HTTP调用,或者通过Dubbo走RPC调用。注册中心用Nacos,配置中心也用Nacos。缓存用Redis集群,消息中间件用RocketMQ或Kafka,数据存储每个服务独立数据库实例,数据量大的表做分库分表。全链路有SkyWalking或Zipkin做链路追踪,定时任务用XXL-Job。

后续所有的面试问题,都是在这个系统的基础上展开的。你脑子里要能画出这张架构图,答起题来才有画面感,而不是背一段与世隔绝的理论。

2. 微服务拆分:面试第一问,怎么讲出深度

2.1 拆分原则:不是代码层面的拆分,是领域层面的划分

面试官问"你做过微服务拆分吗",很多人的回答是"按模块拆,订单一个模块,用户一个模块"。这个回答太浅了,拆模块只是代码层面的物理切割,真正的微服务拆分是领域驱动设计(DDD)里的限界上下文划分。

核心原则是高内聚低耦合,一个服务内部的业务逻辑必须是完整的、闭环的,服务之间的交互尽量少。同时遵循单一职责原则,每个服务只解决一个领域的业务问题。还有一点经常被忽略:服务拆分要和团队组织结构匹配,也就是康威定律,小团队维护小而自治的服务,交付和运维效率才最高。

在电商场景里,拆分的依据主要是业务维度,先把整个电商业务画出来,找到清晰的业务边界,比如用户域、商品域、交易域、支付域。再结合数据维度,看哪些数据是强聚合的,比如订单和订单明细必须在一个库一个服务里,否则分布式事务的代价会让项目很难受。最后还要看流量维度,像商品详情、秒杀这类高并发的读场景,可以考虑把查询逻辑独立成商品读服务,和商品管理写服务分开。

2.2 电商系统的具体拆分案例

我拿之前做过的电商项目举例。订单服务独立负责下单、订单查询、订单状态流转;库存服务独立负责库存查询和库存扣减;商品服务管理SPU/SKU数据、商品详情和上下架;支付服务对接第三方支付渠道,处理统一支付和回调;用户服务管理注册登录、用户信息和收货地址;营销服务负责优惠券和活动配置。

每一步拆分都有一个现实理由。订单和库存必须分开,因为两者的写入压力完全不同,订单是交易峰值流量,库存是高频扣减,分开以后才能独立扩缩容。商品服务和搜索服务分开,因为搜索要依赖Elasticsearch这类独立中间件,而且搜索索引构建和商品变更是完全不同的吞吐模型。支付服务必须独立,因为它涉及资金安全,需要通过消息队列异步处理回调,还要有独立的日志审计。

拆完以后你会立刻感受到代价。一次用户下单,以前是一个本地方法调用,现在变成了订单服务调用库存服务、调用营销服务、扣优惠券、发消息给支付服务,一次操作跨越了网络、涉及多个事务,复杂度成倍增长。所以面试官问拆分的时候,一定要主动讲清楚"拆分带来了什么问题,我是怎么解决分布式事务和服务间通信的",这就是引导面试节奏的技巧。

2.3 拆分粒度的坑与权衡

微服务不是拆得越细越好。服务拆得太细,会导致一次业务流程要经过十几次网络调用,延迟叠加,排查问题要跨好几个系统的日志,运维成本直接起飞。服务拆得太粗,又退化成了单体应用,无法独立部署和扩容,团队之间互相阻塞。

衡量粒度有一个很实用的标准:一个服务如果改了代码,理想情况下不需要同步改别的服务就能独立上线,那么拆分粒度基本是合理的。另一个标准是看团队规模,一个服务至少得有2到3个人长期维护,数量超过团队协作极限就必须合并。

这里还有一个高频追问:微服务和SOA有什么区别。SOA倾向于复用企业级ESB总线做集成,服务粒度偏大,通信依赖重量级的SOAP协议。微服务强调去中心化治理,服务自治,轻量级通信协议比如HTTP/REST或高性能RPC,每个服务可以有自己的数据库甚至自己的技术栈。面试时把两者的核心差异讲清楚,比背定义要好得多。

2.4 数据一致性伴随的复杂要求

拆分之后,原本一个单体应用里用一个数据库事务搞定的事情,变成了跨服务的分布式操作。比如用户下了一个订单,订单库要插入订单数据,库存库要扣减库存,营销库要核销优惠券,支付服务要发起支付。这三四个写在不同的数据库里,无法再用本地事务保证同时成功。

这时候就必须引入分布式事务方案。库存扣减和订单创建的强一致性要求高,我会优先考虑TCC或者最大努力通知。而优惠券核销和支付结果通知这类允许短暂延迟的,用消息队列做最终一致性。你在回答拆分粒度问题时,主动把"拆完以后有哪些地方变得更难了"讲出来,面试官就会觉得你是有真实架构经验的,而不是只会画图的伪架构师。

3. 服务注册与发现:Nacos、Consul、Zookeeper到底怎么选

3.1 先从CAP理论说起

注册中心是整个微服务架构的基石,没有注册中心,服务之间就没法动态发现彼此的地址。市面上主流的有Nacos、Consul、Zookeeper、Eureka。面试官问注册中心选型,本质是在问你对CAP理论的理解。

CAP指一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。分布式系统里网络分区不可避免,所以P是必选项,要在C和A之间做取舍。Zookeeper是CP设计,发生网络分区时,为了保证一致性和Leader选举正确性,会拒绝写入请求、暂时不可用。Eureka是纯AP设计,节点彼此完全对等,一个节点挂了,其他节点照常提供服务,保证可用性,但可能读到过期服务列表。

这个理论理解以后,很多问题就不用死背了。比如为什么Eureka在服务宕机时不会立刻把它从列表里摘掉,因为Eureka优先保证可用性,认为宁可让调用方重试或报错,也不能因为更新服务列表而拒绝新注册的服务。

3.2 Nacos为什么能成为主流选择

现在国内公司用Nacos最多,因为它灵活地支持AP和CP两种模式。临时实例默认走AP模式,服务注册后如果心跳超时,直接剔除节点,适合应对服务频繁上下线的微服务场景。持久实例走CP模式,通过Raft协议保证一致性,更适合需要强一致的场景,比如配置管理。

配置中心也是Nacos的杀手锏。Spring Cloud Alibaba可以非常方便地把配置放在Nacos里,支持动态刷新,修改配置以后应用不需要重启就能生效。相比单独搭一套配置中心,比如Spring Cloud Config加消息总线,Nacos一体化的方案运维成本低很多。按大多数电商项目的实际情况,注册中心加配置中心统一交给Nacos管理是最可靠的。

如果项目用Dubbo比较多,还需要考虑兼容性:Zookeeper对Dubbo的兼容历史最悠久,但Zookeeper做注册中心在处理服务上下线时有几秒延迟,高并发场景下容易误伤调用方。Nacos在这一点上要顺滑很多,这也是它在国内Java生态里越来越主流的原因。

3.3 服务发现与负载均衡怎么配合

注册中心只负责保存服务地址列表,真正发起调用时,消费方要自己实现负载均衡。如果没有特别引入Ribbon,Spring Cloud 2020版本以后默认用的是Spring Cloud LoadBalancer。通过OpenFeign调用一个服务时,Feign内置了负载均衡器,从注册中心拉取服务提供方列表,按轮询、随机或权重策略选择目标实例。

一个需要注意的细节是,负载均衡策略的选择与业务场景强相关。对无状态的查询服务,轮询就够了。对单台实例连接数有限的调用方,要改用最小并发数策略。对需要区分实例性能差异的场景,就要用Nacos注册的权重信息做加权随机。很多人在这个点上答得不好,只是知道"有几种策略"就结束了,要说清楚每个策略解决什么实际问题。

3.4 服务下线延迟:一个容易被忽略的线上事故

我踩过最典型的一个坑,是服务发布时下游服务一直调用到旧实例。根源在于服务从注册中心摘除得不及时,调用方还持有旧的实例列表。现在标准的做法是服务下线前先调用注册中心的注销接口,然后等待几秒钟,确保调用方刷新了本地缓存的服务列表,再真正把进程停掉。线上发布脚本里,这个等待通常设为10到15秒。

如果服务进程突然被kill -9,连注销接口都来不及调用,注册中心只能等心跳超时才剔除实例,这个窗口期调用方会不断请求到死掉的实例。解决方案是让调用方配合配置重试机制,以及把Feign的超时时间设短一点,避免单次请求长时间卡死。

4. 服务间通信:OpenFeign原理与RPC选型

4.1 HTTP与RPC之争,为什么要两者共存

微服务之间的通信,现在基本是HTTP和RPC二分天下。OpenFeign走的是HTTP协议,基于接口加注解声明式调用,对Java开发者非常友好,结构清晰、调试方便,还能直接用Fiddler或Postman看请求报文。它适合跨语言场景不多、追求开发效率的中小型微服务项目,尤其是Spring Cloud全家桶。

Dubbo这类RPC框架走的是自定义TCP二进制协议,序列化效率更高、传输体积更小、调用延迟更低,适合对性能敏感、服务数量庞大、内部Java技术栈统一的大型系统。但是Dubbo的泛化调用、服务治理比较复杂,对团队要求也更高。

在电商场景里,实际往往两类并存:对订单、库存这类内部强依赖的核心调用链,用Dubbo保证性能;对查询商品详情、营销信息这类非关键路径,用OpenFeign降低开发成本。面试时你表达出"根据依赖强度决定通信方式",比盲目吹捧某一种要好得多。

4.2 OpenFeign的底层原理

面试官问Feign原理,其实是想看你有没有看过源码。Feign的核心是一个动态代理机制。你定义一个接口,在接口上标注@FeignClient(name = "order-service"),Feign会为这个接口生成一个JDK动态代理对象。当你在业务代码里调用接口方法时,代理对象会拦截方法调用,读取方法参数和注解,构建出HTTP请求,然后交给底层的负载均衡器和HTTP客户端去执行。

构建请求时会结合Spring MVC的注解解析,比如@RequestMapping、@PathVariable、@RequestBody。微服务名映射到注册中心的具体服务地址。如果你配置了fallback熔断降级类,代理对象还会在调用异常时返回兜底方法。

4.3 一次电商调用链路的完整过程

拿用户下单举例。用户在网页提交订单,请求先打到API网关,网关鉴权后路由到订单服务的下单接口。订单服务要想办法扣减库存,于是通过Feign调用库存服务。这个调用的完整过程是:Feign从Nacos拉取库存服务的实例列表,按负载均衡策略选出一个实例IP,构造包含商品ID和扣减数量的HTTP请求,发送过去。如果扣库存失败或超时,Feign会根据配置的重试机制进行重试。重试成功则流程继续,重试还失败就走降级逻辑,返回业务失败,同时记录日志。

这个链路里必须关注几个参数:connection-timeout用于建立连接的超时时间,read-timeout用于等待服务响应的时间。在电商秒杀场景,read-timeout设到3秒就差不多了,太长会拖垮调用方线程池,太短容易因为GC停顿产生大量超时误报。

4.4 重试与幂等:Feign里最容易出问题的环节

Feign的重试机制是把双刃剑。一个超时请求回来时,服务端可能已经成功处理了,也可能没有处理。如果无脑重试,而目标接口不是幂等的,就会造成重复创建订单、重复扣库存这类脏数据。所以重试的前提是接口幂等。下单接口设计成幂等,需要前端生成全局唯一订单号或业务号,服务端检查这个业务号是否已经被处理过,已处理就直接返回上次结果。

还有一个教训:重试次数不要设太多,默认的Retryer重试100毫秒间隔最多重试3次就可以了。很多线上故障就是重试风暴引起的,上游1秒超时重试5次,瞬间把所有下游连接池打满,把正常流量也拖垮了。

5. 分布式事务:电商下单的"灵魂拷问"

5.1 为什么@Transactional救不了分布式场景

一个朴素的想法是:我在服务方法上加@Transactional,不就能保证所有操作一起提交或者回滚了吗?这个想法在单体应用里是对的,但在微服务下,一个方法调用链路横跨订单库、库存库、优惠券库,Spring管理的是本地数据库连接的事务,它的回滚只作用于当前服务对应的一个数据库。别的服务的数据库操作,根本不在这个事务的管辖范围内。

拆开来看,一次下单中,订单服务插入订单成功,调用库存服务扣减库存成功,然后最后一步插入订单明细时抛异常。订单服务本地事务回滚,但是库存服务的库存已经扣了,没有一起回滚。数据就不一致了。所以分布式事务需要跨服务的协调机制,而不是依赖单个数据库的ACID。

5.2 2PC和TCC,强一致方案的代价

两阶段提交协议是经典方案,有事务协调者和参与者。第一阶段所有参与者执行预提交、锁定资源,第二阶段协调者根据所有参与者的响应决定提交或回滚。在理论层面它保证了强一致,但实际工程里它的弱点很明显:第一阶段锁定资源期间,如果某个参与者一直没有响应,整个事务都会卡住,锁时间过长,吞吐量骤降。另外协调者是单点,协调者挂了,整个事务卡死。

TCC是业务层面的补偿方案,把一个分布式事务拆成Try、Confirm、Cancel三个阶段。Try阶段完成业务检查并预留资源,Confirm阶段真正执行业务,Cancel阶段做补偿释放预留资源。比如扣库存,Try阶段预扣库存但不实际扣,Confirm阶段实际扣减,Cancel阶段释放预扣量。TCC的好处是不用长时间锁数据库资源,性能比2PC好很多。但它侵入性极强,每个业务接口都要实现三个方法,开发成本相当高,而且空回滚、悬挂这些边界问题很考验功底。

5.3 可靠消息最终一致性,电商玩得最多的套路

现实中,大厂电商里真正高频使用、面试最爱问的,是可靠消息最终一致性方案。核心思想很简单:本地事务和消息发送绑定在一起,保证消息一定能发出去,消费者一定能消费到,通过最终一致的状态达到数据对齐。

实现方式通常有三种。一种是本地消息表,本地事务里写入业务数据的同时,在同一数据库里写一条消息记录。然后有一个定时任务把消息表中的消息扫描出来发送到MQ,消费者消费成功后回调更新消息状态。另一种是消息事务,RocketMQ的事务消息把发消息的prepare动作和本地事务放在同一个流程里,先发半消息,执行本地事务,本地事务成功就Commit发送正式消息,失败就回滚,如果事务状态不确定还有反查机制。第三种是订单位标记加定时对账,业务表里有一个状态字段,比如"已支付、待支付"之类的,定时任务扫描超时未处理数据,触发补偿。

用户下单减库存的经典落地方案就是:订单服务本地事务里创建订单,同时发一条"扣减库存"的半消息给RocketMQ。事务提交后消息被正式发送,库存服务消费消息执行扣减,扣成功则回执,扣失败则记录并重试。这样订单和库存就通过异步消息实现最终一致。

5.4 面试这样答分布式事务,才有亮点

不熟悉这块的人只能说出几个名词,熟悉的人要能针对场景选方案。可以按不同业务要求来区分:资金扣款要求实时强一致的,就用TCC;下单扣库存这种允许存在短暂不一致但最终要一致的,用可靠消息最终一致性;低并发、企业内部系统的强一致场景,用2PC也能接受但要考虑扩展性问题。

还有一个重要补充:分布式事务没有银弹,最好的方案是"尽量避免跨服务事务"。比如把订单和订单明细放到同一个数据库,下单扣库存改成通过消息异步化,先返回下单成功,库存扣减失败走补偿。这种"事务外置"的思路在架构层面比任何分布式事务框架都更可靠。

6. 缓存:Redis穿透、击穿、雪崩与一致性

6.1 电商的缓存架构是怎么搭的

电商系统里缓存是分层设计的。最外层是CDN,缓存静态资源和图片。应用测做本地缓存,比如Caffeine,缓存那些几乎不变并且访问量巨大的热点数据,像商品详情里的品牌信息。然后才是Redis集群,承担主要的缓存职责。最后是数据库兜底。

Redis的key设计有讲究,通常是业务前缀加冒号加业务ID,比如product:detail:1001。value序列化选JSON还是protobuf,要看吞吐要求,JSON可读性好,对象体积大;protobuf体积小、反序列化快,但有一定的编译工程成本。过期时间也要按场景设计,商品基本信息的过期时间可以长,库存数量这类实时数据的过期时间必须短,甚至直接不缓存或用分布式锁保护。

6.2 缓存三兄弟:穿透、击穿、雪崩怎么分

缓存穿透,查了一个数据库里也不存在的数据,导致请求每次都打到数据库。攻击者可以用大量不存在的ID发起请求,直接压垮数据库。解决手段主要是布隆过滤器,把有效ID放进布隆过滤器里,查不到就返回空,另一个方法是为空结果也做短暂缓存,几秒到几十秒,扛住瞬时攻击即可。

缓存击穿,某个热点key到了过期时间,大量并发请求同时打到数据库,数据库压力峰值。解决方法是互斥锁,只有一个线程去重建缓存,其他线程等待或者短暂返回空。还有一个方案是逻辑过期,value里存过期时间而不是让Redis自动淘汰,读的时候发现逻辑过期就异步更新缓存,用户拿旧数据继续看。

缓存雪崩,大量key同时过期,或者Redis集群直接宕机,导致所有请求压到数据库。解决办法是设置过期时间时加随机漂移量,避免同一时间成片失效,同时做多级缓存兜底,再结合Redis集群高可用和限流熔断,把数据库保护起来。

6.3 缓存与数据库的双写一致性,最需要实际操作经验

缓存一致性是面试里的重头戏,网上方案很多,但落地起来坑更多。业界最稳定的是Cache Aside Pattern,读的时候先读缓存,缓存miss再查数据库、回填缓存。写的时候先更新数据库,再删除缓存。为什么不是更新缓存而是删除缓存?因为更新数据后去写缓存,这个写动作本身很容易失败,失败就会有一段时间数据不一致。删除缓存失败可以重试,而且下次读请求会触发回填,自然补偿。

删除缓存这个动作,看起来简单,实际里经常丢。可以先删缓存后更新数据库,更新数据库失败,旧数据被删了,新数据没写,用户查出旧数据回填缓存,一致性问题更严重。稳妥的做法是延迟双删:更新数据库前先删除缓存,更新数据库成功后延迟几百毫秒再删一次缓存,把并发期间其它线程回填的旧数据清理掉。

数据一致性要求更高的写法是订阅MySQL binlog,监听数据变更事件,异步删除对应key。这个方案对业务代码侵入少,可靠性高,是电商系统比较成熟的做法。

6.4 热点key和缓存倾斜,被忽略的实战题

经常被问到热点key怎么处理。以前遇到一个案例,某个头部主播带货,一个SKU的详情页QPS直接拉到十万级,单个Redis实例根本扛不住。常规解决思路是做热点key探测,将热点数据多副本缓存,比如同一个key复制多个备份key分散到不同实例,读的时候随机取一个副本。本地缓存也要把这部分热点提前预热,让本机吃下很大一部分流量。

还有大value问题。有人把一个大对象整串塞进Redis,几MB的数据读一次网络传输就要几十毫秒,线程被拖死。解决方法是压缩字段、分片存储或者改用哈希结构。这些细节参数没有标准答案,核心是让面试官看到你处理过真实流量,踩过硬钉子。

7. 消息队列:削峰与最终一致性的关键拼图

7.1 为什么要用MQ,电商峰值靠什么顶住

没有消息队列之前,秒杀期间的下单请求直接打到订单服务,每秒几万次瞬间流量,应用代码没等扩容就被打崩。引入MQ以后,前端请求先进入队列,业务系统以自己的最大消费速度慢慢往下游消费。队列像一个蓄水池,让流量从尖峰变成一条平稳的河。这就是削峰填谷。

同时MQ还承担了解耦职责。支付服务处理完支付成功后,把支付成功消息发给订单服务和营销服务,订单服务不用同步调用营销服务。这样一来,营销服务就算挂了,订单还是正常流转,过了几分钟营销服务恢复了,续上消费就行。

7.2 Kafka、RocketMQ、RabbitMQ怎么选

在乱谈选型之前,先明确事实:Java生态和电商场景里,RocketMQ的使用率非常高,Kafka在日志链路和大数据场景是绝对王者,RabbitMQ更多在老项目和一些简单业务场景里使用。

选型对比看这几个维度。吞吐量方面,Kafka和RocketMQ都能达到十万级每秒,RabbitMQ只有万级。可靠性和事务支持,RocketMQ自带事务消息,这在电商分布式事务场景里是刚需。消息延迟,RabbitMQ最低可到微秒级,RocketMQ毫秒级,Kafka会略高一点。运维成本,Kafka依赖Zookeeper,RocketMQ有专门的Dashboard,RabbitMQ最轻量但集群能力弱一些。

电商系统里最需要的是可靠性和事务消息,所以RocketMQ是比较理想的选择。如果公司是大数据链路为主,同时想复用一套中间件,Kafka也够用,但事务消息需要自己调控件和重试机制,代价不低。

7.3 消息不丢、不重复、不乱序,三大难题逐个击破

消息不丢失要分三段来看:生产端不丢、Broker不丢、消费端不丢。生产端用同步发送并确认发送结果,失败就重发;Broker层面开启刷盘和主从复制,RocketMQ的主从同步复制可以做到一条消息commit前就把数据复制到备节点;消费端先执行业务逻辑成功后再提交ack,不要让消费成功成为空头支票。

消息不重复,本质是要求消费端实现幂等性。去重表或者业务唯一键是常规手段,比如订单服务用自己的订单号作为唯一标识,Redis里setnx一下,如果已经处理过就直接返回成功。消费端没有幂等设计,再好的MQ也没办法阻止重复消息到达。

消息不乱序,方案核心是把同一业务ID的消息发送到同一个MessageQueue,消费者也只用一个线程消费这个队列。比如同一个订单的创建、支付、发货消息,按订单号哈希到固定队列,就能保证按顺序被消费。如果你把消息发到不同队列,消费者端并发消费,顺序必然乱。

7.4 秒杀系统如何用MQ削峰,手写一遍印象更深

秒杀下单是最经典的案例。用户点抢购按钮,网关层先做限流,只放一部分请求到后端,请求进入Redis预扣库存。Redis库存扣成功,就发一条"创建订单"消息给RocketMQ,不直接操作数据库。后台订单服务开启固定数量的消费者线程,比如20个线程,每个线程串行消费消息,最终落到数据库创建订单。

这样就保证了数据库每秒处理的请求是可控的,不会出现数据库被瞬时流量冲垮的问题。秒杀结束后,如果有人订单创建失败,消费者重试几次后转人工处理。面试时把这条链路讲透,基本能覆盖MQ的大部分考点。

8. 分布式锁:怎么做才能真正锁住

8.1 单机锁和分布式环境的本质差异

说到扣库存和防超卖,肯定要聊分布式锁。单机应用里,直接用synchronized关键词或者ReentrantLock就能保证一个JVM进程内只有一个线程进入临界区。但是微服务部署了多个实例,不同的实例运行在不同的JVM里,本地锁没法跨进程生效。三个实例同时处理同一个SKU的扣减,三个JVM的锁不互斥,库存照样超卖。

分布式锁的本质,是在多个进程之间共享一个互斥状态,谁拿到这个状态,谁才有资格执行。必须满足互斥性、可重入性、高可用、防死锁这几个基本素质,才能称得上是个靠谱的分布式锁。

8.2 基于Redis实现分布式锁,SETNX与看门狗

最简单的Redis锁是SETNX,没key就设置成功,设置成功代表加锁成功。但要注意不能少了过期时间,否则客户端持有锁后进程崩溃,锁永远释放不了。Redis里正确加锁是加两个参数同时设置:SET lockKey lockValue EX 30 NX,这个操作是原子的,不会有设置成功但没设置过期时间的坑。

锁的value要带上请求唯一ID,释放锁的时候用Lua脚本比较value,是自己才删除。防止一个线程的锁过期被删了以后,另一个线程拿到锁,然后前一个线程把自己的锁误删了。

上面这个方案有个比较棘手的场景,如果业务逻辑执行时间超过了锁的过期时间,锁提前释放了,别的线程又进来了,怎么解?Redisson框架的看门狗机制是业内普遍采用的方案。获取锁后,后台有一个定时任务,每隔锁过期时间的1/3就自动续期,直到业务完成为止。我试用过,底层用了时间轮调度,非常优雅。如果你的项目不允许引额外框架,就把锁的过期时间设置成业务预计耗时的十倍,但这是静态方案,不够稳。

8.3 Redlock、Zookeeper锁,以及数据库锁

Redis分布式锁有一个争议:主节点挂掉时,锁数据还没来得及复制到从节点,从节点顶上来以后,新客户端就能重新加锁,导致原来持有锁的客户端和新的客户端同时持锁。Redlock算法试图通过加锁到大多数Redis节点来解决这个问题,但因为时钟漂移和GC暂停的存在,在复杂分布式环境里依然不绝对安全。不过实际业务中,绝大多数场景用单点Redis加Redisson就够了,Redlock的高成本收益没那么高。

Zookeeper锁的定位完全不同,它靠临时顺序节点实现。客户端创建临时顺序节点,判断自己是不是最小序号,是就拿到锁,不是就监听前一个节点。临时节点在客户端断连后会被Zookeeper自动删除,天然避免了死锁。而且由于顺序一致性,锁的公平性有保障。缺点是性能不如Redis锁,因为每次加锁释放锁都是Zookeeper的写操作,节点多的时候会有性能瓶颈。

数据库锁,比如select ... for update,能实现锁但性能差,资源占用高,在电商高并发里基本不用,只在一些后台管理功能里偶尔出现。

8.4 锁粒度与业务场景,这才是面试加分项

分布式锁本身不难,难的是锁的粒度。并发扣减库存场景,理想的锁粒度不是锁整个"库存服务",而是锁单个SKU。我在Redis里的做法是lock:stock:{skuId},只锁住一个商品,不同商品的扣减互不干涉,并发能力大幅提升。

还有一个坑是锁超时导致业务没完成。存这个场景要真正扛住流量,单靠分布式锁不够,还得配合数据库乐观锁,update stock set stock = stock - 1 where sku_id = ? and stock >= 1,通过受影响行数判断是否能扣减,这样鲁棒性更强。面试答到这里,就明显比那些只背"SETNX加Lua"的同学高一个层次。

9. Java高频八股文:微服务之外躲不开的那些题

9.1 面试必考:String、HashMap、ConcurrentHashMap

微服务问得再深,八股文还是绕不开,尤其Java基础题是很多大厂的一面门槛。我面试别人的时候经常先丢一道哈希相关的题热场。HashMap在JDK 8里,底层是数组加链表加红黑树,数组长度超过64且链表长度超过8时,链表转红黑树。为什么是8?这是基于泊松分布的计算结果,链表长度为8的概率小于千万分之一,用红黑树来平衡极端情况下查询的性能损失。

ConcurrentHashMap要重点说清楚它是怎么做到线程安全的。JDK 8以后放弃分段锁,使用CAS加synchronized,对数组的每个桶加锁,锁粒度更细,并发性能更好。谈到这里,面试官大概率会追问"CAS是什么意思,CAS有什么缺点",你就可以引出ABA问题以及版本号机制。平时写代码很少用这些底层知识,但面试必须能讲清楚。

String类有两道经典题。字符串常量池和堆中的String对象区别,String不可变的设计动机。不可变的好处是安全、线程安全、可以安全地缓存hashCode。为什么String用final修饰,本质是让多个引用共享同一个字符串对象时不会被意外修改。你说清楚这些底层来源,面试官就能判断你是背的题还是真懂。

9.2 JVM、线程池、GC,一组永远问不完的组合拳

JVM考点高频的是内存区域划分和垃圾收集器。内存区域至少要能画出堆、虚拟机栈、本地方法栈、方法区、程序计数器各自的作用,然后说明哪些是线程私有哪些是线程共享。GC课程上CMS和G1是必谈的,现在ZGC也常被问到,包括它们的适用场景,比如CMS的并发标记清除和内存碎片问题,G1通过Region划分做到可预测停顿,ZGC目标是把停顿时间控制在10毫秒内。

线程池的问题必须能给出具体参数。ThreadPoolExecutor构造函数的七个参数,核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。对CPU密集型任务,核心线程数可以设定为CPU核心数加一;IO密集型任务,核心线程数可以略多,常见经验值是CPU核心数乘2。拒绝策略有四种,AbortPolicy直接抛异常、CallerRunsPolicy由调用线程执行、DiscardPolicy和DiscardOldestPolicy都是静默丢弃。电商场景下,线程池满了直接丢消息是不可接受的,所以往往要配合自定义策略加MQ缓冲。

9.3 Spring Boot和MyBatis的微服务高频题

Spring Boot的自动配置原理是必考题。@SpringBootApplication组合了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。@EnableAutoConfiguration会通过隐藏的META-INF/spring.factories或者spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件加载一批自动配置类,并且配合@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,根据当前classpath里的依赖情况决定是不是初始化相关Bean。你在项目里引入一个starter,本质就是导入了一批自动配置类。

@Transactional失效这个面试题坑了很多人。自调用失效是很常见的一种场景,同类内部通过this调用带事务注解的方法,事务不会生效。原因是Spring事务基于AOP动态代理,this调用走的是当前对象,不会经过代理对象的拦截。还有数据库引擎不支持事务、方法非public、注解被类内部方法直接调用异常被吞了等等,能说个四五个场景才够。

MyBatis的一二级缓存也是经典掉分题。一级缓存是SqlSession级别的,默认开启;二级缓存是Mapper级别的,默认关闭。有个项目里有人开了二级缓存,结果一个商品价格更新后,另一个线程查到的还是旧数据,因为缓存没有及时失效。很多公司明确要求禁用二级缓存,就是吃够了这种苦头。面试时如实讲自己的经验,比背书有说服力得多。

9.4 Java机考/算法:别把基本功丢了

虽然现在大厂一二面都倾向于系统设计题,算法题的概率也不低,尤其是社招P7以下,很可能会考排序和动态规划。冒泡排序这种最基础的算法,还是需要手写不卡壳。比较常用的是快速排序和归并排序,记得对应的时间复杂度与稳定性,快排平均O(nlogn)、不稳定,归并稳定、O(nlogn)但是需要额外空间。

如果还有余力,建议把蓝桥杯或力扣上的一些常见题过一遍,重点是一维和二维动态规划、滑动窗口、TopK问题。TopK可以用二叉堆,也可以手写快速选择,复杂度都是O(n)或者O(nlogk)。这些内容在Spring Cloud调优和GC调优面前看着不起眼,但是现场答不出来会非常减分。

10. 面试策略与实战复盘:这些坑不要再踩

10.1 简历怎么写,才更容易被面到微服务相关题

简历上写项目,每一条都要能引导出技术问题。简单列一句"负责电商订单模块"是不会被深挖的。要写清楚项目规模、技术挑战、数据量级和你承担的角色。例如:支撑日均千万级订单的电商平台,主导订单服务微服务化,实现了订单与库存的最终一致性,RT从400ms降到120ms。这就等于告诉了面试官,你可以聊微服务改造、分布式事务、性能优化、Redis、MQ,全是高频考点。

同时,不要主动写你不熟的技术名词。简历上一旦出现"我精通Kafka",面试官大概率会问一些Kafka底层原理的冷门问题,比如日志段和索引文件的具体结构,答不上来就是减分。适当暴露边界,把你想被问到的技术点前置到项目描述里,是最有效的引导手段。

10.2 遇到不会的问题,这样答反而加分

没有谁什么都会。我面试时最看重的不是你全能,而是你遇到不会的问题时的思考方式。直接说"我不会"会显得没有探索深度。比较好的回答框架是:我先说我理解的部分,然后给出最常见的可行思路,再说明我没做过、不太确定的细节。比如面试官问"你们消息队列的消费积压怎么处理",就算你没实际处理过,也可以说我知道要先确认堆积原因,如果只是瞬时流量,加消费者实例数进行水平扩容;如果下游服务处理瓶颈明显,要考虑先把数据快照落库,后续再补偿处理。

这种"对问题的结构有认知、敢于提出解决路径"的答法,让面试官能评估你的思维模型。比慌张地说"没遇到过"要好得多。

10.3 一场真实面试的复盘:从拆分讲到一致性到缓存

我有一次面候选人,他的项目是电商中台,我沿着一套连环问走下来,他答得很扎实。先是问服务拆分怎么做的,他讲了按领域划分订单、商品、支付、库存,并说明库存服务独立的原因。接着问拆分后订单和库存的一致性怎么办,他立刻说用RocketMQ事务消息做最终一致性,把半消息、本地事务Commit后再发具体细节也讲清了。我又追问"库存扣减失败了怎么办",他引入了消息重试、人工补偿、定时对账兜底。

然后我转向性能,问热点商品详情页怎么抗住高并发。他说先做本地缓存加Redis多级缓存,布隆过滤器挡穿透,热点key多副本存储,再配合限流熔断把数据库保护起来。最后我问他Redis和数据库的一致性,他说Cache Aside,更新库删缓存,再结合binlog订阅双保险。这一整轮下来,两个人都聊得很舒服。他也许不是每一项都很精,但是每一层都有真实思考和踩坑体验,这就是大厂面试最想看到的画像。

10.4 系统设计题的核心,不是背模板而是讲权衡

电商和微服务的面试题,表面是在考技术,实际是在考权衡能力。所有方案都有代价,没有一个东西是完美的。你需要展现的是:在什么约束条件下,你会选择什么方案,会放弃什么好处,会出现什么问题,怎么去兜底和补偿。

拿缓存一致性来说,你说Cache Aside的时候,要能说出更新缓存和删除缓存的差异;你说Redis锁的时候,要能说出看门狗续期的机制;你说MQ削峰的时候,要能说出消息丢失和重复消费的防御方案。每一个选择背后,都有原因的推导。这套思维模型比任何具体答案都更重要。

按照这个思路,把整个微服务电商技术栈的每一个环节都梳理清楚,你在任何大厂面试的微服务环节里,都不会是背八股文的那个人,而是可以跟面试官平等交流技术的那个人。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询