尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Spring Cloud Alibaba分布式商城复现:中间件、检索与下单事务
简介围绕谷粒商城微服务分布式电商项目整理的高级篇代码与配套材料面向具备Java后端基础、希望系统理解电商平台落地方式的开发者重点解决微服务拆分、服务治理与高可用架构等学习诉求。内容围绕用户、商品、订单、支付等服务展开可结合微服务架构图与分布式高级PDF梳理服务注册发现、负载均衡、API网关、分布式事务、消息队列、分库分表、分布式缓存、监控日志及CI/CD等核心环节并通过代码分析理解设计原则与最佳实践对订单创建触发库存扣减、缓存热点数据、跨服务一致性等场景也能形成更直观的认识。压缩包约26.32MB资源页未列具体文件总数与类型明细下载后可按架构图、文档和代码模块对照学习。已有1584人学习适合用于分布式电商项目复盘、技术选型参考和面试前知识串联。1. 直接拿 2020 版分布式商城源码复现最难的不是业务代码把 2020 年前后那套基于 Spring Cloud Alibaba 的分布式商城源码复制到本地mvn clean install能编译通过服务却一个接一个启动失败——这是多数人拿到这套代码后的真实状态。它不是一个启动后就能看到页面的 Demo而是把商品、检索、购物车、订单、秒杀、库存、会员、支付拆成十来个微服务、中间件依赖很多的完整工程。适合三类人需要把电商主链路讲清楚的面试者、需要一个能改能扩的毕设底座的学生、以及想在自己机器上复现分布式事务和缓存一致性场景的初中级后端。下面按「先把中间件跑通、再把三条核心链路走一遍、最后压测验证」的顺序拆每一步都给出可以直接照抄的配置和代码。2. 六件中间件的启动顺序Nacos、MySQL、Redis、ES、MQ、Seata 怎么排绝大多数启动失败不是代码问题而是中间件没启动、启动了但启动顺序不对、或者启动了但服务注册进去的地址根本不通。我的习惯是先把这一层彻底跑通再去碰业务代码否则后面每一个报错都会被误判成代码 bug排查成本翻几倍。2.1 依赖矩阵先看端口和内存再谈启动这套工程的中间件依赖比一般课程项目重先对着下面这张表把环境过一遍缺哪个补哪个不要一边启动服务一边发现少东西。组件在链路里的角色默认端口起之前必须确认什么Nacos注册中心 配置中心8848单机模式启动配置落 MySQL 而不是内嵌库MySQL各业务库、undo_log、Seata 自身库3306字符集 utf8mb4、时区 08:00、允许远程连接Redis缓存、购物车、分布式锁、Session6379设了密码就要同步改每个服务的配置Elasticsearch商品检索9200装了 ik 分词器堆内存别超过物理内存一半RabbitMQ上架消息、订单延时关单5672 / 15672管理台能登录队列别提前手工建Seata TC全局事务协调者8091注册到同一个 Nacos事务分组名要对得上提示整机 16G 内存是底线。ES 单独给 2G 堆Nacos 和 Seata 各 512M剩下六七个 JVM 每个 512M 起步8G 的机器跑到订单服务基本就 OOM 了。2.2 用 compose 把基础设施一次性拉起来手写六个docker run容易漏参数写成 compose 文件好处是端口、卷、环境变量都在一处后面换机器直接搬。version: 3 services: mysql: image: mysql:5.7 # tag 按本地已有镜像改别死抄 command: --character-set-serverutf8mb4 --collation-serverutf8mb4_general_ci --default-time-zone08:00 environment: MYSQL_ROOT_PASSWORD: root ports: [3306:3306] volumes: [./mysql:/var/lib/mysql] redis: image: redis:5 command: redis-server --appendonly yes --requirepass redis ports: [6379:6379] nacos: image: nacos/nacos-server:1.3.2 environment: MODE: standalone SPRING_DATASOURCE_PLATFORM: mysql MYSQL_SERVICE_HOST: 192.168.56.10 # 填宿主机对外 IP不要写 127.0.0.1 MYSQL_SERVICE_DB_NAME: nacos_config MYSQL_SERVICE_USER: root MYSQL_SERVICE_PASSWORD: root ports: [8848:8848] rabbitmq: image: rabbitmq:3-management ports: [5672:5672, 15672:15672] elasticsearch: image: elasticsearch:7.6.2 environment: discovery.type: single-node ES_JAVA_OPTS: -Xms512m -Xmx512m ports: [9200:9200] seata: image: seataio/seata-server:1.3.0 ports: [8091:8091] environment: SEATA_PORT: 8091参数里最容易被忽略的是 Nacos 的MYSQL_SERVICE_HOST。写成127.0.0.1时容器内的 Nacos 连的是它自己初始化脚本一跑就报连接超时写成宿主机内网 IP容器才能回连到同一台机器上的 MySQL。ES 的ES_JAVA_OPTS不设上限默认会吃掉一半物理内存机器上再跑几个 JVM 就直接卡死。RabbitMQ 用带-management的 tag15672 端口能看队列堆积出问题时比翻日志快得多。启动顺序建议按「MySQL → Nacos → Redis → RabbitMQ → ES → Seata → 业务服务」来Nacos 依赖 MySQL 建库建表Seata 依赖 Nacos 做注册业务服务又依赖前面全部。全量docker ps看着都是 Up 不代表能用要逐个验证Nacos 控制台能登录、curl localhost:9200返回集群信息、RabbitMQ 后台能进、Seata 控制台能看到自己注册上来的 TC。2.3 服务端的 bootstrap.yml命名空间和 DataId 规则配置中心用不明白十有八九是 DataId 拼错了。约定是${spring.application.name}-${spring.profiles.active}.${file-extension}少一段都拉不到配置。spring: application: name: mall-product cloud: nacos: discovery: server-addr: 192.168.56.10:8848 namespace: 7c1f9a2e-3b4d-4c8a-9e11-2f6a8b0c1d23 # dev 环境独立命名空间 config: server-addr: 192.168.56.10:8848 file-extension: yaml group: DEFAULT_GROUP namespace: 7c1f9a2e-3b4d-4c8a-9e11-2f6a8b0c1d23namespace一定要填 ID 而不是名字控制台上复制的是 ID填名字时客户端不报错但会发现配置永远拉不到属于典型的没有报错的排查场景。group用默认组即可多环境靠 namespace 隔离比靠 group 隔离清晰。另外启动参数里记得带--spring.profiles.activedev不带的话 DataId 变成mall-product.yaml和你建的对不上。还有一个版本差异要留意Spring Boot 2.4 之后bootstrap.yml默认不加载必须显式引入spring-cloud-starter-bootstrap。这套 2020 年的工程大概率是 2.2 / 2.3 那一档还认 bootstrap但如果你自己升级了 Boot 版本服务会安静地只用本地配置注册中心里一个实例都看不到。2.4 网关路由、断言和跨域一次配完前端调用统一走网关路由配错的表现是 404 或者跨域报错两者都很好定位前提是路径重写规则写对。spring: cloud: gateway: routes: - id: product_route uri: lb://mall-product # lb 表示走注册中心负载均衡 predicates: - Path/api/product/** filters: - RewritePath/api/(?segment.*),/$\{segment} - id: search_route uri: lb://mall-search predicates: - Path/api/search/** filters: - RewritePath/api/(?segment.*),/$\{segment}RewritePath的作用是把前端约定的/api/product/...前缀去掉转发到后端的/product/...。lb://后面的名字必须和spring.application.name完全一致大小写和连字符错一个字符就是 503。跨域我一般放在网关统一处理用CorsWebFilter配addAllowedOriginPattern(*)、允许所有方法和请求头、allowCredentials(true)。注意网关配了跨域前端如果还挂着本地代理再叠一层浏览器会报The Access-Control-Allow-Origin header contains multiple values。跨域只在一层做要么网关要么 Nginx。3. 商品上架到 ES三级分类、SPU/SKU 与批量写入的落地动作商品这条链路是整套工程里表结构最复杂、也最能体现设计功力的部分。它的终点很明确用户在检索页输入一个关键词筛选品牌、分类、价格区间和规格属性返回的是 SKU 列表。所以真正的目标是把数据库里的 SPU/SKU 结构转换成一份适合检索的 ES 文档。3.1 三级分类一张表加一次全表查询别写三层 for分类表结构是典型的自关联cat_id/parent_cid/name/sort/cat_level。新手常写成递归查数据库一层一个select前端打开一个下拉框打十几次 SQL。// 一次查全表在内存里按 parentCid 分组建树分类总量通常在千级完全放得下 ListCategoryEntity all categoryDao.selectList(null); MapLong, ListCategoryEntity byParent all.stream() .filter(c - c.getShowStatus() ! null c.getShowStatus() 1) .collect(Collectors.groupingBy(CategoryEntity::getParentCid)); ListCategoryEntity roots byParent.getOrDefault(0L, Collections.emptyList()); roots.forEach(root - root.setChildren(byParent.getOrDefault(root.getCatId(), Collections.emptyList()))); return roots;parentCid 0是根节点sort字段决定兄弟节点顺序showStatus用来做逻辑下架。三级全部一次性建完的代价不过几百微秒比递归 SQL 快一个数量级也避免了 N1 查询。真正需要懒加载的是第四层往后的场景而电商分类几乎没有第四层。前端拿到的树形结构在提交商品时要落成catalog_id三级分类 ID和catalog_path1 2 3形式。catalog_path看着冗余但在检索页做面包屑导航时能省掉一次跨服务查询属于典型的拿空间换响应时间。3.2 SPU 与 SKU 的字段来源检索文档的每一个字段都要说得出出处这套表设计的核心是把「商品是什么」和「商品有哪些可售组合」拆开。检索文档一条对应一个 SKU字段来源如下ES 字段来源表说明skuId/spuIdpms_sku_info文档主键用 skuId保证幂等覆盖skuTitlepms_sku_info.sku_title参与全文检索用 ik 分词skuPricepms_sku_info.price检索用范围过滤下单时必须回 DB 重新验价catalogId/brandIdpms_spu_info分类与品牌聚合的维度字段attrspms_sku_sale_attr_value关联pms_attr规格属性做成 nestedhasStock库存服务上架时同步一次后续靠 MQ 更新skuPrice存成keyword而不是double是为了避免浮点精度带来的边界问题范围查询时用range配合字符串比较或者直接存「分」的整数。attrs必须用nested用object类型时attrId1 AND attrValue红色这种组合条件会退化成跨字段匹配出现「筛选 8G 内存却筛出 6G 的手机」这类排序错误。3.3 上架到 ESmapping 先定死再写 bulkmapping 一旦建好再改字段类型就要 reindex所以第一次建索引时把类型一次性确认清楚。PUT /product { mappings: { properties: { skuId: { type: long }, spuId: { type: long }, skuTitle: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, skuPrice: { type: keyword }, catalogId: { type: long }, brandId: { type: long }, hasStock: { type: boolean }, attrs: { type: nested, properties: { attrId: { type: long }, attrName: { type: keyword }, attrValue: { type: keyword } } } } } }写入侧用BulkRequest批量提交一条文档一个IndexRequestid用skuId。public void upSpu(Long spuId) { // 1. 取出该 SPU 下所有 SKU组装成检索模型含规格属性 ListSkuEsModel models searchFeign.getSkusBySpuId(spuId); if (CollectionUtils.isEmpty(models)) { return; } BulkRequest bulk new BulkRequest(); for (SkuEsModel m : models) { IndexRequest req new IndexRequest(product) .id(String.valueOf(m.getSkuId())) // 用 skuId 做文档 ID重复上架即覆盖 .source(JSON.toJSONString(m), XContentType.JSON); bulk.add(req); } try { BulkResponse resp client.bulk(bulk, RequestOptions.DEFAULT); if (resp.hasFailures()) { // 部分失败要打出来不能当整体成功 log.error(商品上架部分失败: {}, resp.buildFailureMessage()); } } catch (IOException e) { throw new RuntimeException(ES 上架异常, spuId spuId, e); } }几个参数直接决定成败。id用skuId让接口天然幂等重复调用不会产生重复文档。单批条数控制在 500 到 1000 之间太大容易触发es_rejected_execution太小则网络往返开销占主导。别在循环里调refreshtrue或者每条单独index一个 SPU 几百个 SKU 会把段合并压垮写入延迟翻十倍都不止。上架接口返回成功只代表请求发出去了前端的「上架」按钮应该做禁用防连点。3.4 增量同步与全量重建两条路分开走别混上架一次就同步一次是增量路线用消息驱动。商品服务在upSpu成功后发一条消息到product.up队列检索服务监听后调 ES 写入。中间任何一步失败都不应该阻断主流程商品在数据库里已经是上架状态检索侧晚几秒看到是可接受的。RabbitListener(queues product.up.queue) public void onUp(Long spuId) { try { searchService.upSpu(spuId); } catch (Exception e) { // 抛出后由重试机制接管超过次数进死信队列人工处理 throw new RuntimeException(上架消息处理失败, spuId spuId, e); } }全量重建是另一条路用一个手动触发的接口把指定分类或全部商品重新推一遍适合第一次初始化、或者 ES 索引 mapping 变更后重新灌数据。两条路必须能独立开关混用会导致「重建跑到一半增量消息进来把旧数据又覆盖回去」。提示让消费者幂等。同一条上架消息被重复投递是常态文档覆盖写天然幂等但如果你在消费端做了「库存累加」这类操作就必须加去重表或者用消息 ID 判重。4. 下单链路的锁与事务Redisson 兜住并发Seata 兜住一致性商品和检索是读多写少下单是写多、并发高、还跨服务跨库。这一段是整套代码里最容易出线上问题的部分也是面试里被追问最多的地方。我的处理原则是能用 Redis 挡掉的并发绝不让它落到数据库必须跨库保证的强一致交给 Seata其余一律走最终一致。4.1 缓存三兄弟穿透、击穿、雪崩各有各的处理方式穿透是查一个数据库里根本不存在的 ID每次都绕到 DB击穿是某个热点 key 过期瞬间大量请求同时回源雪崩是大量 key 同一时刻失效。三者的解法完全不同混着写就是无效防御。public SkuInfoVO getSku(Long skuId) { String key sku:info: skuId; String cached redisTemplate.opsForValue().get(key); if (cached ! null) { // 空串是防穿透的哨兵值表示「查过了确实没有」 return .equals(cached) ? null : JSON.parseObject(cached, SkuInfoVO.class); } RLock lock redissonClient.getLock(lock:sku: skuId); try { // 只等 3 秒拿不到锁就直接回源避免线程全部堵在锁上 if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { cached redisTemplate.opsForValue().get(key); // 二次确认可能已被别的线程填好 if (cached ! null) { return .equals(cached) ? null : JSON.parseObject(cached, SkuInfoVO.class); } SkuInfoVO db skuDao.selectById(skuId); if (db null) { redisTemplate.opsForValue().set(key, , 60, TimeUnit.SECONDS); // 空值 TTL 要短 return null; } redisTemplate.opsForValue().set(key, JSON.toJSONString(db), 30 ThreadLocalRandom.current().nextInt(10), TimeUnit.MINUTES); return db; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } return skuDao.selectById(skuId); // 抢锁失败退化为直查宁可慢也别阻塞 }几个参数值是有讲究的。空值缓存 TTL 设 60 秒太长了会让新上架的商品 5 分钟内搜不到详情正常缓存 TTL 加 0 到 10 分钟的随机扰动避免同一批 key 整点集体失效这就是防雪崩最省事的做法。锁的waitTime只给 3 秒是因为在秒杀这类场景下让请求快速失败退化为直查比让几千个线程挂在锁上把 Tomcat 线程池打满要好。isHeldByCurrentThread()判断不能省否则锁超时自动释放后再unlock会抛IllegalMonitorStateException这个异常在日志里非常容易误导人。4.2 缓存一致性删缓存而不是更新缓存顺序别搞反下单改价格、改库存这类场景一定是「先写数据库再删缓存」不是「先写库再更新缓存」。更新缓存的问题在于并发写时的后写者可能把先写者的结果覆盖掉而删缓存只是让下一次读回源天然规避了顺序问题。延迟双删是常见做法更新 DB 前删一次更新后睡 500 毫秒再删一次。第二次删是为了兜住「删完缓存的瞬间另一个读请求把旧值又写回去了」这个窗口。睡眠时间别拍脑袋取业务读接口的 P99 响应时间作为参考值。这套代码里读接口一般几十毫秒500 毫秒足够。真正稳的方案是订阅 binlog。用 Canal 解析 MySQL 的变更投递到 MQ由专门的消费者去删缓存。好处是缓存删除和业务代码彻底解耦哪怕有人绕过 Service 直接改库也能同步到。代价是运维成本多一个组件中小规模的项目用「删缓存 短 TTL 兜底」就够TTL 一定要设它是最后一道保障任何时候都不能省。4.3 下单扣库存Redis 预扣加 Lua别用 get 再 decr秒杀和普通下单都要先扣库存。get出来判断再decr是两条命令中间这段时间足够超卖几十单。-- KEYS[1] 库存 key如 sku:stock:1001ARGV[1] 扣减数量 -- 返回 -1 库存不足返回剩余库存表示扣减成功 local stock tonumber(redis.call(get, KEYS[1])) local num tonumber(ARGV[1]) if stock nil then return -1 end if stock num then return -1 end return redis.call(decrby, KEYS[1], num)Lua 脚本在 Redis 里单线程执行校验和扣减是一个原子操作这是它存在的全部理由。加载方式用DefaultRedisScriptLong脚本内容放进resources/scripts/启动时加载一次别每次调用都传脚本字符串那样会白白多一次脚本缓存的网络传输。扣减成功后发一条 MQ 消息去落数据库落库失败要把 Redis 里的库存加回去。这里要接受一个事实Redis 预扣和数据库落账之间永远存在短暂不一致的窗口靠的是消息重试加定时对账任务兜底。对账任务每天跑一次比对 Redis 库存和sku_stock表的差值超过阈值报警。4.4 Seata AT 模式事务边界画在下单入口不要画在所有地方跨库的两步写是下单和扣减库存这两步用GlobalTransactional包住其余的远程查询、发消息全部放在事务外面。GlobalTransactional(rollbackFor Exception.class, timeoutMills 30000) public SubmitOrderResp submitOrder(SubmitOrderReq req) { // 1. 验价、验库存只读放事务内也不会产生 undo_log OrderConfirmVO confirm orderService.checkPrice(req); // 2. 扣减库存远程调用被调用方需要开启 Transactional 并参与全局事务 wareFeign.lockStock(req.getSkuId(), req.getNum()); // 3. 创建订单本地库写入Seata 会记录 beforeImage / afterImage 到 undo_log OrderEntity order orderService.createOrder(req, confirm); return new SubmitOrderResp(order.getOrderSn()); }rollbackFor Exception.class不能省Seata 默认只回滚RuntimeException和Error业务里抛的受检异常会被直接忽略全局事务照常提交这是最常见的「事务没生效」原因。timeoutMills设 30 秒超过 30 秒 TC 会主动回滚比让请求无限挂着好。服务端要准备三样东西业务库里建undo_log表每个参与全局事务的库都要有、Seata 客户端配置里tx-service-group和 TC 侧的事务分组名对齐、TC 注册到同一个 Nacos 命名空间。undo_log表结构照官方脚本建别自己简化字段少了log_status或context会在回滚时报反序列化异常。注意AT 模式依赖主键。表没有主键时镜像数据无法定位到具体行回滚会失败但事务状态显示已回滚属于典型的不易察觉的问题。另外GlobalTransactional标注的方法被同类内部this调用时代理不生效和Transactional是同一个坑。5. 排查记录五个卡住半天的启动与调用故障下面这五条是这套工程里重复出现频率最高的故障每一条都按「现象 → 原因 → 解决」写遇到类似情况可以直接对号入座。5.1 服务全绿但 Feign 调用超时现象Nacos 控制台里所有实例都在线网关日志却是Read timed out executing GET http://mall-product/...。原因注册到 Nacos 的 IP 是容器内网 IP172.x 段调用方从宿主机根本路由不到健康检查却能通。Nacos 客户端默认取网卡的第一个非回环地址容器里拿到的就是内部 IP。解决容器启动时固定spring.cloud.nacos.discovery.ip为宿主机对外 IP或者给 Nacos 容器加network_mode: host。验证办法很直接在 Nacos 实例列表里点开详情看 IP 是不是你能ping通的那个。5.2 登录后下单提示未登录现象前端登录成功页面上有用户信息一调下单接口就返回 401 或者用户 ID 为空。原因网关校验完 JWT 后把用户信息放进了请求头但 Feign 默认不转发请求头跨服务调用时Cookie、Authorization全部丢失下游的拦截器取不到用户。解决加一个RequestInterceptor从当前请求上下文里取需要透传的头塞进 Feign 请求并配一个ThreadLocal在异步线程池里做上下文传递。坑在于异步编排之后RequestContextHolder拿不到东西必须在切线程前手动set一次。5.3 全局事务抛了异常却没回滚现象下单接口抛了业务异常前端收到错误提示但订单表里已经有一条记录库存也扣了。原因三种可能一是异常被try-catch吞掉没往外抛二是抛的是受检异常而没配rollbackFor三是GlobalTransactional方法被同一个类里的其他方法this调用代理失效注解根本没生效。解决先看方法调用栈是不是走了代理断点打在该方法第一行看this是不是代理对象再看异常是否真的抛到了入口层最后检查注解的rollbackFor。三层排完基本就定位了。5.4 商品上架成功但检索搜不到现象上架接口返回 200数据库里商品是上架状态检索页一个都搜不出来。原因中文分词器没装。skuTitle的 mapping 里指定了ik_max_wordES 里没有这个插件时建索引会直接失败而如果先建了索引后改 mapping写入会静默丢字段。第二种情况是字段被默认映射成text却按keyword精确匹配去查永远查不中。解决docker exec进容器执行elasticsearch-plugin list确认 ik 已安装没有就装完重启。索引 mapping 变更后必须 reindex不能直接改改完调一次全量重建接口重灌数据。5.5 MQ 重试导致库存多扣现象一笔订单扣了两次库存sku_stock表里数量对不上日志里同一条消息被消费了两次。原因消费者处理完业务逻辑后抛了异常比如发通知失败RabbitMQ 认为消费失败重投或者消费者在收到消息后还没来得及 ack 就断连了。这两种情况下重复消费是必然的不是偶发。解决消费端必须幂等。用消息 ID 或者「订单号 操作类型」建唯一索引插不进去就说明处理过了直接 ack 返回。同时把非核心逻辑发短信、发通知从主消费链路里拆出去别让一个通知失败把整个库存处理拖成重试。6. 压测与验证把秒杀接口打到瓶颈的一套参数和观察点前面几步跑通只说明功能可用「能不能扛」要单独验证。压测最容易犯的错误是一上来就压写接口看到 TPS 低就改代码改完再压还是低因为瓶颈根本就不在代码上。我的顺序是先压读接口做基线再压写接口找拐点。6.1 压测前先做的三件事第一把日志级别从DEBUG调到INFO异步 appender 打开否则磁盘 IO 会先成为瓶颈。第二把连接池配好HikariCP 的maximum-pool-size按「CPU 核数 × 2 磁盘数」估一般 20 到 30 就够配到 100 只会让数据库更慢。第三压测机和被测服务分开机器同一台机器上 JMeter 和 JVM 抢 CPU测出来的数字没有参考价值。6.2 JMeter 的参数配置秒杀接口需要带 token所以用setUp线程组先登录一次拿到 token写进 JMeter 属性或者 CSV主线程组通过${__P(token)}引用。主线程组的参数我一般这么设参数取值理由线程数200 / 500 / 1000 三档递增找 TPS 拐点不是单点数字Ramp-up10 秒避免启动瞬间的假尖峰循环次数勾选「永远」勾选调度器用持续时间控制更稳持续时间每档 180 秒前 30 秒算预热不计入统计集合点200 并发同一时刻秒杀场景必须模拟瞬时并发断言HTTP 200 且业务code 0只看 HTTP 码会把业务失败算成成功不用 JMeter 的话wrk更适合快速摸基线# -t 线程数 -c 并发连接数 -d 持续时长压读接口先看裸吞吐 wrk -t8 -c500 -d60s --latency http://192.168.56.10:10000/api/product/skuinfo/1001-c不要超过压测机的文件句柄上限--latency会额外输出 P50 / P99 分布比平均值有用得多。6.3 看哪些指标怎么判断拐点只看 TPS 是不够的必须同时看四组数响应时间P95 和 P99不看平均、错误率、服务端 GC 停顿、以及 Redis 和数据库的压力。判断拐点的标准是「TPS 不再随并发增长而 P99 开始抬头」这个点就是服务的真实容量。服务端的黑匣子主要看三处jstat -gcutil pid 1000看 Full GC 频率秒杀压测下如果每秒都在 Full GC先查是不是对象创建太猛而不是加大堆HikariCP 的activeConnections如果长时间等于maximumPoolSize说明数据库是瓶颈Redis 的info stats看keyspace_hits命中率命中率掉到 90% 以下说明缓存策略有问题。秒杀接口还要单独确认一件事限流有没有真的生效。Sentinel 的流控规则建议在控制台配成 QPS 模式、阈值设为你测出来的容量的 80%然后压测时观察被限流的请求是不是快速失败返回而不是排队等待。快速失败配合前端的「活动太火爆」提示比让用户等到超时体验好得多。提示压测完记得把 Redis 里预扣的库存重置、清掉测试订单否则下一次压测的数据全部失真——这个坑我踩过一次两轮数据对比完全对不上查了两小时才发现是脏数据。整套代码我前前后后重装过七八次环境最后一次是因为换了台笔记本忘了给 ES 单独限堆机器跑到订单压测那一步直接卡死。从那以后我每次重新部署这套工程都强制走一遍「中间件逐个验证 → 单服务启动 → 网关连通 → 读接口冒烟 → 写接口压测」这五步一步都不跳。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

