简介:这是一套高分通过、经导师指导的分布式网上商城毕设项目,面向计算机专业本科生及Java进阶学习者,解决毕业设计选题难、分布式架构实战经验不足、全栈项目整合能力弱等核心问题,亦适用于课程设计与期末大作业。资源包共792个文件,涵盖115个Java后端模块、45个Vue前端组件、164个JS交互逻辑、53个CSS样式文件及1个完整MySQL 5.7数据库SQL脚本,辅以开发说明文档、答辩PPT、开题报告(LW)和3个批处理启动脚本(install/run/build),结构清晰、开箱即用。目前已有450人学习下载,所有模块均经严格调试,支持JDK1.8+SpringBoot+Tomcat7+Navicat11环境一键部署。读者可直接获得权限分明的双角色系统(管理员含订单/商品/用户/系统管理,用户支持信息查看与个人中心)、前后端分离工程规范、典型微服务模块划分(如商品、订单、用户服务独立部署)及真实电商场景下的分布式事务与接口联调实践参考。
1. 为什么一个“SpringCloud+Vue网上商城”压缩包,让很多开发者卡在启动第一分钟?
这不是一个简单的毕设模板,而是一套真实可跑、边界清晰、暴露分布式典型矛盾的最小闭环系统。它用 SpringCloud Alibaba(Nacos + Sentinel + Seata)做后端服务治理,Vue3 + Pinia + Element Plus 做前端工程化,MySQL + Redis + RabbitMQ 构成数据与消息底座——所有组件版本对齐、配置可查、接口连通、数据库脚本完整。我见过太多人解压后直接npm run serve和mvn spring-boot:run,结果卡在 Nacos 连不上、Feign 调用 404、Vue 请求跨域 500、或者下单事务根本没回滚。问题不在代码写得差,而在于分布式不是单体加个注解就能跑通:服务注册时机、网关路由前缀、Feign 的 fallback 配置粒度、Redis 连接池超时与主线程阻塞的耦合、甚至 MySQL 的transaction-isolation级别都会让整个链路静默失败。它适合两类人:一是刚学完 SpringCloud 概念、急需一个“能摸到”的实物来验证熔断/降级/分布式事务逻辑的中级开发者;二是需要快速搭建演示环境、但又不愿被“假微服务”(如只拆包不拆库)误导的技术负责人。下面,我们不讲理论,只做三件事:把服务真正拉起来、让一次下单完整走通、然后告诉你哪几个配置项改错一个,整条链路就变成黑匣子。
2. 本地环境拉起:从解压到看到首页的 7 个确定性步骤
这个压缩包不是“开箱即用”,而是“开箱即调”。它的价值恰恰在于暴露了真实部署中必须面对的细节。我一般会按以下顺序推进,跳过任意一步都可能在后续环节付出双倍时间。
2.1 解压后先看懂目录结构:别急着 run,先认门
解压后你会看到类似这样的根目录:
shop-system/ ├── docs/ # 数据库建表SQL、接口文档(Markdown)、部署说明 ├── gateway/ # SpringCloud Gateway 网关模块(含路由配置) ├── order-service/ # 订单服务(含 Seata AT 模式事务) ├── product-service/ # 商品服务(含库存扣减逻辑) ├── user-service/ # 用户服务(含 JWT 登录鉴权) ├── vue-shop/ # Vue3 前端项目(注意:不是 public 目录!) └── pom.xml # 根 POM,定义父版本和模块依赖提示:
docs/下的shop_db.sql是唯一可信的建库脚本,别用 Navicat 自动生成的 DDL;application-dev.yml中的spring.profiles.active默认是dev,但实际运行时你很可能要切local或nacos,这点后面会踩坑。
2.2 启动 Nacos Server:不是下载就完事,关键在端口与模式
Nacos 是整个服务发现与配置中心的基石。压缩包里通常附带nacos-server-2.2.3.zip(SpringCloud Alibaba 2022.x 兼容版),但直接双击startup.cmd很可能失败——因为默认是集群模式,而本地开发只需单机。
# 进入 nacos/bin 目录后执行(Windows) startup.cmd -m standalone # macOS / Linux sh startup.sh -m standalone启动成功标志:访问http://localhost:8848/nacos,账号密码默认nacos/nacos,首页右上角显示"Standalone Mode"。如果看到 "Cluster Mode" 或页面空白,请检查conf/application.properties中是否误写了nacos.core.mode=cluster,或logs/start.out是否报Port 8848 is already in use。
参数说明:
-m standalone强制单机模式,绕过 Raft 协议初始化;若你本地 8848 被占用,需在conf/application.properties中修改server.port=8849,并同步更新所有服务bootstrap.yml里的server-addr: localhost:8849。
2.3 初始化数据库:MySQL 8.0+ 必须显式设置时区与隔离级别
docs/shop_db.sql脚本看似简单,但有两处硬性要求:
- MySQL 版本 ≥ 8.0.22(因用了
JSON类型存订单快照); - 必须在创建数据库时指定字符集与排序规则:
CREATE DATABASE `shop_db` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;执行脚本后,立刻验证关键参数:
-- 查看当前会话隔离级别(必须是 REPEATABLE-READ) SELECT @@transaction_isolation; -- 查看全局时区(必须是 +08:00,否则 Seata 时间戳解析异常) SELECT @@global.time_zone, @@session.time_zone;若time_zone是SYSTEM,请执行:
SET GLOBAL time_zone = '+08:00'; SET time_zone = '+08:00';注意:MySQL 8.0 默认
caching_sha2_password插件,SpringBoot 2.3+ 需在 JDBC URL 加?serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useSSL=false,否则连接直接拒绝。
2.4 启动各微服务:顺序、配置、日志三者缺一不可
服务启动有强依赖顺序:Nacos → Config Server(如有)→ User-Service → Product-Service → Order-Service → Gateway。其中user-service是认证中心,其他服务启动时会向其请求公钥;product-service的库存扣减是order-service下单的前置依赖。
以user-service为例,正确启动命令:
cd user-service mvn clean compile -Dmaven.test.skip=true java -jar target/user-service.jar \ --spring.profiles.active=dev \ --server.port=8001 \ --spring.cloud.nacos.discovery.server-addr=localhost:8848 \ --spring.cloud.nacos.config.server-addr=localhost:8848关键点:
--spring.profiles.active=dev必须显式传入,不能只靠application.yml里的default;--server.port用于避免端口冲突(product-service用 8002,order-service用 8003);- 日志中必须看到
Located property source: CompositePropertySource和Registering service with nacos才算注册成功。
逻辑说明:SpringCloud 启动时先加载
bootstrap.yml(读 Nacos 配置),再加载application.yml(本服务配置)。若bootstrap.yml中spring.cloud.nacos.config.group写成DEFAULT_GROUP(Nacos 默认组),但你在 Nacos 控制台新建配置时选了SHOP_GROUP,服务将无法拉取配置,表现为@Value("${xxx}")为空。
2.5 启动 Vue 前端:代理配置决定你能否看到登录页
进入vue-shop/目录,不要直接npm install—— 先确认package.json中node和npm版本要求(常见为node >= 16.14.0,npm >= 8.19.0):
# 检查版本 node -v # 必须 ≥ 16.14.0 npm -v # 必须 ≥ 8.19.0 # 安装依赖(推荐使用 cnpm 或 pnpm 加速) npm install --registry=https://registry.npmmirror.com # 启动开发服务器 npm run serve此时访问http://localhost:8080,大概率看到白屏或 404。原因几乎 100% 出在vue.config.js的代理配置:
// vue-shop/vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', // ⚠️ 注意:这是网关端口,不是某个服务端口! changeOrigin: true, pathRewrite: { '^/api': '' // 把 /api/order/create 重写为 /order/create 发给网关 } } } } }参数说明:
target必须指向gateway服务(默认8081),而非user-service(8001);pathRewrite是关键,Vue 请求/api/login,代理后发给网关的是/login,网关再根据路由规则转发给user-service。若此处写错,所有请求 404,控制台 Network 面板能看到Failed to load resource: the server responded with a status of 404 ()。
2.6 验证服务互通:用 curl 绕过前端,直击链路核心
当 Vue 页面仍白屏时,最高效的验证方式是跳过浏览器,用curl检查底层链路:
# 1. 检查网关是否收到请求(返回 200 且含 HTML) curl -I http://localhost:8081/ # 2. 检查用户服务健康状态(返回 JSON 含 "status":"UP") curl http://localhost:8001/actuator/health # 3. 模拟登录(获取 token,注意 Content-Type) curl -X POST http://localhost:8081/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 4. 用 token 调用商品列表(验证网关路由+服务转发) curl -H "Authorization: Bearer eyJhbGciOi..." \ http://localhost:8081/product/list若第 3 步返回401 Unauthorized,说明user-service未正确加载 JWT 密钥(检查resources/application-dev.yml中jwt.secret是否与前端utils/request.js里一致);若第 4 步返回503 Service Unavailable,说明product-service未在 Nacos 注册成功,或网关路由配置中uri: lb://product-service的服务名拼写错误(大小写敏感!)。
3. 让一次下单真正走通:从点击“立即购买”到数据库落库的 5 层穿透
能启动不等于能跑通业务。这个商城最考验分布式能力的场景就是「下单」:它横跨用户校验、库存扣减、订单生成、支付模拟、消息通知 5 个环节,且要求强一致性(库存不能超卖、订单不能重复)。下面以order-service的OrderController.createOrder()为锚点,逐层拆解。
3.1 前端触发:Pinia 状态管理如何把用户行为转为原子请求
Vue 端下单按钮绑定的是handleCreateOrder()方法,其核心逻辑在stores/order.js:
// vue-shop/src/stores/order.js export const useOrderStore = defineStore('order', { actions: { async createOrder(payload) { try { const res = await api.post('/order/create', payload, { headers: { Authorization: `Bearer ${this.token}` } // ⚠️ token 来自 user store }); this.orderId = res.data.orderId; // 触发消息队列通知物流服务(伪代码) this.$socket.emit('order_created', { orderId: res.data.orderId }); } catch (e) { ElMessage.error(e.response?.data?.message || '下单失败'); } } } });关键点:
payload包含userId,productId,count,addressId,由cartStore和userStore组合而来;Authorization头必须存在,否则网关AuthFilter直接拦截返回401;api.post()封装了统一错误处理,但不会自动重试——这意味着前端不做兜底,后端就必须保证幂等。
逻辑说明:Vue 的响应式依赖收集在此处生效。若
cartStore中购物车数据未通过getCartList()接口刷新,payload里的count可能是旧值,导致库存扣减数错误。这不是 Bug,而是分布式系统中“状态最终一致性”的体现:前端缓存 ≠ 后端实时库存。
3.2 网关路由:Gateway 如何把/order/create分发到order-service
gateway模块的application-dev.yml中定义了核心路由:
spring: cloud: gateway: routes: - id: order-service uri: lb://order-service # ⚠️ lb 表示负载均衡,依赖 Nacos 服务发现 predicates: - Path=/order/** filters: - StripPrefix=1 # 去掉 /order 前缀,/order/create → /create - RewritePath=/order/(?<segment>.*), /$\{segment} # 同上,更灵活写法当请求http://localhost:8081/order/create到达网关:
Path=/order/**匹配成功;StripPrefix=1将路径重写为/create;lb://order-service从 Nacos 获取order-service实例列表(如192.168.1.100:8003),按轮询策略选择一台;- 请求被转发至
http://192.168.1.100:8003/create。
参数说明:
lb://是 SpringCloud Gateway 的专用协议,表示“从服务注册中心拉取实例并负载均衡”;若此处写成http://localhost:8003,则失去服务发现能力,且无法横向扩展。
3.3 订单服务:Seata AT 模式如何保障“扣库存+生订单”原子性
order-service的OrderController.createOrder()方法被@GlobalTransactional注解包裹:
@GlobalTransactional(timeoutMills = 30000, name = "create-order") public Result<OrderVO> createOrder(@RequestBody OrderDTO dto) { // 1. 校验用户余额(调用 user-service FeignClient) userClient.deductBalance(dto.getUserId(), dto.getPayAmount()); // 2. 扣减库存(调用 product-service FeignClient) productClient.decreaseStock(dto.getProductId(), dto.getCount()); // 3. 创建订单(本地数据库 insert) Order order = buildOrder(dto); orderMapper.insert(order); return Result.success(new OrderVO(order)); }Seata 在此过程中做了三件事:
- TM(Transaction Manager):
@GlobalTransactional标记的入口方法,向 TC(Seata Server)申请全局事务 ID(XID); - RM(Resource Manager):每个
@DataSourceTransactionManager管理的数据源,在 SQL 执行前后生成undo_log表记录(如库存扣减前的stock=100); - TC(Transaction Coordinator):独立部署的 Seata Server,协调所有分支事务提交或回滚。
关键验证点:若
productClient.decreaseStock()抛出异常(如库存不足),userClient.deductBalance()的余额扣减会自动回滚——这要求user-service和product-service都集成 Seata RM,并在undo_log表中记录补偿日志。若某服务漏配seata-spring-cloud-starter-alibaba依赖,该分支将无法回滚,造成数据不一致。
3.4 库存服务:Redis 缓存与 MySQL 的双写一致性怎么破
product-service的decreaseStock()方法采用“先删缓存,再更新 DB,最后异步回写缓存”策略:
@Transactional public void decreaseStock(Long productId, Integer count) { // 1. 删除 Redis 中的库存缓存(防止脏读) redisTemplate.delete("product:stock:" + productId); // 2. 更新 MySQL 库存(带乐观锁) int updated = productMapper.updateStockById(productId, count); if (updated == 0) { throw new BusinessException("库存不足"); } // 3. 发送 MQ 消息,由 consumer 异步重建缓存 rabbitTemplate.convertAndSend("stock.exchange", "stock.update", productId); }这里没有用@CacheEvict,因为:
@CacheEvict在事务提交后才执行,期间其他线程可能读到旧缓存;- 主动
delete确保缓存失效发生在 DB 更新之前,规避“读已提交”隔离级别下的幻读。
参数说明:
updateStockById对应的 SQL 使用version字段实现乐观锁:
UPDATE product SET stock = stock - #{count}, version = version + 1 WHERE id = #{id} AND version = #{version}若version不匹配,updated返回 0,抛出业务异常,触发 Seata 全局回滚。
3.5 消息通知:RabbitMQ 的 confirm 机制如何确保物流通知不丢
下单成功后,order-service会发送一条order.created消息到 RabbitMQ:
// order-service 中 rabbitTemplate.convertAndSend( "order.exchange", "order.created", new OrderCreatedEvent(orderId, userId, productId) );但生产环境必须开启publisher-confirm(发布确认):
# order-service application-dev.yml spring: rabbitmq: publisher-confirm-type: correlated # ⚠️ 必须设为 correlated,支持回调 template: mandatory: true # 消息路由失败时触发 ReturnCallback对应配置回调 Bean:
@Component public class OrderConfirmCallback implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (!ack) { log.error("消息发送失败,correlationData: {}, cause: {}", correlationData, cause); // 此处可加入死信队列重试逻辑 } } @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { log.warn("消息退回,replyText: {}, routingKey: {}", replyText, routingKey); } }逻辑说明:
confirm回调解决“消息是否到达 Broker”,returned回调解决“消息是否被路由到 Queue”。若exchange不存在或routingKey无匹配 queue,returnedMessage会被触发。这是比try-catch更底层的可靠性保障。
4. 避坑指南:5 个让开发者连续加班到凌晨的真实翻车现场
这些不是教科书里的“可能遇到的问题”,而是我在三个不同团队复现该系统时,亲手踩过的、导致整条链路静默失败的硬核坑。每一条都附带现象、根因和可立即执行的修复命令。
4.1 现象:Nacos 控制台能看到服务注册,但 Feign 调用始终 404
原因:@FeignClient的name属性与 Nacos 中服务名大小写不一致。Nacos 服务名默认全小写,但开发者在@FeignClient(name = "ProductService")中写了驼峰,导致 Feign 无法匹配实例。
解决:统一改为小写,并检查application.yml中spring.application.name是否一致:
# product-service application-dev.yml spring: application: name: product-service # 必须全小写、中划线// order-service 中 @FeignClient(name = "product-service") // 与上面完全一致 public interface ProductClient { ... }4.2 现象:下单时 Seata 报Could not find global transaction xid,但 XID 明明存在
原因:order-service的SeataAutoConfiguration未生效,因pom.xml中seata-spring-cloud-starter-alibaba依赖范围写成了<scope>test</scope>。
解决:删除 scope 或改为compile,并确认bootstrap.yml中spring.cloud.alibaba.seata.tx-service-group与 Seata Serverregistry.conf中service.vgroupMapping.my_test_tx_group的 group 名一致:
<!-- order-service pom.xml --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>seata-spring-cloud-starter-alibaba</artifactId> <!-- 删除 <scope>test</scope> --> </dependency>4.3 现象:Vue 登录成功,但后续所有请求返回403 Forbidden
原因:网关AuthFilter中 JWT 解析失败,因user-service的jwt.secret是随机生成的字符串(如@Value("${jwt.secret:#{T(java.util.UUID).randomUUID().toString()}}")),每次重启都变,导致网关无法用同一密钥验签。
解决:在user-service和gateway的application-dev.yml中,硬编码一个固定 secret:
jwt: secret: shop-2024-super-secret-key-123456 # 两端必须完全一致4.4 现象:商品列表接口返回空数组,但 MySQL 中有数据
原因:product-service的 MyBatisProductMapper.xml中,<select>标签的resultMap引用了不存在的ProductVO,MyBatis 静默忽略映射,返回空对象集合。
解决:检查resultMapID 是否与ProductVO类字段完全匹配(包括下划线转驼峰):
<!-- product-service src/main/resources/mapper/ProductMapper.xml --> <resultMap id="ProductVO" type="com.shop.vo.ProductVO"> <id property="id" column="id"/> <result property="productName" column="product_name"/> <!-- column 必须是数据库字段名 --> <result property="stock" column="stock"/> </resultMap>4.5 现象:RabbitMQ 消费者收不到消息,rabbitmqctl list_queues显示 queue 为空
原因:order-service发送消息时指定了routingKey="order.created",但logistics-consumer的@RabbitListener绑定的是queue = "logistics.queue",未声明 exchange 和 binding,导致消息被丢弃。
解决:在消费者端显式声明 exchange、queue 和 binding:
@Configuration public class RabbitMQConfig { @Bean public TopicExchange orderExchange() { return new TopicExchange("order.exchange"); // 与生产者一致 } @Bean public Queue logisticsQueue() { return QueueBuilder.durable("logistics.queue").build(); } @Bean public Binding binding() { return BindingBuilder.bind(logisticsQueue()).to(orderExchange()); } }5. 生产就绪检查清单:6 个必须手动验证的临界参数
能本地跑通只是起点。当你准备把这套架构推到测试环境甚至预发时,以下 6 个参数若未显式检查,90% 的线上故障都源于此。它们不像server.port那样显眼,却像定时炸弹一样埋在配置深处。
5.1 Nacos 配置中心的dataId命名规范:大小写与后缀决定加载成败
dataId是 Nacos 中定位配置的唯一钥匙。order-service的bootstrap.yml中:
spring: cloud: nacos: config: server-addr: localhost:8848 group: DEFAULT_GROUP file-extension: yaml # ⚠️ dataId 由 spring.application.name + . + file-extension 构成 # 若 application.name=order-service,则 dataId=order-service.yaml但在 Nacos 控制台新建配置时,很多人手误写成OrderService.yaml或order_service.yaml,导致服务启动后@Value为空。必须严格遵循spring.application.name的值,且file-extension必须与控制台选择的格式一致(yaml≠yml)。
| 服务名(application.name) | 正确 dataId | 错误 dataId | 后果 |
|---|---|---|---|
order-service | order-service.yaml | OrderService.yaml | 配置不加载 |
user-service | user-service.yaml | user-service.yml | YAML 解析失败 |
gateway | gateway.yaml | gateway-dev.yaml | profile 无法切换 |
验证命令:启动服务后,查看日志中
Loading config data from行,确认加载的 dataId 与 Nacos 控制台完全一致。
5.2 Seata Server 的store.mode:文件模式 vs 数据库模式的性能分水岭
Seata Server 默认store.mode=file,所有全局事务日志写入本地data目录。这在单机开发没问题,但一旦部署多节点 Seata Server(高可用必需),file模式会导致事务状态丢失——因为各节点无法共享文件。
必须改为db模式,并在seata/conf/application.yml中配置:
store: mode: db db: datasource: druid dbType: mysql driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_db?serverTimezone=GMT%2B8&useSSL=false user: root password: 123456同时执行seata/script/server/db/mysql.sql初始化seata_db库。不改此项,线上扩缩容 Seata Server 时,全局事务 100% 中断。
5.3 RabbitMQ 的prefetchCount:消费者吞吐量的隐形天花板
order-service的消费者默认prefetchCount=1(SpringBoot AMQP 默认值),意味着 RabbitMQ 每次只给消费者发 1 条消息,等 ACK 后才发下一条。这在高并发下单场景下,会成为性能瓶颈。
必须在application-dev.yml中显式增大:
spring: rabbitmq: listener: simple: prefetch: 10 # ⚠️ 从 1 改为 10,提升吞吐 acknowledge-mode: manual # 必须手动 ACK,配合 prefetch消费者代码中需显式调用channel.basicAck():
@RabbitListener(queues = "order.queue") public void onOrderCreated(OrderCreatedEvent event, Channel channel, Message message) throws IOException { try { logisticsService.notify(event); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); } }参数说明:
prefetch=10表示 RabbitMQ 最多缓存 10 条未 ACK 消息给该消费者;basicNack(..., true)表示拒收后重新入队,避免消息丢失。
5.4 Vue 的proxy配置:开发与生产环境的跨域本质差异
vue.config.js中的proxy仅在npm run serve时生效,构建生产包(npm run build)后完全失效。生产环境必须由 Nginx 或网关统一处理跨域。
因此,vue-shop/dist/部署到 Nginx 时,必须配置:
location /api/ { proxy_pass http://localhost:8081/; # 指向网关 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }若忘记此配置,生产环境所有请求将 404——因为浏览器直接请求https://your-domain.com/api/login,而 Nginx 未转发给网关。
5.5 MySQL 的max_connections:连接池耗尽的无声杀手
order-service的 HikariCP 默认maximumPoolSize=10,但若spring.datasource.hikari.maximum-pool-size未显式配置,且 MySQL 服务端max_connections=151(默认值),当多个服务(user/product/order/gateway)同时启动,每个占 10 连接,151 很快被占满,新连接直接拒绝。
必须在每个服务的application-dev.yml中显式限制:
spring: datasource: hikari: maximum-pool-size: 5 # ⚠️ 从默认 10 降至 5 minimum-idle: 2 connection-timeout: 30000同时检查 MySQL 服务端:
-- 查看当前连接数 SHOW STATUS LIKE 'Threads_connected'; -- 临时调高(重启失效) SET GLOBAL max_connections = 500; -- 永久生效需修改 my.cnf # [mysqld] # max_connections = 5005.6 Redis 的maxmemory-policy:缓存雪崩的推手
product-service使用 Redis 缓存商品信息,但若redis.conf中maxmemory-policy为noeviction(默认),内存满时新写入直接失败,redisTemplate.delete()抛OOM command not allowed,导致库存扣减流程中断。
必须改为allkeys-lru:
# 进入 Redis CLI redis-cli > CONFIG SET maxmemory-policy allkeys-lru > CONFIG REWRITE # 持久化到 redis.conf并在application-dev.yml中配置:
spring: redis: lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0逻辑说明:
allkeys-lru表示当内存不足时,淘汰最近最少使用的 key;若用volatile-lru,而缓存未设 TTL(如redisTemplate.opsForValue().set("key", "value")),则永远不会淘汰,最终 OOM。
我带过的每个团队,在第一次把这套商城推到测试环境时,都至少栽在这 6 个参数中的 2 个以上。它们不难改,但没人会主动去查——直到凌晨三点告警电话响起。现在你知道了,希望帮到你。
本文还有配套的精品资源,点击获取