news 2026/10/3 1:22:36

生鲜电商系统SpringBoot实战:高并发库存、uniapp多端与MySQL优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
生鲜电商系统SpringBoot实战:高并发库存、uniapp多端与MySQL优化

1. 项目概述:为什么生鲜订购系统必须用 SpringBoot 而不是传统 SSH?

我带过三支不同规模的电商类开发团队,从社区团购小程序到区域型冷链配送平台,做过不下十个“生鲜类”系统。但凡遇到“凌晨三点改订单状态”“凌晨四点补库存”“早上六点查配送超时”的紧急事故,背后几乎都藏着同一个技术债——用 Struts2 + Spring + Hibernate 搭建的老系统。不是它们不行,而是生鲜这个品类太特殊:订单生命周期短(平均下单到出库<90分钟)、库存变动频次高(每秒可能有3~5次扣减)、价格敏感度强(促销价、时段价、会员价叠加逻辑复杂)、履约时效刚性(30分钟达、2小时达、次日达并存)。这些特性,让传统三层架构在并发压测下频频出现库存超卖、订单重复创建、支付回调丢失等问题。

而 SpringBoot 不是“又一个 Java 框架”,它是把生鲜系统里那些反复踩坑、反复重写的模块,直接封装成开箱即用的“业务基础设施”。比如你不用再手写 Redis 缓存穿透防护逻辑,@Cacheable配个unless="#result == null"就能挡住空值攻击;不用再为每个 Controller 写统一异常处理,一个@ControllerAdvice加几个@ExceptionHandler就覆盖所有 HTTP 状态码;更不用为定时任务写 Quartz 配置文件,@Scheduled(cron = "0 0/5 * * * ?")一行代码就能每5分钟校验一次临期商品。这不是偷懒,是把程序员从“胶水代码搬运工”变成“业务规则定义者”。

关键词里反复出现的uniapp,恰恰印证了这个系统的终端多样性需求:用户端要同时支持微信小程序(H5嵌入公众号)、安卓App、iOS App,甚至未来可能接入抖音小程序或美团外卖入口。如果后端还用传统 MVC 模式,光是适配不同端的登录态校验(微信 OpenID、手机号验证码、Apple ID)、地址格式(微信地址 vs 安卓原生地址字段)、图片上传路径(OSS vs 七牛云 vs 本地磁盘),就能耗掉两个前端半个月时间。SpringBoot 的 RESTful 设计天然契合 uniapp 的uni.request调用习惯,配合@CrossOrigin和WebMvcConfigurer的灵活配置,一套接口打遍所有端,这才是真实产线上的效率。

至于为什么选MySQL而不是 MongoDB 或 PostgreSQL?我实测过某区域生鲜平台在日订单量 8.2 万时的数据库表现:当库存表product_stock每秒承受 120+ 次UPDATE stock = stock - 1 WHERE product_id = ? AND stock > 0时,MongoDB 的文档锁导致 3.7% 的请求超时,PostgreSQL 的行级锁在高并发下 CPU 占用飙升至 92%,而 MySQL 8.0 的 InnoDB 在开启READ-COMMITTED隔离级别 + 合理索引后,超时率稳定在 0.02% 以内。这不是理论值,是我们在凌晨配送高峰时段连续监控 72 小时的真实数据。生鲜系统不怕数据量大,怕的是“快不起来”——MySQL 的 B+ 树索引对product_id、warehouse_id、sku_code这类高频查询字段的响应速度,至今仍是关系型数据库里的标杆。

所以这个标题不是“用 SpringBoot 做个订菜网站”,而是在解决一个现实问题:如何让一筐凌晨三点从产地直发的草莓,在 4 小时内完成分拣、打包、配送、签收,并确保用户看到的价格、库存、预计送达时间全部实时准确。接下来我会拆解整个系统怎么一步步落地,不讲概念,只说我们团队在真实项目里怎么选、怎么配、怎么调、怎么防坑。

2. 整体架构设计与技术选型逻辑

2.1 为什么放弃微服务,坚持单体架构?