HJ212协议报文解析:Java解码、CRC16与粘包处理

HJ212协议报文解析:Java解码、CRC16与粘包处理

简介:面向环保监测系统开发者的HJ212协议Java解析实践包,围绕《污染源在线自动监控(监测)系统数据传输标准》,提供数据采集、编码解码与交互处理的参考实现,适合需要对接污染源在线监测设备、处理XML/JSON报…

📅 2026/11/2 2:02:20
Android城市选择器实战:拼音索引、定位与搜索过滤

Android城市选择器实战:拼音索引、定位与搜索过滤

简介:Android城市选择器是一套面向Android开发者的可复用城市选择组件,模仿美团城市选择界面,适合电商、旅游、社交和生活服务类应用快速加入城市切换功能。压缩包共285个文件、约908KB,包含164个class、31个java源码、40个xml布局…

📅 2026/11/2 2:02:20
UNet图像分割实战:PyTorch从掩膜生成到滑窗推理与ONNX校验

UNet图像分割实战:PyTorch从掩膜生成到滑窗推理与ONNX校验

简介:面向Python图像处理与深度学习实践者的U-Net图像分割资料包,适合希望从数据准备、模型构建、损失函数选择到训练预测完整走通分割流程的初中级开发者,也可用于医学影像、遥感等场景的入门复现。压缩包共21个文件、约5.6MB,以…

