简介谷粒商城-2020最新文件代码面向具备Java与Spring Cloud基础的后端开发者定位为微服务分布式电商项目高级篇的配套代码与学习材料帮助读者理解可扩展、高可用在线购物平台的落地方式。压缩包约26.32MB页面文件总数显示为0、类型明细暂无数据内容侧重项目代码及微服务架构说明便于结合描述中的架构图与模块设计进行对照。已有1584人学习下载。其价值在于围绕用户、订单、商品、支付等服务拆分串联服务注册发现、负载均衡、API网关、分布式事务、消息队列、分库分表、分布式缓存、监控日志与CI/CD等核心知识点适合用于课程复盘、架构对照和分布式方案查漏补缺。1. 一套 2020 版分布式商城代码拆开之后还能不能用拿到一份 2020 年封版的分布式商城全栈代码多数人的第一反应不是兴奋而是怀疑几年过去注册中心、网关、搜索引擎的 API 都换过好几轮这套东西还能不能编译过我的结论是能跑但要把里面的默认配置当成废纸重新看一遍。这份代码覆盖商品、库存、订单、会员、优惠券、检索、秒杀、购物车、认证十个微服务前后端分离中间件用到 Nacos、Gateway、Sentinel、Redis、RabbitMQ、Elasticsearch、Seata少见地把分布式事务、分布式锁、缓存一致性、异步解耦全都压在一条下单链路上讲透。适合两类人想从单体 CRUD 跳到微服务拆分的新手以及面试前需要一条完整链路撑住深挖的从业者。不适合想开箱即用上线的人它的价值在结构不在运维成熟度。2. 九个微服务怎么在本地跑起来中间件、注册中心与网关的启动顺序这份代码最容易劝退人的地方不是业务逻辑而是启动顺序。十个服务加七个中间件谁先起、谁依赖谁README 里往往一句话带过结果就是控制台刷一屏Connection refused然后开始怀疑代码是坏的。我的做法是先画出依赖方向中间件 → 注册中心与配置中心 → 网关 → 业务服务逐层往上点火任何一层报错都先停下来解决不要一次性全部启动再大海捞针。2.1 拆分边界与依赖方向为什么是这十个服务先看模块表这张表决定了后面所有调试的方向。微服务拆得对不对看一件事服务之间的调用是不是单向的。这份代码里商品、库存、会员是底座订单是编排者网关是唯一入口检索和秒杀是旁路。服务默认端口职责关键依赖mall-gateway88统一入口、路由、跨域、鉴权前置Nacosmall-product10000分类、品牌、SPU/SKU、属性MySQL、Redis、ESmall-ware11000库存、采购单、库存解锁MySQL、Redismall-order9000订单、订单项、关单、支付回调MySQL、RabbitMQ、Seatamall-member8000会员、收货地址、积分、社交登录MySQL、Redismall-coupon7000优惠券、满减、领取记录MySQL、Redismall-cart6000购物车Redis 存储Redismall-search12000商品检索、聚合筛选ESmall-seckill13000秒杀场次、秒杀下单Redis、RabbitMQmall-auth20000认证服务、令牌签发Redis需要说明的是订单服务对商品的依赖方式下单时订单服务不直接读商品表而是调用商品服务拿 SPU 快照再落到oms_order_item里。这是分布式环境下的常规做法跨库 JOIN 在拆库那天就死了能留的只有冗余快照。2.2 中间件用容器拉起来一次把七个依赖铺平不建议在宿主机装 MySQL、Redis、ES 这一套版本冲突的排查成本远高于容器启动。下面这段是压缩过的启动脚本注意 ES 的内存必须压下去默认 1G 堆在开发机上很容易把编译进程挤死。# 数据目录统一挂在 /data/mall 下方便删库重来 docker network create mall-net # MySQL 5.7这份代码的建表语句用到了 utf8mb4 老排序规则8.0 会报错 docker run -d --name mall-mysql --network mall-net -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot \ -v /data/mall/mysql:/var/lib/mysql \ mysql:5.7 --character-set-serverutf8mb4 --collation-serverutf8mb4_general_ci # Redis关掉 RDB只留 AOF本地断电也不丢购物车 docker run -d --name mall-redis --network mall-net -p 6379:6379 \ redis:6.2 redis-server --appendonly yes --save # Nacos 2.0.39848 是 gRPC 端口只映射 8848 会出现注册上了但调不通 docker run -d --name mall-nacos --network mall-net \ -p 8848:8848 -p 9848:9848 \ -e MODEstandalone -e JVM_XMS256m -e JVM_XMX256m \ nacos/nacos-server:2.0.3 # RabbitMQ 带管理台延迟队列如果装了插件也可以在这里加 -e docker run -d --name mall-rabbit --network mall-net \ -p 5672:5672 -p 15672:15672 rabbitmq:3.9-management # Elasticsearch单节点 关安全仅为本地检索调试 docker run -d --name mall-es --network mall-net -p 9200:9200 \ -e discovery.typesingle-node -e ES_JAVA_OPTS-Xms512m -Xmx512m \ -e xpack.security.enabledfalse elasticsearch:7.9.3参数里最值得盯的是两个Nacos 的9848端口和 ES 的ES_JAVA_OPTS。前者是 2.x 版本走 gRPC 通信必需的漏了之后服务能注册但 Feign 拿不到实例列表报错信息完全指不到这里后者不压机器内存一到 16G 以下就开始随机 OOM。MySQL 故意锁在 5.7是因为建表脚本里的排序规则和部分datetime默认值在 8.0 下行为不同先跑通再谈升级。建库和导数据用一个循环脚本按文件名排序执行避免外键依赖顺序出错# sql 目录里通常是 0-建库.sql、1-商品.sql、2-订单.sql 这种命名 cd /path/to/mall-sql for f in $(ls *.sql | sort); do echo importing $f mysql -h127.0.0.1 -uroot -proot --default-character-setutf8mb4 $f || exit 1 done2.3 配置中心Data ID 命名和 bootstrap 的坑2020 年前后的 Spring Cloud Alibaba 项目普遍把配置丢到 NacosData ID 规则是服务名.后缀Group 用DEFAULT_GROUP。代码里每个模块的application.yml只保留本地端口其余全部外置。如果手上有导出的配置压缩包可以直接用接口一条条推上去比在控制台点一百次快得多。# 把 config/ 下每个 yaml 推到 NacosData ID 取文件名 for f in config/*.yaml; do data_id$(basename $f) curl -s -X POST http://127.0.0.1:8848/nacos/v1/cs/configs \ -d dataId${data_id} \ -d groupDEFAULT_GROUP \ -d typeyaml \ -d tenantdev \ --data-urlencode content${f} echo pushed ${data_id} done对应的bootstrap.yml长这样注意它必须叫bootstrap而且 Spring Boot 2.4 之后默认不再加载 bootstrap 上下文需要在pom.xml里补一个依赖否则配置中心永远拉不到东西服务照样能起只是所有外置参数静默失效——这种静默失败是最难查的一类问题。spring: application: name: mall-product # 这个名字必须和 Feign 里的服务名一模一样 cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: dev config: server-addr: 127.0.0.1:8848 namespace: dev file-extension: yaml shared-configs: # 公共数据源、Redis 配置放这里各服务共享 ->!-- Spring Boot 2.4 必须显式引入否则 bootstrap.yml 不被解析 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-bootstrap/artifactId /dependency提示spring.application.name、Feign 接口上的name、Nacos 里的 Data ID 前缀这三处必须完全一致包括大小写和中划线。不一致的表现往往是服务列表里有两个长得差不多的实例调用随机失败。2.4 网关路由与跨域把入口收成一条线网关是唯一入口前端只认http://localhost:88/api/**其余端口一律不对外。路由规则用RewritePath剥掉/api前缀这样后端 Controller 不用为了网关改路径。spring: cloud: gateway: routes: - id: product_route uri: lb://mall-product # lb 表示走注册中心负载均衡 predicates: - Path/api/product/** filters: - RewritePath/api/product/(?segment.*), /${segment} - id: order_route uri: lb://mall-order predicates: - Path/api/order/** filters: - RewritePath/api/order/(?segment.*), /${segment}跨域不要在网关和业务服务里各配一遍两边都加Access-Control-Allow-Origin会导致浏览器报头字段重复而直接拒绝。统一在网关加一个全局过滤器处理并且记住预检请求OPTIONS必须在鉴权之前放行否则前端永远是 401后端日志干净得像没事发生。// 网关全局跨域处理ORDER 设为最高优先级抢在鉴权过滤器之前 Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); // 不能用 addAllowedOrigin(*) 配 allowCredentials config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsWebFilter(source); }3. SPU/SKU 与三级分类商品域建模和缓存一致性的取舍商品域是这套代码里设计密度最高的部分也是最容易被当成建表练习跳过去的部分。真到下单、检索、秒杀三个场景同时压过来SPU/SKU 的划分方式、规格参数的存储形态、分类树的缓存策略会直接决定后面每一个接口好不好写。这一章把建模理由和缓存实现拆开讲能照抄的部分我都标了参数。3.1 为什么 SPU 和 SKU 一定要分开spu_info存的是iPhone 15这一层管标题、品牌、分类、上下架状态sku_info存的是iPhone 15 256G 蓝色这一层管价格、库存、图片、销量。分开的理由很实在价格和库存是随 SKU 变的标题和详情是随 SPU 不变的混在一张表里改一次标题要锁住几十行库存记录。表关键字段写入频率读放大spu_infospu_name、catalog_id、brand_id、publish_status低高详情页sku_infospu_id、price、sku_title、sku_default_img中高列表页sku_imagessku_id、img_url、img_sort低高attr / attr_valueattr_id、attr_name、value_name低中spu_attr_valuespu_id、attr_id、attr_value低中一个细节publish_status用 0/1 而不是布尔是为了后面加审核中已下架待清理这类状态时不用改表结构。这类小决定在二手代码里随处可见抄的时候别嫌啰嗦。3.2 三级分类树一次查全表内存拼树分类数据量小、变更少、读极多是最标准的缓存对象。这份代码的写法是全表查出后按parent_cid在内存里分组拼树用一次 SQL 换掉递归查询的 N 次。我一般会把它再往前推一步整棵树序列化成一个 JSON 塞进 Redis前端要的本来就是树结构。public MapString, ListCategoryEntity getCatalogJson() { String key catalog:json; // 1. 先打缓存命中直接反序列化返回 String cached redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(cached)) { return JSON.parseObject(cached, new TypeReferenceMapString, ListCategoryEntity() {}); } // 2. 未命中走分布式锁只放一个线程回源防止击穿 RLock lock redissonClient.getLock(catalog:lock); try { if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 3. 双重检查抢到锁时别人可能已经填好了 cached redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(cached)) { return JSON.parseObject(cached, new TypeReferenceMapString, ListCategoryEntity() {}); } MapString, ListCategoryEntity tree buildTreeFromDb(); // 4. 加随机过期避免同一时刻集体失效 redisTemplate.opsForValue().set(key, JSON.toJSONString(tree), 1 new Random().nextInt(6), TimeUnit.HOURS); return tree; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 5. 必须判断持有者否则可能误删别人的锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } return buildTreeFromDb(); // 兜底拿不到锁就直接查库宁可慢不可错 }参数上要盯三处tryLock(3, 10, ...)的 3 是等待时间比接口超时短一半左右合适10 是租期Redisson 会自动 renew但如果方法里做了远程调用租期要给足过期时间加随机数是防止缓存雪崩的常规做法1 到 7 小时的抖动对分类数据完全够用。3.3 双写一致性先删缓存还是先更库更新分类时到底先删缓存还是先更库这套代码用的是先更库、再删缓存这也是绝大多数业务的默认答案。它的风险窗口在于更新完库、删缓存之前有读请求把旧值回填了之后缓存里就一直是脏的。低频写场景这个窗口可以忽略商品这类写少读多的数据加上过期时间兜底基本够用。Transactional public void updateCategory(CategoryEntity entity) { categoryMapper.updateById(entity); // 1. 第一次删尽量把窗口压小 redisTemplate.delete(catalog:json); // 2. 延迟再删覆盖删完到事务提交之间被回填的脏数据 // 延迟时间要比一次读请求的完整耗时略长200~500ms 是常见取值 taskExecutor.execute(() - { try { Thread.sleep(300); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } redisTemplate.delete(catalog:json); }); }延迟双删不是银弹sleep时间靠拍脑袋就是它的原罪。真要严格走 binlog 订阅做异步失效更靠谱但对这份代码的定位来说属于过度设计。我的建议是分类、品牌这类数据用延迟双删库存这种强一致场景压根不要缓存绝对值只缓存标志位或者直接查库。注意延迟任务一定要丢进线程池不要在主线程sleep。主线程睡 300ms 意味着接口 P99 直接抬 300ms压测时这个数字会非常难看。3.4 规格参数JSON 冗余列和子表的取舍商品的规格参数颜色、内存、尺寸在不同类目下差异极大严格建模要做attr_group、attr、attr_value三张表再加一张关联表查询时要 JOIN 四次。这套代码在spu_info上额外挂了一列decript拼写错误也一并保留存 JSON详情页直接反序列化就能渲染规格表。两种方式各有边界需要按规格筛选比如只看 256G就必须走检索服务把规格打平进 ES 的nested字段纯展示用 JSON 列最省事。我见过有人把筛选逻辑直接写在 MySQL 的 JSON 函数上几万商品还行上十万就开始全表扫。判断标准很简单查询条件里出现的字段就不该藏在 JSON 里。4. 下单扣库存这条链路Seata AT、Redisson 锁与延迟队列的配合下单是这套代码的考试题一门课挂了整本书都白读。它同时踩中三个分布式难题跨服务的库存扣减要保证一致性、同一 SKU 的并发扣减不能超卖、订单超时未支付要回滚库存。分别对应 Seata AT、Redisson 分布式锁、RabbitMQ 延迟队列三个东西必须在同一条链路上排好序排错了就会出现锁住了还是超卖或者事务回滚了库存没回来。4.1 为什么用 Seata AT 而不是 TCCAT 模式对业务代码几乎零侵入靠数据源代理自动生成undo_log回滚时按镜像反向补偿。TCC 需要手写 Try/Confirm/Cancel 三个方法一致性更强但开发成本翻几倍。这份代码的定位是教学与二次开发选 AT 是合理的。代价要清楚AT 依赖全局锁热点 SKU 上会形成串行等待undo_log表必须每个参与事务的库都建少一个就报Table undo_log doesnt existGlobalTransactional必须加在事务发起方的入口方法上加在 Controller 层会导致 Feign 调用绕开代理事务静默失效。我一般加在 Service 的编排方法上离远程调用最近。// 下单编排加在 Service 层保证 Feign 调用在事务代理范围内 GlobalTransactional(rollbackFor Exception.class, timeoutMills 30000) public SubmitOrderResponse submitOrder(Long memberId, OrderSubmitVo vo) { // 1. 验价远程查商品当前价格防止前端传价 ListOrderItemVo items productFeign.getSkusByIds(vo.getSkuIds()); BigDecimal currentTotal calcTotal(items); // 2. 锁库存本地事务 AT 分支事务失败由 Seata 回滚 boolean locked wareFeign.lockStock(vo.getSkuLocks()); if (!locked) { throw new BizException(库存不足); } // 3. 扣优惠券、扣积分、创建订单全部是分支事务 couponFeign.useCoupon(vo.getCouponId(), memberId); OrderEntity order saveOrder(memberId, vo, currentTotal); // 4. 发送延迟消息30 分钟后检查支付状态未付则关单并解锁库存 rabbitTemplate.convertAndSend(order-event-exchange, order.create.order, order.getOrderSn()); return buildResponse(order); }timeoutMills设 30 秒是个保守值压测时如果一次下单要跨四五个服务很容易踩到默认 60 秒的全局超时反过来设太短正常请求会被误回滚。经验取值是链路 P99 的两到三倍。4.2 扣库存的 SQL 怎么写才不超卖分布式锁解决的是同一时刻多个请求的并发问题SQL 层面还要再兜一层。这段写法用stock - num 0做条件更新靠数据库行锁保证原子性返回影响行数为 0 就代表扣减失败。!-- 条件更新影响行数 0 即库存不足不需要先 select 再判断 -- update iddeductStock UPDATE wms_ware_sku SET stock_locked stock_locked #{num} WHERE sku_id #{skuId} AND ware_id #{wareId} AND stock - stock_locked - #{num} 0 /update很多人写成先select stock判断再update中间隔了一个网络往返高并发下必然超卖。条件更新把判断和写入压进一条语句是这类场景唯一可靠的做法。4.3 Redisson 锁的粒度和续期锁的粒度必须是 SKU 而不是 SPU也不要用全局锁。锁 key 带上仓库维度更稳因为同一 SKU 可能分布在多个仓库。public boolean lockStock(ListSkuLockVo locks) { // 按 skuId 排序避免两个订单以不同顺序加锁导致死锁 locks.sort(Comparator.comparing(SkuLockVo::getSkuId)); ListRLock acquired new ArrayList(); try { for (SkuLockVo vo : locks) { RLock lock redissonClient.getLock( lock:stock: vo.getWareId() : vo.getSkuId()); // 等待 3s租期不显式指定交给看门狗每 10s 续一次 boolean ok lock.tryLock(3, TimeUnit.SECONDS); if (!ok) { return false; // 拿不到就整体失败由上层决定重试 } acquired.add(lock); } return doDeduct(locks); // 真正的条件更新在这里 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { // 逆序释放且必须判断持有者 for (int i acquired.size() - 1; i 0; i--) { RLock lock acquired.get(i); if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }三个关键点按 SKU 排序加锁防死锁不显式传租期让看门狗续finally里逆序释放并判断持有者。少任何一个压测跑到几百并发就开始出现锁释放异常或者请求卡死。提示如果用tryLock(waitTime, leaseTime, unit)显式传了租期看门狗就失效了业务执行时间超过租期会导致锁提前释放另一个线程进来直接超卖。这个坑几乎每个用 Redisson 的人都踩过一次。4.4 延迟队列关单别用定时任务扫表订单 30 分钟未支付要关单并解锁库存用定时任务每分钟扫一次表是最常见的偷懒写法订单量一上来就是全表扫描加锁竞争。这套代码用的是 RabbitMQ 的 TTL 死信队列下单时发一条 30 分钟过期的消息到普通队列消息过期后进死信交换机消费者收到就开始查支付状态。Configuration public class OrderMQConfig { // 业务队列消息在这里躺 30 分钟不设消费者 Bean public Queue orderDelayQueue() { return QueueBuilder.durable(order.delay.queue) .withArgument(x-dead-letter-exchange, order-event-exchange) .withArgument(x-dead-letter-routing-key, order.release.order) .withArgument(x-message-ttl, 30 * 60 * 1000) // 30 分钟 .build(); } // 死信队列真正的消费入口 Bean public Queue orderReleaseQueue() { return QueueBuilder.durable(order.release.queue).build(); } Bean public Binding delayBinding() { return BindingBuilder.bind(orderDelayQueue()) .to(new TopicExchange(order-event-exchange, true, false)) .with(order.create.order); } }TTL 队列有个众所周知的缺陷消息是排队过期的队头那条没过期后面的就算到期了也出不来时间精度依赖队列顺序。订单场景一般能接受因为下单时间本身就有先后。真要做到精确就装rabbitmq_delayed_message_exchange插件改走延迟交换机配置只改一行x-delayed-type。5. 排查清单本地起不来、Feign 404、事务不回滚的 5 个血泪记录这套代码我前后在不同机器上跑过几遍下面五条是复现率最高、报错信息最会骗人的。每条按现象、原因、解决写遇到同类问题直接对号入座。5.1 服务注册上了Feign 调用 404现象Nacos 服务列表里能看到mall-product网关转发也正常但订单服务调商品服务时稳定返回 404日志里连请求路径都找不到。原因FeignClient(name mall-product)里的名字和spring.application.name大小写或中划线不一致或者被调服务配置了server.servlet.context-pathFeign 拼接路径时没带上。还有一种更隐蔽的两个服务名只差一个-Nacos 里显示成两条记录负载均衡随机挑中错误那条。解决先看 Nacos 实例列表里到底有几个名字逐个比对。有context-path的要么在 Feign 接口上显式拼全路径要么统一去掉。我习惯在公共模块里定义常量ServiceNameConst.PRODUCT两边都引用它从根上断掉手写字符串的可能。5.2 抛了异常Seata 却没回滚现象下单时库存分支抛了异常订单表回滚了但库存的stock_locked已经加上去了数据对不上。原因三种情况最常见。GlobalTransactional加在了 Controller 上Feign 调用发生在事务代理之外参与方的库没建undo_log表或者异常被 catch 后吞掉了没有重新抛出Seata 认为分支成功。解决注解下沉到 Service 编排方法给每个业务库执行一遍undo_log建表语句catch 里要么throw要么TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。排查时把 Seata 日志级别调到INFO能看到每个分支的注册和回滚记录比猜快得多。5.3 加了分布式锁压测还是超卖现象单机测试一切正常几百并发压测后库存出现负数。原因tryLock传了显式租期但没有续期业务执行超过租期后锁自动释放或者 key 的粒度是 SPU不同 SKU 之间互相等待超时后走了拿不到锁就直接扣的兜底逻辑。解决去掉显式租期交给看门狗或者在 finally 里判断isHeldByCurrentThread后再释放key 精确到wareId skuId。兜底逻辑改成抛异常不要拿不到锁就放行那是给超卖开后门。5.4 缓存和数据库对不上重启就好现象后台改了一个分类名前端列表还是旧的重启服务或者等一段时间又正常了。原因更新走了先删缓存、再写库写库还没提交读请求就把旧值回填了。或者延迟双删的延迟时间比一次读请求的耗时还短等于没删。解决改成先写库再删缓存延迟时间调到 300ms 以上并丢线程池执行。更稳妥的做法是给缓存加版本号更新时incr版本读的时候比对版本不一致就重建。5.5 网关跨域报头字段重复现象前端调接口浏览器控制台报The Access-Control-Allow-Origin header contains multiple valuesNetwork 里能看到两个同名响应头。原因网关配了一次跨域下游业务服务也配了一次两个头叠加。解决只保留网关一处把业务服务里的CrossOrigin和全局跨域配置全删掉。另外allowCredentials(true)时不能用addAllowedOrigin(*)要用addAllowedOriginPattern(*)否则响应头不会下发。6. 从能跑到敢讲链路验证、压测数据与二次开发的取巧点代码跑起来只是及格线能讲清楚每条链路的数据流向才算真的拿下了。我自己的验证习惯是从网关打一条完整请求一路看服务端日志的 traceId 是否贯穿任何一段断掉都说明中间件或配置有问题。下面这张表是我每次换机器部署后必过的清单比盲点页面快很多。验证点请求方式通过标准分类树缓存GET/api/product/category/catalog/json连打两次第二次日志里没有 SQL检索GET/api/search?keyword手机catelog3Id225返回聚合的品牌与规格面下单POST/api/order/submit带 skuId 与地址订单表有记录库存stock_locked增加关单把 TTL 临时改成 30 秒重启消费者超时后订单状态变已关闭库存解锁事务回滚下单时传一个不存在的优惠券 ID订单、库存、积分三张表全部无变化压测不用上重型工具wrk或ab打商品详情接口就够看瓶颈在哪。我一般先用 50 并发跑 60 秒看 P99 和错误率再翻倍到 200。如果 TPS 不涨反降八成是数据库连接池打满了先调spring.datasource.hikari.maximum-pool-size默认 10 在这套代码里明显偏小调到 20 到 30 之间比较合理再往上加之前先看慢 SQL。# 50 并发压商品详情持续 60 秒-T 指定超时 wrk -t4 -c50 -d60s -T5s \ http://localhost:88/api/product/skuinfo/info/1 # 顺带确认缓存生效压测前后各看一眼 Redis 命中情况 redis-cli info stats | grep -E keyspace_hits|keyspace_misses二次开发取巧的地方有两个。一是秒杀模块mall-seckill里库存预热到 Redis、下单走信号量限流、结果异步通知前端这套结构可以原样搬到任何抢购场景改的只是商品 ID 来源二是认证模块令牌校验和网关鉴权分离换 JWT 或者换成自定义 token 都只动一个过滤器。这两块是复用价值最高的其余模块更多是补齐页面的重复劳动。注意改秒杀模块前先把本地 RabbitMQ 的prefetch调成 1 再压测默认值会让消费端一次拉一堆消息日志看起来像串单其实是正常的批处理。说实话我第一次跑这套代码时栽在一个特别蠢的地方把gateway的uri写成了http://mall-product本地测试全通压测直接雪崩因为那等于绕过注册中心直连了一个固定 IP。从那以后我每次改完路由都强制先看一遍 Nacos 的实例 IP 是不是和写死的一致再跑一次网关到服务的连通性验证哪怕只是改了个缩进。代码是死的链路是活的多花三分钟确认能省掉一晚上的排查。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站