首页 / 资讯中心 / 文章详情

电商微服务与缓存实战:从Redis选型到库存一致性面试指南

电商微服务与缓存实战:从Redis选型到库存一致性面试指南 ★ FEATURED ARTICLE
一个电商后端项目从单体到微服务缓存从本地Map到Redis集群这里面全是面试官最爱挖的考点。我最近帮几个准备跳槽的兄弟做模拟面试发现大家八股文背得滚瓜烂熟一落到“电商秒杀场景下缓存和库存怎么保持一致”这种实战问题上就卡壳。这篇我就拿一个典型的电商订单链路来拆把微服务拆分思路、缓存技术选型、数据一致性保障这些硬骨头一块块啃清楚顺便把我踩过的坑和面试官追问的应对方式都交代明白。1. 业务场景与架构演进面试官想听的从来不是背概念很多人在简历上写“熟悉微服务架构”但一问“为什么订单模块要单独拆出来”就开始支支吾吾。面试官考察的核心其实只有一句话你有没有真正从业务体量和团队协作角度思考过架构边界。1.1 从单体到微服务电商业务拆分的真实动因假设我们做一个电商平台早期用户量小订单、商品、用户、支付全在一个Web应用里部署在一台服务器上开发部署都很快。但等日活上了百万问题就来了大促时商品详情页流量暴涨订单服务也跟着被拖垮因为所有模块共享进程内存和数据库连接池。拆分的直接动因有三类我在项目里都真实遇到过资源隔离需求商品浏览是读多写少订单是写多读少混合部署时互相抢CPU和内存谁都不舒服。独立扩缩容需求秒杀场景下订单服务需要瞬间扩容到20个节点但用户服务根本不需要动单体架构做不到这种差异化伸缩。团队协作需求十几个人同时在一个代码库里改merge冲突能让人崩溃按业务域拆开之后订单团队和商品团队各自维护自己的代码库和发布节奏。从技术上说微服务拆分本质上是把“进程内调用”变成“网络调用”这个转变会引入一系列新问题——网络延迟、服务发现、容错、分布式事务——但换来的是系统整体的弹性和可维护性。面试时如果你能把这个取舍逻辑讲清楚比列举十个Spring Cloud组件名字管用得多。1.2 电商核心链路拆解订单、库存、支付的服务边界一个标准电商下单链路涉及至少四个核心服务用户服务、商品服务、库存服务、订单服务再加上后续的支付服务和物流服务。边界划分我画过无数次图核心原则是按业务能力而非技术层切分。用户服务管账号、地址、偏好商品服务管SPU/SKU、价格、详情库存服务管SKU级别的库存扣减和锁定订单服务管订单状态流转和超时关单。支付必须独立因为它涉及资金安全、对账、第三方渠道对接和业务订单是不同频率和不同可靠性的东西。有一个很容易踩的边界坑优惠券和订单该不该放一起。我的经验是初期可以放进订单服务但一旦出现“营销中台”的多业务复用需求就要拆出独立的营销服务。面试题里经常会问“拆分的粒度怎么把握”我的回答思路是先看独立发布频率、再看独立伸缩需求、最后看团队规模三个条件都不满足就老老实实先别拆。1.3 调用链路上的网络开销与超时治理拆完服务第一个直面的事实就是以前一个本地方法调用1毫秒搞定现在变成一次HTTP或者RPC调用动辄5到10毫秒。一个下单流程要依次调用库存、优惠券、订单三个服务串行下来延迟轻松超过50毫秒高峰期网络抖动一下就直接超时。应对手法主要有三层超时设置必须有梯度连接超时500ms、读超时1s-3s且下游服务要按P99延迟来定上限不能凭感觉写。异步化替代串行下单链路里砍价、积分、消息通知这些非关键路径全部改发MQ异步处理同步只保留库存扣减、订单创建和支付。降级开关常备优惠券服务挂了不能影响基础下单流程开关一关默认走“无优惠”逻辑。面试官问到微服务最大的挑战我一般都会把这条链路讲一遍核心想表达的是微服务不是把代码拆开就完事而是要把“分布式带来的复杂度”用工程手段治理掉。如果你能在回答里带上真实的超时参数和降级策略比背十条Spring Cloud特性更有说服力。2. 缓存技术在电商场景的落位从Redis选型到缓存策略设计电商系统对缓存的需求非常直白——商品详情页的QPS可能是数据库承受能力的几十倍。我在项目里把Redis用在四个位置商品详情缓存、库存预热、分布式锁、延迟队列。下面逐个拆。2.1 商品详情页缓存缓存粒度与Key设计细节商品详情页包含基础信息、价格、库存、销量、评价这些数据的更新频率和一致性要求都不一样。最开始图省事把整个页面渲染结果缓存成一个JSON字符串Key就是skuId。压测时发现两个问题价格一变就要全量刷新缓存评价的分页数据也被卷进大Key里读多写少但偶尔的写放大很严重。后来改成分区缓存按数据域拆Keysku:basic:{id}、sku:price:{id}、sku:stock:{id}、sku:comment:page:{id}:{page}。每个域独立设置过期时间基础信息半小时价格五分钟库存实时失效。这样单个缓存值变小更新影响面变窄命中率也上来了。Key命名的规范也很重要我一般用“业务:域:ID[:子域]”的格式这样排查问题时能快速定位也方便用Redis的scan命令做模式匹配。还有一个容易忽略的坑不要用keys命令生产环境数据量一大一次全表扫描会让Redis阻塞几秒钟所有请求全部排队事故就是这么来的。2.2 缓存更新策略Cache Aside模式与最终一致性取舍市面上讲缓存更新的文章满天飞但解决不了“先更新数据库还是先更新缓存”的纠结。我项目里采用的是标准的Cache Aside旁路缓存模式读的时候先查缓存没有就查库回填写的时候先更新数据库再删除缓存。这里有个看起来违背直觉但实际正确的点先更新数据库再删缓存还是先删缓存再更新数据库。理论上两者都有并发窗口但实践中“先更新库再删缓存”的出错概率更小因为删缓存这个动作即使失败最多就是多一次缓存穿透回源而“先删缓存再更新库”如果更新库失败缓存里就是旧数据而且后续请求会把旧数据重新加载进去影响时间更长。删除缓存失败怎么办我当时的方案是启动一个兜底任务扫描最近一分钟内更新过的商品批量刷新缓存。后来引入了binlog订阅监听MySQL的变更日志异步把相关缓存删掉这个方案更通用也彻底。2.3 本地缓存与分布式缓存的分层组合纯Redis在极高并发下还是有一个瓶颈——网络IO。即使Redis本身能抗住每秒十万级操作应用服务器到Redis之间的网络往返也会占用大量连接资源。所以我在商品详情页加了一层本地缓存Caffeine查不到本地再查Redis。本地缓存适合两种数据一种是几乎不变的基础配置另一种是短时间容忍不一致的热点数据。Caffeine的配置我有现成参数maximumSize10_000expireAfterWrite60s热点商品详情页的命中率能到95%以上。当然本地缓存有代价——多实例之间的数据一致性没法保证。一个订单完成后库存减少了用户A所在的实例本地缓存可能还是旧库存。我的取舍是把秒杀券余量这类强感知数据放到Redis把商品图片、富文本描述这类弱一致数据放本地缓存分层明确两者互不干扰。3. 高并发写场景库存扣减与缓存一致性实战面试里电商场景考得最深的就是库存。扣库存方案市面上能摆出一堆数据库乐观锁、Redis预减、分布式锁、消息队列串行化。但每个方案都有适用的具体前提能讲清楚“为什么这个场景选这个方案”才见功力。3.1 秒杀场景下超卖问题的根因分析超卖的本质是:多个并发请求同时读到库存为1然后都执行了扣减。数据库层面用update stock set count count - 1 where sku_id ? and count 0这种带条件的SQL能解决,但前提是数据库能抗住流量。秒杀场景的真正瓶颈往往不在正确性,而在流量放大。假设库存只有1000件,但瞬间来了50万请求,如果全部打到数据库,即使SQL再对,数据库连接也会被打满。这时候需要在数据库前面加一道拦截,让真正的写请求只有前几千个。我老东家当年踩过一个真实事故:库存扣减用的Redis预减,预减成功了,但订单服务调用库存服务确认扣减时,库存服务因为Full GC停顿了8秒,大量请求超时重试,结果Redis里的预减值被扣成了负数,数据库里的真实库存没怎么动,前台却显示已经抢光。这个事故告诉我们,缓存预减和数据库扣减必须有一条“最终一致”的闭环,不能扣完就撒手不管。3.2 Redis减库存Lua脚本保证原子性在秒杀场景,我用的是RedisLua脚本做库存预减。Lua脚本能保证多条Redis命令的原子性,核心逻辑是:先判断库存是否大于0,大于才执行扣减,返回成功;否则直接返回失败。脚本长这样:local stock redis.call(get, KEYS[1]) if not stock then return -1 end if tonumber(stock) tonumber(ARGV[1]) then return 0 end redis.call(decrby, KEYS[1], ARGV[1]) return 1这套方案的好处是:Redis单线程执行Lua脚本天然串行,不存在并发扣减问题;而且判断加扣减在一个脚本里完成,不用在应用层分段处理。QPS上的表现很稳定,单个Redis节点每秒能处理两万次左右的扣减操作。但要注意,Redis预减成功的请求只是拿到了“入场券”,真正的库存扣减必须异步落到数据库,否则Redis一重启就全没了。我的做法是:预减成功后发一条MQ消息给订单服务,订单服务消费消息后再执行数据库扣减,两边通过最终一致性兜底。3.3 库存流水与对账兜底:数据库扣减的最终防线Redis里的库存只是为了挡流量,数据库里的库存才是真正的权威数据。我在库存服务的数据库里建了一张库存流水表,每次扣减都记录一条流水:skuId、订单号、扣减数量、操作前余量、操作后余量,建唯一索引(订单号, skuId)。这样同一笔订单重复扣减时,唯一索引直接挡住,天然幂等。对账兜底任务每天凌晨跑一次,扫描Redis预减记录和数据库流水,发现不一致就走补偿脚本,把Redis数据修正为数据库的权威值。这两个兜底机制在线上救过我两次,一次是MQ消息重复消费,一次是应用重启导致预减记录丢失,没有流水表和对账任务,事故复盘会非常难看。4. 微服务通信与分布式事务:订单状态不一致的解法订单链路最怕的就是状态不一致:库存扣了但订单没创建成功,或者订单已支付但积分没到账。这属于分布式事务的范畴,面试官通常喜欢问“怎么保证数据一致性”,我下面展开讲实践里真正可落地的方案。4.1 本地消息表:不用引入消息中间件也能保证最终一致最稳妥的方案之一是本地消息表。核心思路是:把“业务操作”和“写消息”放到同一个数据库事务里。比如创建订单时,在订单库里同时插入一条待发送的MQ消息记录,两个操作共用一个本地事务,要么同时成功,要么同时回滚。然后起一个定时任务,扫描消息表中状态为“待发送”的记录,把消息投递到MQ,投递成功才把状态改成“已发送”。下游消费方处理完后回调一个确认接口,再把消息状态改成“已完成”。这套方案的优点是不需要引入分布式事务协调器,能解决大部分场景;缺点是消息表会膨胀,需要定期清理,而且投递效率受定时任务轮询频率限制。我一般在消息量不大、全年无大促的B端系统里用它,简单可靠。4.2 事务消息:更优雅但需要中间件支持如果项目用了RocketMQ,事务消息是更好的选择。它的执行流程分成两步:先发送“半消息”,半消息对消费者不可见;然后执行本地事务,根据事务结果向MQ提交或回滚;如果提交时网络中断,MQ会反向回查本地事务状态。我实践中的体会是,事务消息能解决“本地消息表”的大部分痛点,比如不用自己管理消息状态、不用轮询,但前提是团队对RocketMQ的运维能力要跟上,不然半消息积压了都不知道去哪排查。中小团队如果没专职中间件运维,我还是建议用本地消息表多写几个类的事,别为了优雅给自己挖坑。4.3 幂等设计与分布式锁的细节实战分布式系统里,靠消息队列保证不丢消息,但重复消费是常态。下单接口必须做幂等,我的做法是:前端生成一个全局唯一的requestId,后端收到请求先查Redis里的幂等键,存在就直接返回上次结果,不存在就尝试用SETNX占位,占位成功才继续处理。分布式锁我遇到过一个大坑:用SETNX加锁后忘记设过期时间,业务线程卡死锁就永远不释放,其他线程全部阻塞。后来统一改成分部署锁工具类:加锁用SET key value NX EX 30,解锁时用Lua脚本比对value再删除,防止把自己的锁解了别人的。value我用的是UUID加线程ID,保证唯一。还有一个面试高频题是“Redisson的看门狗机制”,它的原理是后台定时给锁续期,如果不设置过期时间,默认30秒锁会被续到业务执行完。理解了这点,你就明白为什么生产环境都要设一个合理的业务超时上限,防止死锁又要有兜底。5. 缓存三大坑:穿透、击穿、雪崩的电商应对实录缓存这块面试题绕不开的就是穿透、击穿、雪崩,几乎每个Java岗位的面试都会问。我直接结合电商场景把三种情况和应对方案一次讲透。5.1 缓存穿透:恶意请求打穿数据库缓存穿透是指查询一个根本不存在的数据,缓存和数据库都没有,请求直接打到数据库。电商场景里很典型:一个攻击者循环请求不存在的商品ID,每次都能绕过缓存直接查库。我的应对三板斧:第一,接口层做参数校验,商品ID必须是合法数字且小于最大值;第二,缓存空值,设置5分钟的过期时间,避免同一个不存在的ID反复打库;第三,用布隆过滤器,把所有存在的商品ID加载到过滤器里,请求来了先判断ID是否存在,不存在直接返回。布隆过滤器的实现我用的是Redisson的RBloomFilter,初始化时加载全量SKU,效果很稳定。5.2 缓存击穿:热点Key瞬间失效缓存击穿是指一个热点Key在失效的瞬间,大量并发请求同时回源数据库。秒杀商品的库存Key就是典型的热点Key,一到整点缓存过期,瞬间的请求全部打到数据库。我用两种方式解决:一是热点Key的过期时间不要设成定点,加一个随机偏移量,比如30分钟加0到5分钟的随机值,避免集体失效;二是用互斥锁,缓存查不到时先去尝试获得分布式锁,拿不到锁的线程短暂sleep后重试,拿到锁的线程才去查库并回填缓存。这个方案的最大代价是牺牲了一点点并发度,但在热点Key只有一两个的场景下非常管用。5.3 缓存雪崩:大规模Key同时失效雪崩是穿透和击穿的大规模版本,一般两种原因:一是大量Key在同一时间过期,二是Redis节点宕机。针对过期时间问题,加随机偏移量就能解决大部分;针对宕机问题,必须做Redis的高可用,线上至少是主从加哨兵,大厂会用Cluster。电商大促前我们有个固定操作:压测一遍缓存集群,把所有热Key的过期时间提前打散,同时启动缓存预热任务,在活动开始前半小时就把热点数据加载到缓存里。这套操作配合Redis的高可用架构,基本能挡住雪崩的冲击。6. 面试答题思路与实战建议:怎么把项目经验讲出亮点最后这部分我以面试官视角来聊——同样的项目经历,为什么有人能拿高分,有人讲完面试官毫无印象。差别往往不在技术深度,而在讲故事的方式。6.1 项目描述遵循“业务背景-技术选型-落地效果”三段式面试官最反感的是上来就堆术语——“我们用了Spring Cloud全家桶加Redis加MQ”——但没有业务背景,他根本不知道你这些技术解决的是什么问题。我建议的叙事结构是:**第一段:业务背景(用户量、QPS、核心痛点),比如“商品详情页的QPS从200涨到2000,数据库连接池扛不住,高峰期请求大量超时”。第二段:技术选型及理由,比如“引入Redis做详情页缓存,热点数据用Caffeine做本地缓存,双层缓存把数据库QPS降到200”。第三段:落地效果和关键指标,比如“缓存命中率从70%提升到95%,接口RT从300ms降到30ms,大促期间未出现数据库连接打满”。这套结构的好处是:面试官能在30秒内抓住你的思路,而且每一段他都能追问细节,你提前准备过就不会慌。6.2 对“数据一致性”追问的必备话术数据一致性是面试官最爱深挖的方向,基本追问链条是这样的:“你怎么保证缓存和数据库一致”→“如果这里失败了怎么办”→“MQ消息丢了怎么办”→“消息重复了怎么办”。这是个连环坑,如果你回答不一致,下面全崩。我的应对是以“最终一致性”为主线,分三层来答:实时一致性层面:先更新数据库,再删除缓存,加上binlog订阅兜底删除。异步链路层面:用本地消息表或事务消息,保证业务和消息的原子性。消费层面:消费方做幂等,唯一键防重,失败重试,死信队列兜底。每一层都能往深了讲,而且逻辑是递进的,显得你对整个链路有把控。这个话术我在模拟面试里验证过多次,能有效引导面试官往你有准备的深度上问。6.3 加分项:主动暴露你知道的“坑”并给出解法面试中一个很加分的动作是主动讲出方案的局限性和你踩过的坑。比如我讲Redis减库存方案时,会主动说:“这个方案有个隐患,Redis重启会丢库存数据,所以必须有对账任务从数据库同步修正,我在项目里跑了一个每天早上六点的定时任务,上线后确实发现过一次不一致。”这段话的杀伤力在于:它证明你是真的在线上跑过这个方案,而不是背了网上的文章。我在辅导候选人时经常说一句:面试官要的不是一个完美的系统,而是一个能发现问题、定位问题、解决问题的人。你承认方案有坑,但给出了检测和兜底手段,这比强行说“我们方案完美”更有说服力。6.4 准备一份“技术亮点清单”和对应的追问答案我建议每个准备面试Java岗位的人都花两天时间,把自己做过的最有含金量的项目拆成十来个技术亮点,每个亮点写一段“背景-方案-坑-解决”四段式的文字。我当时的清单大概是:RedisLua库存扣减、CaffeineRedis双层缓存、binlog订阅缓存删除、本地消息表最终一致性、幂等设计防止重复下单、分布式锁防并发重复处理。每个点我都能在三分钟内讲完,而且每个点都准备了两层追问的答案。有了这份清单,你进面试间前心里就有底,面试官的随机提问大概率绕不出这个范围。反过来,如果没准备,临场组织语言容易越讲越乱,明明是自己的项目,讲得像个旁观者,那就太亏了。电商场景微服务与缓存技术的面试,说到底考的是你有没有真刀真枪处理过分布式系统的复杂度。我见过太多候选人原理说得天花乱坠,但一问到“你的Redis过期策略为什么这么设”就露出马脚。把本文讲的几个核心场景——服务拆分边界、缓存更新策略、库存扣减方案、最终一致性链路、缓存三大坑——用自己的项目重新走一遍,把每个方案的取舍记在心里,面试时你就能从“背题的”变成“解决问题的”。这不仅是面试技巧,更是实打实的架构能力沉淀。
阅读完成 · 觉得有帮助?
咨询建站