📅 2026/11/2 2:02:20
MORE NEWS

更多资讯

📰

OpenFOAM风工程实战:方程即代码与分层架构解析

简介:这份PDF面向刚接触开源CFD工具的研究生、工程师与科研人员,系统梳理OpenFOAM的来龙去脉与核心能力,帮助读者在动手建模前建立对软件架构与设计理念的整体认识。资源共1个PDF文件,压缩包约162KB,内容源自南京工业大…

📰

Harness不是被模型吃掉,而是升级为AI能力仲裁者

1. 这个问题到底在问什么:从“Harness会不会被模型吃掉”看AI工程落地的真实困境“Harness会不会被模型吃掉”——乍一看像句玩笑话,但如果你最近两周翻过GitHub Trending、刷过知乎Agent话题热帖、或者在VS Code里反复调试过harness failed to load plu…

📰

React Native设备信息获取核心指南:从基础API到生产级实践

1. 这不是“又一个工具库”,而是React Native项目里最常被低估的基础设施你有没有遇到过这样的场景:App上线后运营同学突然问,“iOS用户占比多少?”,你翻遍代码发现没存设备系统版本;灰度发布时想按Android…

📰

解决Trae编辑器Ctrl+左键无法跳转C++函数定义问题

1. 问题现象与初步排查在Trae编辑器中遇到Ctrl左键无法跳转C函数定义的问题时,我首先进行了基础环境检查。从GitHub issue #606的反馈来看,这个问题具有以下特征:语言特异性:不仅影响C,Python和Java同样存在此问题环境…

📰

端侧大模型部署工程师:芯片级LLM落地的五层能力栈

1. 这不是“调个API”就能糊弄过去的新岗位:端侧大模型部署工程师的真实战场“端侧大模型部署工程师”——这八个字最近在招聘平台和工程师社群里炸开,薪资带宽直接对标AI架构师,但简历投进去十有八九石沉大海。我去年帮三家做边缘AI硬件的公…

📰

单电感升降压斩波拓扑与双闭环MATLAB仿真解析

简介:围绕升、降压直流斩波电路的设计与仿真,这份面向电力电子课程设计学习者及初学者的文档,系统梳理DC/DC变换器的拓扑选择、工作模式与储能电感参数设计,并讲解PWM闭环控制、占空比调节和反馈稳压思路。正文从降压斩波电路的工…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