SpringBoot实战:果蔬电商库存扣减、订单超时与安全认证设计
简介供毕业设计或课程设计使用的果蔬电商平台完整工程包基于SpringBoot与Vue技术栈面向具备Java和前端基础的开发者可快速获得一套前后端分离的电商系统参考实现。压缩包一共包含751个文件大小约16.6MB核心内容包括107个Java后端源码文件、124个前端JavaScript脚本与Vue组件、26个HTML页面、42个CSS样式表并配齐159张PNG图片和120张GIF动态图等界面素材同时提供MySQL数据库脚本、项目说明文档与开发环境配置文件。资源内部目录划分清晰按照后端服务、前端页面、数据库设计、项目文档和素材资源分模块存放便于按需检索与学习。目前已有29人学习下载。借助这份资源可以完整还原从数据库表设计到页面交互的功能链路理解SpringBoot接口开发、Vue组件化开发、模板页面渲染等关键环节同时也能作为课程报告、答辩演示或二次开发的基础工程尤其适合希望系统掌握全栈项目架构的读者。1. 果蔬电商的SpringBoot骨架从交付路径说起用户视角里买菜是一连串很自然的动作早上打开手机挑几样时令果蔬下单付款傍晚就收到货。但站在后端视角这个流程背后是商品、库存、价格、订单、支付、配送六个环节的实时协同任何一个环节稍微错一点表现出来就是“下单成功了却没货”或者“货到了价格不对”。果蔬平台相比普通电商有几个独特痛点规格不统一一斤装、两斤装、礼盒装、时令性强、保鲜期短、同城配送时效要求高。这些问题都会倒逼数据模型和交易链路做专门设计而不是把标准电商脚手架拿来直接用。SpringBoot恰好是这套系统的合适底座它的自动装配能力让基础设施的接入成本变得极低配合Redis做缓存、RabbitMQ做异步任务、Spring Security做认证一个从商品展示到订单完成的交易闭环可以在很短的时间里跑起来。这篇博文不打算介绍果蔬电商平台的功能清单而是按我真正开发时会走的路径来组织先设计果蔬特有的表结构和库存模型再落地SpringBoot的配置与启动流程接着处理并发下单和订单超时这类核心业务然后补上身份认证与接口安全最后用压测和运行时工具给项目做个体检。每段都有可直接复制的代码、命令或参数表包括我踩过的坑。2. 建表与实体SpringBoot项目里果蔬数据模型的落地2.1 果蔬商品的核心字段与普通SKU的差异普通电商的商品体系用SPU和SKU就能覆盖但果蔬身上有太多“非标”信息产地、时令、保鲜期、单位换算、成熟度。比如同一款西红柿山东产的和云南产的口感、价格、运输损耗都不一样同一个SKU早市和晚市的挂牌价也可能不同。我的做法是在product表里把固定属性提成字段把动态属性放进specsJSON列避免后续加规格时反复改表CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT 商品名, category_id int(11) NOT NULL COMMENT 品类id1叶菜 2根茎 3水果 4肉禽, origin varchar(32) DEFAULT NULL COMMENT 产地, unit varchar(8) DEFAULT 份 COMMENT 销售单位份/斤/盒, price decimal(10,2) NOT NULL COMMENT 当前售价单位元, original_price decimal(10,2) DEFAULT NULL COMMENT 挂牌原价用于展示划线价, shelf_life int(11) DEFAULT 2 COMMENT 保鲜期单位天, season varchar(8) DEFAULT NULL COMMENT 时令春/夏/秋/冬/全年, specs json DEFAULT NULL COMMENT 规格属性如重量区间、包装方式, status tinyint(4) DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT果蔬商品表;这里有三个设计上的取舍。价格用decimal(10,2)而不用float是避免浮点数造成的金额误差。shelf_life单独拎出来成为字段而不是塞进JSON是因为后面要根据它算临期折扣、自动下架和购物车提醒这些场景要频繁使用拿JSON里的内容做查询条件是很痛苦的。season字段看起来冗余但对于果蔬电商来说运营后台非常依赖“当前时令有哪些货”这个筛选维度把它作为索引字段比在JSON里模糊匹配高效得多。对应的SpringBoot实体用MyBatis-Plus实现Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private String name; private Integer categoryId; private String origin; private String unit; private BigDecimal price; private BigDecimal originalPrice; private Integer shelfLife; private String season; private String specs; private Integer status; }Data由Lombok生成getter/setterTableName指定表名TableId(type IdType.AUTO)映射数据库自增主键。BigDecimal用于金额字段与数据库的decimal列严格对应。specs在Java侧可以定义成String让MyBatis-Plus直接映射JSON列如果想让序列化更规整也可以声明为ListProductSpec配合JacksonTypeHandler使用但需要额外配置TableField(typeHandler JacksonTypeHandler.class) private ListProductSpec specs;注意一旦使用了typeHandler在TableName注解里也必须声明autoResultMap true否则查询结果里shelves属性会是null。2.2 库存设计把“已锁定”和“可售”分开果蔬电商的库存与普通商品最大的差异在于损耗。运输过程中压坏一批或当天卖不掉需要报废这些都会让库存变化。如果只维护一个总库存字段很难处理“用户已下单但还没付款”这段时间的库存占用。我通常把库存表设计成如下结构CREATE TABLE inventory ( id bigint(20) NOT NULL AUTO_INCREMENT, product_id bigint(20) NOT NULL, total_stock int(11) NOT NULL DEFAULT 0 COMMENT 累计入库总量, locked_stock int(11) NOT NULL DEFAULT 0 COMMENT 已锁定数量下单未支付, available_stock int(11) NOT NULL DEFAULT 0 COMMENT 可售库存, wasted_stock int(11) NOT NULL DEFAULT 0 COMMENT 损耗报废数量, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, PRIMARY KEY (id), UNIQUE KEY uk_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT果蔬库存表;三者之间的关系是total_stock - locked_stock - wasted_stock available_stock。每次用户下单时locked_stock增加available_stock减少支付完成时locked_stock减少但total_stock不变订单超时取消时则反向操作把库存释放回去。损耗单独用wasted_stock记录后续可以统计损耗率对果蔬这类品类这是很关键的经营指标。更新库存时使用带条件的UPDATE语句防止超卖UPDATE inventory SET available_stock available_stock - #{count}, locked_stock locked_stock #{count}, version version 1 WHERE product_id #{productId} AND available_stock #{count}这条SQL的含义是仅当当前available_stock不小于购买数量时才执行扣减。如果影响行数为0说明库存不足需要在业务层抛出异常。version字段用于乐观锁的额外保护在高并发重复提交时也可以直接参与where条件。2.3 订单状态用整数枚举收口状态流转订单表需要覆盖从创建到完成的完整生命周期。果蔬订单还多了一个“配送中”的状态因为同城配送有明确的时效性要求CREATE TABLE orders ( id bigint(20) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 下单用户, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2备货中 3配送中 4已完成 5已取消 6售后中, remark varchar(255) DEFAULT NULL COMMENT 用户备注, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;订单号我不用数据库自增而是程序里用时间戳加用户ID加随机数生成或者直接引入雪花算法。因为订单号会出现在支付回执、物流单和客服工单里自增ID会暴露销量信息而且分布式环境下自增ID也容易冲突。上表中的status用tinyint应用层统一在枚举类里定义Getter public enum OrderStatusEnum { UNPAID(0, 待支付), PAID(1, 已支付), PREPARING(2, 备货中), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELLED(5, 已取消), AFTER_SALE(6, 售后中); private final Integer code; private final String desc; OrderStatusEnum(Integer code, String desc) { this.code code; this.desc desc; } }这样设计的好处是数据库层面只存一个整数不会因为改状态名而迁移数据Java侧通过枚举做状态校验和流转控制也方便在代码里搜索某个状态的所有使用点。3. SpringBoot配置落地从初始化到运行时的常见坑3.1 自动装配原理为什么SpringBoot能省掉一堆基础设施代码SpringBoot最核心的能力是自动装配。启动类上的SpringBootApplication组合注解里藏着EnableAutoConfiguration它会扫描依赖jar包中META-INF/spring.factories文件里列出的AutoConfiguration类按条件注解如ConditionalOnClass、ConditionalOnMissingBean决定是否实例化对应的Bean。举个实际例子当项目依赖里引入了spring-boot-starter-data-redisRedisAutoConfiguration会被加载如果当前上下文里没有自定义的RedisTemplateBeanSpringBoot会自动创建一个。这就是为什么我们在没写一行配置的情况下就能Autowired注入RedisTemplate。这个机制带来的问题是出问题时排查链路变长。你看到某个Bean莫名其妙存在但又不知道它是从哪个自动配置类里出来的。调试时可以给启动参数加一个调试开关java -jar fruit-shop.jar --debug启动日志会输出所有自动装配的判定结果列出CONDITION EVALUATION DEPLOYMENT段里面清楚标注了每个自动配置类是匹配成功还是失败以及原因。这是排查“为什么RedisAutoConfiguration没生效”这类问题最直接的手段。3.2 从零创建SpringBoot项目时遇到的Initializr网络问题使用IDEA创建SpringBoot项目时经常卡在Initialization failed for https://start.spring.io创建进度条一直转最后报连接超时。这未必是本地网络故障而是默认的Spring Initializr服务响应慢尤其在公司网络环境下更明显。常见做法是把创建向导里的Server URL换成国内镜像地址https://start.aliyun.com。在IDEA的Settings - Build Tools - Build Tool Spring Boot - Spring Initializr里修改URL或者在创建项目弹窗右上角直接选择Custom并填入镜像地址。修改后重新创建下载SpringBoot脚手架的耗时通常能降到几秒内。如果需要确认镜像是否可连通可以在终端执行curl -I https://start.aliyun.com -m 10返回HTTP/1.1 200说明网络链路通畅。如果返回超时需要检查本地是否有代理拦截了https请求或者防火墙限制了外网连接。3.3 SpringBoot版本选择升高还是降低搜索词里“springboot版本太高”和“回退到1.8”是高频问题。SpringBoot 3.x要求JDK17或更高如果你的生产环境还停留在JDK8必须要回退到2.7.x。目前SpringBoot 2.7系列的最后一个小版本是2.7.13它支持JDK8同时也能兼容大部分常用starter。回退方式直接改pom.xmlparent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.13/version relativePath/ /parent同时把maven的编译版本也对齐properties java.version1.8/java.version /properties注意改成2.7.x之后如果有以下依赖需要额外确认兼容性springdoc-openapi的版本需要降到1.x系列2.x只支持SpringBoot 3mybatis-plus-spring-boot3-starter要换回mybatis-plus-boot-starter。另外SpringBoot 2.7默认用的是javax.servlet包SpringBoot 3换成了jakarta.servlet代码里凡是import javax.servlet.*的都要同步调整。3.4 Profile多环境配置与敏感信息脱敏果蔬电商项目至少分dev、test、prod三套环境。配置文件的组织方式是spring: config: activate: on-profile: dev datasource: url: jdbc:mysql://localhost:3306/fruit_dev username: root password: root123 data: redis: host: localhost port: 6379spring: config: activate: on-profile: prod datasource: url: jdbc:mysql://prod-mysql.internal:3306/fruit_prod username: fruit_app password: ${DB_PASSWORD}生产环境的密码不要直接写在yaml文件里再提交到Git可以用环境变量占位符注入。更稳妥的做法是把敏感配置用Jasypt加密配置项里存的是一串密文运行时通过密钥解密jasypt: encryptor: password: ${JASYPT_PASSWORD} algorithm: PBEWithMD5AndDES spring: datasource: password: ENC(9A7f3Kj2mZx8Qw1)注意JASYPT_PASSWORD这个密钥本身还是要靠环境变量或启动参数传入它的作用是把明文密码升级成密文存储避免误提交导致的数据库口令泄露。启动时用java -jar fruit-shop.jar --jasypt.encryptor.passwordxxx传入或者在部署平台的密钥管理里配置。4. 并发下单与库存扣减SpringBoot对接Redis和MQ4.1 高并发入口用Redis做商品详情页热点缓存果蔬电商的访问特点是集中在早市和晚市两个时段瞬时流量可能是平时的十倍。如果所有请求都直接打到MySQL上数据库压力很容易打满。我一般会在商品详情这个热点路径上叠一层Redis缓存。常见做法是在SpringBoot启动时把上架状态的商品预加载到RedisComponent RequiredArgsConstructor public class ProductCacheInitializer implements ApplicationRunner { private final StringRedisTemplate stringRedisTemplate; private final ProductMapper productMapper; Override public void run(ApplicationArguments args) { ListProduct products productMapper.selectList( new LambdaQueryWrapperProduct().eq(Product::getStatus, 1)); for (Product product : products) { stringRedisTemplate.opsForValue().set( product:detail: product.getId(), JSONObject.toJSONString(product), 10, TimeUnit.MINUTES); } } }ApplicationRunner是SpringBoot提供的一个启动回调接口run方法在所有Bean初始化完成后执行。StringRedisTemplate是SpringBoot自动配置好的Redis客户端opsForValue()用于操作字符串类型的数据。缓存有效期设置10分钟这样每次库存变动或者价格调整后最多10分钟就会刷新一次运营上可以接受。这里有一个需要注意的细节productMapper.selectList返回的对象直接序列化存入Redis时如果该实体中有LocalDateTime类型的字段默认序列化会报错或者生成不规范的字符串。解决方案是在Product类的createTime字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解或者统一在配置里注册JavaTimeModuleBean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }4.2 防止超卖Redis预扣减 数据库乐观锁双保险用户点击“立即购买”时根据商品ID先到Redis里做一次预扣减。Redis的DECR操作是原子的可以用返回值判断是否还剩下库存public boolean tryDeductStock(Long productId, Integer count) { String key product:stock: productId; Long remain stringRedisTemplate.opsForValue().decrement(key, count); if (remain null || remain 0) { // 扣超了把多扣的加回去 stringRedisTemplate.opsForValue().increment(key, count); return false; } return true; }decrement方法对应Redis的DECRBY命令原子性由Redis单线程模型保证。返回值是扣减后的剩余量如果小于0说明库存不够需要把扣掉的量回补回去。但Redis扣减成功并不代表数据库一定能扣减成功。下单后创建订单记录时还要再执行一次数据库层的条件更新作为最终校验Transactional public Long createOrder(Long userId, Long productId, Integer count) { int updated inventoryMapper.deductStock(productId, count); if (updated 0) { throw new BusinessException(库存不足); } // 创建订单记录 }inventoryMapper.deductStock对应的SQL就是第2章的带条件UPDATE。两个机制叠加后Redis层挡住了99%的无效流量数据库层处理真正的并发写请求二者各司其职。4.3 订单超时未支付RabbitMQ延迟队列自动关单用户下单后迟迟不付款库存一直被锁着。常见做法有两种定时任务扫表关单或者用RabbitMQ的延迟队列。扫表方案的缺点是轮询间隔不好设定间隔短了浪费数据库资源间隔长了用户等待时间太久。我倾向于用延迟队列。RabbitMQ的延迟队列实现原理是“死信交换机过期队列”组合。先声明一个10分钟过期的队列消息过期后自动转入死信交换机死信交换机再把消息路由到真正的关单队列Configuration public class RabbitMQConfig { public static final String ORDER_DELAY_QUEUE order.delay.queue; public static final String ORDER_DEAD_QUEUE order.dead.queue; public static final String ORDER_DELAY_EXCHANGE order.delay.exchange; public static final String ORDER_DEAD_EXCHANGE order.dead.exchange; Bean public Queue delayQueue() { return QueueBuilder.durable(ORDER_DELAY_QUEUE) .withArgument(x-dead-letter-exchange, ORDER_DEAD_EXCHANGE) .withArgument(x-dead-letter-routing-key, ORDER_DEAD_QUEUE) .withArgument(x-message-ttl, 10 * 60 * 1000) .build(); } Bean public Queue deadQueue() { return QueueBuilder.durable(ORDER_DEAD_QUEUE).build(); } Bean public DirectExchange delayExchange() { return new DirectExchange(ORDER_DELAY_EXCHANGE); } Bean public DirectExchange deadExchange() { return new DirectExchange(ORDER_DEAD_EXCHANGE); } Bean public Binding delayBinding() { return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(ORDER_DELAY_QUEUE); } Bean public Binding deadBinding() { return BindingBuilder.bind(deadQueue()).to(deadExchange()).with(ORDER_DEAD_QUEUE); } }生产者在下单接口里发送延迟消息rabbitTemplate.convertAndSend( RabbitMQConfig.ORDER_DELAY_EXCHANGE, RabbitMQConfig.ORDER_DELAY_QUEUE, orderId.toString());消费者在关单队列里监听并处理超时订单RabbitListener(queues RabbitMQConfig.ORDER_DEAD_QUEUE) public void cancelTimeoutOrder(String orderIdStr) { Long orderId Long.valueOf(orderIdStr); Order order orderMapper.selectById(orderId); if (order.getStatus() OrderStatusEnum.UNPAID.getCode()) { // 关闭订单释放库存 orderMapper.updateStatus(orderId, OrderStatusEnum.CANCELLED.getCode()); inventoryMapper.releaseStock(order.getProductId(), order.getCount()); } }消费者里必须做订单状态判断因为用户可能在消息积压期间完成了支付如果再执行关单操作就会误伤正常订单。cancelTimeoutOrder方法在关单前检查状态是否为待支付是才执行关闭和库存释放保证幂等。延迟队列的坑在于TTL是队列级别设置的同一队列里的消息过期时间相同。如果你的业务需要不同订单类型对应不同超时时间比如普通订单10分钟、预售订单24小时就要为每个超时时间单独建一条队列而不是复用同一条。5. 身份与安全SpringBoot Security JWT的双端会话方案5.1 为什么果蔬电商项目不用Session用户端是手机小程序或H5管理后台是浏览器两者都需要维持登录状态。传统Session方案在集群部署时需要引入Redis做Session共享否则用户请求落到不同实例就会掉线。Session ID天然带有服务端状态想要实现移动端和Web端共用一套登录态也比较别扭。JWT方案则把状态放在客户端服务端不保存会话。用户登录成功后服务端签发一个包含用户ID、角色、过期时间的JWT令牌客户端在后续请求的Authorization头里携带这个令牌服务端验签后解析即可。这样服务端任何实例都能处理请求天然支持水平扩展。5.2 集成Spring Security时的核心配置引入Spring Security之后所有接口默认都是需要认证的这对果蔬电商项目来说需要精细配置放行规则。首页、商品列表、商品详情这些不需要登录但是下单、购物车、订单查询、个人中心必须登录。配置类Configuration EnableWebSecurity RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(/api/auth/**, /api/product/**, /api/home/**).permitAll() .antMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }sessionCreationPolicy(SessionCreationPolicy.STATELESS)表示不创建HttpSession所有认证信息都从JWT里取。permitAll()放行公开接口。hasRole(ADMIN)限定只有管理员角色能访问后台接口。addFilterBefore把自定义JWT过滤器插入到用户名密码认证过滤器之前这样请求进入业务接口之前就已经完成身份校验。JWT过滤器核心逻辑Component RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null jwtTokenProvider.validateToken(token)) { String username jwtTokenProvider.getUsername(token); ListGrantedAuthority authorities jwtTokenProvider.getAuthorities(token); UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(authenticationToken); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }resolveToken从请求头里提取Bearer前缀后面的Token内容。validateToken负责验签和过期检查。SecurityContextHolder.getContext().setAuthentication(...)把用户身份注入Spring Security上下文后续业务代码里通过AuthenticationPrincipal或SecurityContextHolder就能获取当前用户信息。JWT的坑在于过期时间不能设得太短也不能太长。果蔬平台的用户更倾向于“打开即买”频繁要求重新登录会流失订单我一般把用户端Token的过期时间设置成7天管理后台设置成30分钟。但Token一旦签发就无法主动失效所以要求严格的项目会再引入Redis黑名单机制把退出登录的Token存到Redis里每次请求时先检查是否在黑名单中。5.3 防止下单接口被刷幂等Token方案果蔬平台的早市时段流量集中用户可能因为网络延迟重复点击下单按钮导致同一个订单创建多次。这个问题的标准解法是下单接口增加一个幂等Token。流程是客户端在进入结算页时向后端请求一个一次性Token后端将其存入Redis并设置5分钟过期点击“提交订单”时携带这个Token后端先尝试删除Redis里的Token只有删除成功才继续处理下单流程。删除操作的原子性保证同一个Token只能被使用一次public boolean applyIdempotentToken(String token) { Boolean deleted stringRedisTemplate.delete(idempotent: token); return Boolean.TRUE.equals(deleted); }stringRedisTemplate.delete返回Boolean如果这个Token已经不存在即已被使用过返回False业务直接抛出“请勿重复提交”。因为Redis是单线程执行命令删除和判断之间不会有并发问题。这个方案同时还能防止攻击者对下单接口发起重复请求。6. 压测与排查SpringBoot果蔬项目的运行时体检6.1 用Arthas定位接口耗时在哪服务上线后遇到“接口偶尔变慢”的问题常规日志看不出端倪。Arthas是Java诊断工具可以从外部查看运行中的方法耗时。启动项目后在终端执行curl -O https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar选择对应进程后使用trace命令追踪下单方法的内部调用链trace com.fruitshop.service.OrderService createOrder #cost 200#cost 200是过滤条件只显示耗时超过200毫秒的调用路径。输出会列出createOrder方法内部每一步的耗时分布到底是Redis慢了、数据库慢了还是外部接口慢了一眼就能定位。如果Java进程占用的内存持续走高用dashboard命令查看堆内存使用率dashboard输出里heap used如果接近最大值说明需要调大堆内存或者排查是否有对象没有释放。此时再执行heapdump生成堆转储文件heapdump /tmp/fruit-app.hprof生成的文件接着用Eclipse MAT或VisualVM分析重点看大对象和类实例数量排行多半能找到问题所在。注意Arthas的heapdump命令会触发一次Full GC生产环境执行需要评估对业务的影响低峰期再做。6.2 Heapdump敏感信息泄露的防护Arthas的heapdump功能本身是双刃剑排查内存问题很好用但如果dump文件落入攻击者手里堆转储里包含的用户密码、Token等敏感信息就会暴露。Spring Boot Actuator如果开放了heapdump端点也会存在同样的风险。防护策略分两层。首先生产环境关闭Actuator的敏感端点management: endpoints: web: exposure: include: health,info只暴露health和infoheapdump端点在生成环境默认就是关闭的。其次对包含敏感数据的字段在序列化时脱敏比如用户密码在存库时已经用BCrypt加密JWT的密钥放在配置中心和环境变量里不落入代码仓库。做了这两层处理后即使dump文件泄露攻击者拿到的也只是密文破解成本会高很多。6.3 JVM与Tomcat参数推荐果蔬电商项目的流量特征是尖峰明显JVM参数和Tomcat线程池参数需要配合调整。我的推荐配置如下server: tomcat: threads: max: 400 min-spare: 50 accept-count: 1000 max-connections: 10000max-threads控制最大工作线程数配置太大会导致线程过多、上下文切换频繁太小则容易在流量尖峰时拒绝请求。accept-count是等待队列长度排队中的请求不会立即被拒。max-connections是Tomcat能接受的TCP连接数含等待队列里的请求。JVM参数在启动时设置java -Xms2g -Xmx2g -XX:MetaspaceSize512m \ -XX:MaxMetaspaceSize512m \ -Xss512k -XX:UseG1GC \ -jar fruit-shop.jar-Xms和-Xmx设置相等避免堆大小动态伸缩带来的性能抖动。UseG1GC适合多核大内存场景停顿时间可控。Xss512k是线程栈大小SpringBoot的反射和拦截器链条较深在JDK8环境下栈太小容易抛StackOverflowError。压测验证时先用JMeter模拟100并发打下单接口观察p99响应时间。如果超过500毫秒优先检查数据库连接池配置。默认的HikariCP连接池参数里maximum-pool-size是10这个数值在并发超过10时会成为瓶颈需要调大spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 3000调大连接池后重新压测观察响应时间曲线是否变得平滑。如果仍然存在长尾请求再回头看慢SQL日志用trace和dashboard逐个环节排查。这套“压测—观测—调参”的循环是我排查性能问题最常用也最有效的路径。本文还有配套的精品资源点击获取