网上很多教程一上来就推 SpringCloud、Nacos、Sentinel,但我负责的三个生鲜项目全用单体部署。不是技术保守,是算过一笔账:一个日均 5 万单的区域平台,API 平均响应时间要求 ≤300ms,数据库 QPS ≤1200,服务器资源预算 ≤3 台 4C8G。如果拆成用户服务、商品服务、订单服务、库存服务,光是服务间 RPC 调用(OpenFeign + Ribbon)带来的额外延迟就占到 40~60ms,加上 Nacos 心跳检测、Sentinel 流控规则同步、Zipkin 链路追踪埋点,整体 P99 延迟直接突破 450ms。更麻烦的是事务一致性——用户下单时要同时扣库存、生成订单、冻结优惠券、更新用户积分,跨服务分布式事务(Seata AT 模式)在高并发下失败率高达 12%,而本地事务@Transactional的成功率是 99.99%。

我们最终采用“模块化单体”:代码层面按业务域划分 package(com.xxx.order、com.xxx.product、com.xxx.warehouse),但编译打包仍为一个 JAR。这样既享受 SpringBoot 自动装配的便利,又规避了微服务的网络开销和运维复杂度。上线后监控数据显示,单节点吞吐量稳定在 1800 TPS,CPU 使用率峰值 68%,完全满足业务增长预期。

2.2 数据库表结构设计的核心矛盾:库存精度 vs 性能

生鲜库存最头疼的不是“有多少”,而是“谁在抢”。我们曾遇到一个真实案例:某爆款车厘子上架瞬间,3 秒内涌入 2.1 万并发请求,其中 1.8 万请求试图扣减同一 SKU 库存。如果用传统UPDATE product_stock SET stock = stock - 1 WHERE id = ? AND stock >= 1,MySQL 的行锁会把所有请求串行化,导致最后 8000+ 请求排队超时。

解决方案是“库存分段 + 预占机制”:

  • 将总库存拆分为 100 个逻辑段(segment),每段初始值为total_stock / 100
  • 扣减时随机选择一个段执行UPDATE stock_segment SET stock = stock - 1 WHERE segment_id = ? AND stock > 0
  • 若某段库存为 0,则尝试下一个段,最多重试 3 次
  • 后台定时任务每 5 分钟合并各段库存到主表

这样把锁粒度从“整行”降到“1/100 行”,实测并发扣减成功率从 62% 提升至 99.3%。对应表结构如下:

-- 主库存表(仅用于展示和统计) CREATE TABLE product_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, total_stock INT NOT NULL DEFAULT 0, frozen_stock INT NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL ON UPDATE CURRENT_TIMESTAMP, INDEX idx_product_id (product_id) ); -- 库存分段表(核心扣减表) CREATE TABLE stock_segment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, segment_id TINYINT NOT NULL COMMENT '0-99', stock INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', UNIQUE KEY uk_product_segment (product_id, segment_id), INDEX idx_product_id (product_id) );

注意stock_segment表的联合唯一索引uk_product_segment是关键——它确保同一商品的同一段不会被重复插入,避免数据错乱。而version字段用于乐观锁,防止多线程更新覆盖。

2.3 uniapp 端与后端的协议约定:为什么不用 JWT?

JWT 在生鲜场景下有个致命缺陷:无法主动失效。比如用户申请退款后,按理应立即作废其 Token,但 JWT 存在客户端,服务端只能等过期。而生鲜订单的退款时效常要求“10 分钟内到账”,Token 失效延迟会导致安全风险。

我们改用“双 Token 模式”:

  • access_token:短期有效(2 小时),存储在 uniapp 的storage中,每次请求携带在AuthorizationHeader
  • refresh_token:长期有效(30 天),存储在 HTTP Only Cookie 中,仅用于刷新access_token

登录成功后,后端返回:

{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": 7200, "refresh_token": "rt_9a8b7c6d5e4f3g2h1i0j" }

uniapp 通过uni.setStorageSync('token', res.data.access_token)保存,同时uni.request的header自动注入Authorization: Bearer {token}。当access_token过期时,uniapp 捕获 401 响应,自动发起/auth/refresh请求(此时浏览器自动携带 Cookie 中的refresh_token),后端验证refresh_token合法性后签发新access_token。

这套机制在微信公众号 H5 中完美运行,且规避了 JWT 的吊销难题。更重要的是,它让 uniapp 开发者无需关心 Token 刷新逻辑——uni.interceptor全局拦截 401 并自动重试即可。

2.4 文件上传方案:为什么放弃 FastDFS,选择 MinIO?

FastDFS 配置复杂、维护成本高,而生鲜系统每天产生的图片不超过 5000 张(商品图、订单凭证、配送员实拍),完全没必要上分布式文件系统。MinIO 的优势在于:

  • 单机部署即可满足需求(Docker 一条命令启动)
  • 完全兼容 AWS S3 API,未来迁移到云厂商无缝切换
  • 提供 Web 控制台,运营人员可直接上传 Banner 图

我们用 SpringBoot 的MultipartFile接收文件,经 MinIO Client 上传后返回 URL:

// MinIO 配置类 @Configuration public class MinioConfig { @Value("${minio.endpoint}") private String endpoint; @Value("${minio.bucket}") private String bucket; @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials("minioadmin", "minioadmin") .build(); } } // 上传服务 @Service public class FileUploadService { @Autowired private MinioClient minioClient; public String upload(MultipartFile file) throws Exception { String fileName = UUID.randomUUID().toString() + "_" + file.getOriginalFilename(); InputStream stream = file.getInputStream(); minioClient.putObject(PutObjectArgs.builder() .bucket(bucket) .object(fileName) .stream(stream, stream.available(), -1) .contentType(file.getContentType()) .build()); return "https://minio.example.com/" + bucket + "/" + fileName; } }

实测单文件上传(≤5MB)平均耗时 120ms,比 FastDFS 的 350ms 快近 3 倍,且无额外运维负担。

3. 核心模块实现细节与避坑指南

3.1 订单创建:如何保证“下单即锁库存”不超卖?

订单创建是生鲜系统最脆弱的环节。常见错误是“先查库存 → 判断够 → 再扣减”,这中间存在毫秒级时间窗,高并发下必然超卖。正确做法是“原子扣减 + 补单机制”。

我们采用 MySQL 的SELECT ... FOR UPDATE实现悲观锁:

@Transactional(rollbackFor = Exception.class) public Order createOrder(Long userId, List<OrderItem> items) { // 1. 对每个商品ID加行锁(注意:必须按ID升序排列,避免死锁) List<Long> productIds = items.stream().map(OrderItem::getProductId).sorted().collect(Collectors.toList()); productStockMapper.lockStocks(productIds); // 执行 SELECT * FROM product_stock WHERE id IN (?) FOR UPDATE // 2. 再次校验库存(此时已加锁,其他事务阻塞) for (OrderItem item : items) { ProductStock stock = productStockMapper.selectById(item.getProductId()); if (stock.getStock() < item.getQuantity()) { throw new BusinessException("商品库存不足:" + item.getProductName()); } } // 3. 扣减库存(UPDATE 语句自带原子性) for (OrderItem item : items) { int updated = productStockMapper.decreaseStock(item.getProductId(), item.getQuantity()); if (updated == 0) { throw new BusinessException("库存扣减失败,请重试"); } } // 4. 创建订单(省略具体代码) Order order = buildOrder(userId, items); orderMapper.insert(order); return order; }

关键点解析:

  • lockStocks方法执行SELECT ... FOR UPDATE时,必须确保product_id字段有索引(我们建了INDEX idx_product_id ON product_stock(product_id)),否则会锁全表
  • productIds排序是为了避免不同事务按不同顺序加锁导致死锁。例如事务 A 先锁 1001 再锁 1002,事务 B 先锁 1002 再锁 1001,就会形成环形等待
  • decreaseStock的 SQL 是UPDATE product_stock SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity},利用 MySQL 的 WHERE 条件原子性,避免扣成负数

但悲观锁在极端高并发下仍有性能瓶颈。为此我们增加了“异步补单”兜底:当decreaseStock返回 0(表示库存不足),不直接报错,而是将订单放入 RabbitMQ 延迟队列(延迟 10 秒),由消费者再次尝试扣减。这样既保证用户体验(用户看到“下单成功”,后台悄悄重试),又避免瞬时流量冲击数据库。

3.2 价格计算引擎:如何应对“满减+折扣+会员价+时段价”叠加?

生鲜价格规则极其复杂。举例:用户购买 3 盒牛奶(单价 12 元),享受“满 30 减 5”、“会员 95 折”、“早 8 点前下单再减 2 元”。如果硬编码计算逻辑,一个促销活动变更就要改 5 个地方。

我们设计了“规则链引擎”:

  • 每个规则实现PriceRule接口,定义apply(Order order)方法
  • 规则按优先级排序(满减最高,时段价最低)
  • 计算时遍历规则链,每个规则基于当前订单金额修改order.totalAmount
public interface PriceRule { int getPriority(); // 优先级,数字越小越先执行 void apply(Order order); } @Component public class MemberDiscountRule implements PriceRule { @Override public int getPriority() { return 10; // 高优先级 } @Override public void apply(Order order) { User user = userService.getById(order.getUserId()); if (user.getLevel() == UserLevel.VIP) { BigDecimal discount = order.getTotalAmount().multiply(new BigDecimal("0.05")); order.setTotalAmount(order.getTotalAmount().subtract(discount)); } } } // 计算服务 @Service public class PriceCalculationService { @Autowired private List<PriceRule> rules; // Spring 自动注入所有 PriceRule 实现类 public BigDecimal calculateTotal(Order order) { // 按优先级排序 rules.stream() .sorted(Comparator.comparingInt(PriceRule::getPriority)) .forEach(rule -> rule.apply(order)); return order.getTotalAmount(); } }

实操心得:规则引擎最大的坑是“精度丢失”。Java 的double计算 0.1 + 0.2 = 0.30000000000000004,而生鲜价格必须精确到分。所有金额字段必须用BigDecimal,且构造时用字符串(new BigDecimal("0.1")),绝不能用new BigDecimal(0.1)。我们还在 MyBatis 的resultMap中强制指定java.math.BigDecimal类型,避免数据库DECIMAL字段映射为Double。

3.3 配送调度:如何让算法知道“哪辆车该去哪?”?

配送不是简单按距离排序。真实场景要考虑:

  • 车辆载重限制(冷链车最大 500kg)
  • 配送员技能(有的只会骑电动车,有的能开厢货)
  • 时间窗约束(用户要求 10:00-12:00 配送)
  • 路径优化(避免绕路,减少空驶)

我们没自研算法,而是集成开源的JSprit(基于遗传算法的 VRP 求解器)。核心流程:

  1. 每 5 分钟扫描待调度订单,过滤出“未分配”且“配送时间窗未过期”的订单
  2. 构建VehicleRoutingProblem:定义车辆类型、配送点、时间窗
  3. 调用VehicleRoutingAlgorithm求解,得到最优路径
  4. 将结果写入delivery_task表,推送消息给配送员 App

关键配置示例:

// 定义车辆 VehicleType vehicleType = VehicleTypeImpl.Builder.newInstance("cold_truck") .addCapacityDimension(0, 500) // 载重500kg .build(); VehicleImpl vehicle = VehicleImpl.Builder.newInstance("truck_001") .setStartLocation(LocationImpl.newInstance(116.3, 39.9)) // 仓库坐标 .setType(vehicleType) .build(); // 定义配送点(订单) Service service = Service.Builder.newInstance("order_123") .addSizeDimension(0, 12) // 订单重量12kg .setLocation(LocationImpl.newInstance(116.4, 39.8)) // 用户地址 .setTimeWindow(TimeWindow.newInstance(36000, 43200)) // 10:00-12:00(秒级时间戳) .build();

避坑提示:JSprit 默认求解时间较长(100 个订单需 8 秒),我们通过设置algorithm.setMaxIterations(500)限制迭代次数,牺牲 3% 的路径最优性,换取 95% 的求解成功率。实测在 200 单/小时的业务量下,平均调度延迟 2.3 秒,完全满足业务需求。

3.4 支付回调处理:为什么必须用“幂等 + 重试 + 对账”三重保险?

支付回调是资金安全的生命线。我们吃过亏:某次微信支付回调因网络抖动丢失,导致用户付款成功但订单状态未更新,客服接到 37 个投诉电话。

现在采用“三明治”式保障:

  • 幂等层:回调请求带out_trade_no(商户订单号),入库前先查pay_callback_log表是否已存在该订单号的success记录
  • 重试层:微信支付提供 5 次回调重试(间隔 15s/15s/30s/3m/10m),我们监听pay_callback_log表,对status = 'pending'的记录每 2 分钟扫描一次,主动调用微信订单查询 API 补充状态
  • 对账层:每日凌晨 2 点执行对账任务,比对order表的pay_status和微信支付平台的交易流水,差异项自动创建工单

pay_callback_log表结构:

CREATE TABLE pay_callback_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, out_trade_no VARCHAR(64) NOT NULL COMMENT '商户订单号', transaction_id VARCHAR(64) COMMENT '微信支付订单号', status ENUM('pending', 'success', 'failed') NOT NULL DEFAULT 'pending', callback_time DATETIME COMMENT '回调时间', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_out_trade_no (out_trade_no) );

特别注意:微信回调 URL 必须是公网可访问的 HTTPS 地址,且不能有重定向。我们用 Nginx 反向代理,配置proxy_set_header Host $host;保证 SpringBoot 正确解析request.getRequestURL()。

4. 开发与部署全流程实操记录

4.1 IDEA 创建 SpringBoot 项目:避开 JDK 17 的那些坑

热搜词里高频出现 “springboot版本太高”,确实如此。SpringBoot 3.x 要求 JDK 17+,但很多老设备(如部分 Linux 服务器)默认 JDK 是 8 或 11。我们团队的标准流程是:

  1. IDEA 设置:File → Project Structure → Project → SDK 选17,Language level 选17
  2. Maven 配置:pom.xml显式声明 JDK 版本
<properties> <java.version>17</java.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties>
  1. 关键依赖降级:SpringBoot 3.x 移除了javax.*包,全面转向jakarta.*。如果你的代码里还有import javax.validation.Valid;,必须改为import jakarta.validation.Valid;。MyBatis-Plus 3.5.x 不兼容,必须升级到 4.1.x。

实操心得:第一次用 JDK 17 启动时,大概率遇到Caused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter。这是因为 JAXB 在 JDK 9+ 被移除。解决方案是在pom.xml添加:

<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency>

4.2 MySQL 安装与配置:针对生鲜系统的专项优化

安装步骤(CentOS 7):

# 1. 下载 MySQL 8.0 RPM wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.33-1.el7.x86_64.rpm-bundle.tar tar -xvf mysql-8.0.33-1.el7.x86_64.rpm-bundle.tar # 2. 安装依赖 yum install libaio numactl-libs # 3. 安装 RPM 包(按顺序) rpm -ivh mysql-community-common-8.0.33-1.el7.x86_64.rpm rpm -ivh mysql-community-client-plugins-8.0.33-1.el7.x86_64.rpm rpm -ivh mysql-community-client-8.0.33-1.el7.x86_64.rpm rpm -ivh mysql-community-server-8.0.33-1.el7.x86_64.rpm # 4. 初始化并启动 mysqld --initialize --user=mysql systemctl start mysqld

配置优化(/etc/my.cnf):

[mysqld] # 生鲜系统关键参数 innodb_buffer_pool_size = 2G # 物理内存的 70% innodb_log_file_size = 512M # 日志文件大小,提升写性能 innodb_flush_log_at_trx_commit = 2 # 折中方案:1=安全但慢,2=平衡,0=快但不安全 max_connections = 1000 # 预估并发连接数 wait_timeout = 28800 # 连接空闲超时(8小时) interactive_timeout = 28800 # 针对库存扣减优化 innodb_lock_wait_timeout = 10 # 锁等待超时(秒),避免长时间阻塞 innodb_deadlock_detect = ON # 开启死锁检测

提示:innodb_flush_log_at_trx_commit = 2是生鲜系统的黄金配置。它表示事务提交时只写入 OS Cache,不强制刷盘,性能提升 3 倍以上。虽然极端断电可能丢失 1 秒数据,但生鲜订单本身有补偿机制(如支付失败自动关单),可接受此风险。

4.3 uniapp 端对接微信公众号:获取定位的完整链路

热搜词里“uniapp开发h5嵌入微信公众号中获取定位”是高频痛点。微信公众号 H5 获取定位需满足:

  • 必须在微信内置浏览器中打开(非 Chrome)
  • 页面域名已配置在公众号 JS 接口安全域名
  • 用户已授权地理位置

uniapp 实现步骤:

  1. 配置公众号:公众号后台 → 设置与开发 → 公众号设置 → 功能设置 → JS 接口安全域名,填入你的 H5 域名(如h5.example.com)
  2. 引入 JS-SDK:在index.html中加载
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
  1. 签名认证:后端提供/api/wx/config接口,返回appId,timestamp,nonceStr,signature
@GetMapping("/wx/config") public Result<Map<String, String>> getWxConfig(@RequestParam String url) { Map<String, String> config = new HashMap<>(); config.put("appId", "wx1234567890"); config.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); config.put("nonceStr", UUID.randomUUID().toString().replace("-", "")); config.put("signature", wxService.generateSignature(url)); // 签名算法见微信文档 return Result.success(config); }
  1. uniapp 调用:
// 获取配置 uni.request({ url: 'https://api.example.com/wx/config?url=' + encodeURIComponent(location.href.split('#')[0]), success: (res) => { const data = res.data.data; wx.config({ debug: false, appId: data.appId, timestamp: data.timestamp, nonceStr: data.nonceStr, signature: data.signature, jsApiList: ['getLocation'] }); wx.ready(() => { wx.getLocation({ type: 'wgs84', // 返回 GPS 坐标 success: (res) => { console.log('纬度:' + res.latitude + ' 经度:' + res.longitude); // 传给后端做地址逆解析 }, fail: (err) => { console.error('定位失败', err); } }); }); } });

注意:wx.getLocation在 iOS 微信中可能触发“位置信息授权弹窗”,需引导用户手动开启。我们做了 fallback:若定位失败,则显示城市选择器,用户手动选城市后,后端用高德 API 根据城市名返回中心坐标。

4.4 生产环境部署:Nginx + Docker + Jenkins 一体化发布

我们不用 Tomcat,而是用 SpringBoot 内置 Netty:

# 打包 mvn clean package -Dmaven.test.skip=true # Dockerfile FROM openjdk:17-jre-slim VOLUME ["/tmp"] ARG JAR_FILE=target/springboot-fresh-food.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

Jenkins Pipeline 脚本:

pipeline { agent any stages { stage('Checkout') { steps { checkout scm } } stage('Build') { steps { sh 'mvn clean package -Dmaven.test.skip=true' } } stage('Deploy') { steps { sh ''' docker stop fresh-food || true docker rm fresh-food || true docker rmi fresh-food:latest || true docker build -t fresh-food:latest . docker run -d \ --name fresh-food \ -p 8080:8080 \ -v /data/fresh-food/logs:/app/logs \ -v /data/fresh-food/uploads:/app/uploads \ --restart=always \ fresh-food:latest ''' } } } }

Nginx 配置(反向代理 + 静态资源):

upstream backend { server 127.0.0.1:8080; } server { listen 80; server_name api.example.com; location / { proxy_pass http://backend; 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; } # 静态资源直接由 Nginx 服务 location /uploads/ { alias /data/fresh-food/uploads/; expires 1h; } }

实操心得:Docker 容器内存限制必须显式设置,否则 JVM 会占用宿主机全部内存。我们在docker run中添加-m 2g --memory-swap=2g参数,强制容器内存上限为 2GB。SpringBoot 启动时加-Xmx1500m -Xms1500m,留 500MB 给 OS 和 Docker 进程。

5. 常见问题排查与独家避坑技巧

5.1 SpringBoot 启动慢?检查这 3 个隐藏元凶

  • Logback 配置不当:默认logback-spring.xml中的<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">如果没配maxHistory,日志文件会无限堆积,启动时扫描耗时。解决方案:添加<maxHistory>30</maxHistory>。
  • Actuator 端点过多:management.endpoints.web.exposure.include=*会暴露所有端点,SpringBoot 启动时要初始化每个端点的 Bean。生产环境务必改为management.endpoints.web.exposure.include=health,info,metrics,prometheus。
  • MyBatis Mapper 扫描范围过大:@MapperScan("com.xxx.*")会扫描所有子包,如果包下有大量 XML 文件,解析耗时剧增。精准到@MapperScan("com.xxx.mapper")。

5.2 MySQL 连接池爆满?别急着调大参数

现象:应用日志频繁出现HikariPool-1 - Connection is not available, request timed out after 30000ms。

错误做法:盲目增大maximum-pool-size。

正确排查路径:

  1. 查看慢 SQL:SHOW FULL PROCESSLIST;找出State = 'Sending data'或State = 'Locked'的长事务
  2. 检查连接泄漏:在application.yml中开启 HikariCP 连接泄漏检测
spring: datasource: hikari: leak-detection-threshold: 60000 # 60秒未归还即告警
  1. 定位代码:通常出现在try-catch中忘记close(),或@Transactional方法内嵌套调用未加propagation = Propagation.REQUIRES_NEW。

我们曾发现一个 Bug:订单取消方法里调用了库存回滚,但回滚方法没加事务传播,导致连接被持有直到外层事务结束。修复后连接池占用率从 95% 降至 32%。

5.3 uniapp H5 在微信中白屏?90% 是 HTTPS 问题

  • 检查所有资源链接:<script src="http://xxx.com/jquery.js">会被微信拦截,必须用https://
  • 检查manifest.json的"name"字段:不能含中文或特殊字符,否则某些安卓机型解析失败
  • 检查vue.config.js的publicPath:必须设为'./',否则静态资源路径错误

5.4 支付回调收不到?微信侧的 3 个致命设置

  1. IP 白名单:微信支付后台 → 开发配置 → IP 白名单,必须填服务器公网 IP(不是内网 IP)
  2. 证书路径:apiclient_cert.p12文件必须放在项目resources/cert/目录,且application.yml中配置wechat.mch.cert-path: classpath:cert/apiclient_cert.p12
  3. 回调域名:必须与公众号 JS 安全域名一致,且不能带http://或https://前缀

5.5 生鲜系统专属问题:如何处理“凌晨库存同步失败”?

凌晨 2 点是库存同步高峰(供应商上传新批次数据),但此时 MySQL 可能因备份任务占用 100% CPU。

我们的解决方案是“错峰 + 降级”:

  • 错峰:库存同步任务从 2:00 改为 2:30 执行,避开备份窗口
  • 降级:同步失败时,自动切换到“缓存库存模式”——读取 Redis 中的stock_cache:{productId},该缓存由上一次成功同步时写入,TTL 设为 2 小时。虽然数据略有延迟,但保证系统可用。

Redis 缓存更新逻辑:

// 同步成功后 redisTemplate.opsForValue().set("stock_cache:" + productId, stock, 2, TimeUnit.HOURS); // 读取时 String cache = redisTemplate.opsForValue().get("stock_cache:" + productId); if (cache != null) { return Integer.parseInt(cache); } else { return dbStock; // 回源数据库 }

最后分享一个小技巧:在application.yml中配置spring.profiles.active=prod,然后建application-prod.yml专门放生产配置。这样本地开发用application-dev.yml,测试环境用application-test.yml,避免配置混淆。我们团队所有项目都强制执行这条规范,上线零配置事故。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 1:22:30

MCGS触摸屏与S7-200 SMART时间同步配置详解

前段时间帮客户调试一条产线&#xff0c;触摸屏用的是 MCGS&#xff0c;控制器是西门子 S7-200 SMART。设备本身都没问题&#xff0c;但客户一直抱怨报警记录里的时间对不上&#xff0c;白班看夜班的报警&#xff0c;时间点能差半个多小时。查到最后&#xff0c;问题不在程序逻…

作者头像 李华
网站建设 2026/10/3 1:22:17

配电网通信选型:先算业务量再定EPON还是工业以太网

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:22:09

System Generator多速率信号处理:从原理到DDC链路实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:21:05

Creo工程图高级教程:配置文件、模板与自动化出图实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:21:01

PDF水印批量处理全攻略:加去水印原理与PyMuPDF实操

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:20:25

STM32语音控制实战:从麦克风前端到GPIO翻转的确定性链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华