基于SpringBoot的Java毕设电商平台实战:从模块解耦到高并发下单优化
1. 学生项目常见痛点:为什么跑完演示就崩了?
毕设答辩现场,老师一句“并发 100 下单试试”往往让系统直接 502。把最常见、也最容易被忽视的三颗雷先拎出来:
- 事务失效:Service 层方法被同类内部调用,@Transactional 注解被 Spring AOP 绕过,库存扣减与订单写入不在同一事务,出现“超卖”或“少卖”。
- 重复提交:前端没做防抖,F5 刷新一下,订单表瞬间多两条记录;后端也没做幂等,老师一压测就穿帮。
- N+1 查询:商品列表接口用 MyBatis 默认懒加载,1 次查 20 条商品,再循环 20 次查库存、20 次查店铺信息,接口 RT 直接飙到 2 s。
这三颗雷只要爆一颗,答辩分数就“原地蒸发”。下文所有设计都围绕“先排雷、再提速”展开。
2. 技术选型对比:别在毕设里“炫技”,要“稳”
把容易纠结的几组选型拉个表,结论先行,再讲原因。
| 维度 | 方案 A | 方案 B | 推荐(毕设场景) | 理由 |
|---|---|---|---|---|
| ORM | MyBatis-Plus | Spring Data JPA | MyBatis-Plus | 手写 SQL 灵活,后期加索引、联表不踩坑;JPA 在复杂查询下容易 N+1,调优门槛高。 |
| 缓存 | Caffeine 本地缓存 | Redis 远程缓存 | Redis | 本地缓存无法横向扩展,压测时多实例数据不一致;Redis 还能当分布式锁用,一举两得。 |
| 消息组件 | JDK 线程池 | RabbitMQ | RabbitMQ | 线程池在重启后任务全丢,老师一句“断电恢复”就翻车;RabbitMQ 持久化+ACK 更稳。 |
| 安全框架 | Shiro | Spring Security | Spring Security | 与 SpringBoot 无缝集成,JWT 插件成熟,社区示例多,省时间。 |
结论:毕设技术栈首重“资料全、能跑通、老师懂”,别选冷门组合给自己加戏。
3. 核心模块实现细节:DDD 拆包 + 代码级示例
3.1 模块划分(DDD 轻量化)
- 用户域(user-center):注册、登录、JWT 刷新
- 商品域(item-center):商品、库存、SKU
- 订单域(order-center):购物车、订单、订单明细
- 网关域(gateway):统一鉴权、限流、日志
每个域都是独立 SpringBoot Module,用 Maven 依赖串联,IDEA 里一键折叠,答辩时老师看得清爽。
3.2 用户鉴权:JWT 双 Token 机制
- AccessToken:有效期 15 min,放在 Header;失效后前端用 RefreshToken 换新的。
- RefreshToken:有效期 7 天,Redis 存储,可手动吊销。
- 统一网关解析 Token,把 userId 塞进请求头,下游服务无感解密。
关键代码(Clean Code,中文注释):
// UserAuthService.java public TokenInfo login(LoginDTO dto){ // 1. 校验收参 ValidationUtil.validate(dto); // 2. 密码解密(RSA)并验证 String rawPwd = rsaUtil.decrypt(dto.getPassword()); User user = userMapper.selectOne(Wrappers.<User>lambdaQuery() .eq(User::getUsername, dto.getUsername())); Assert.notNull(user, "用户名或密码错误"); if(!pwdEncoder.matches(rawPwd, user.getPassword())){ throw new BizException("用户名或密码错误"); BizException("用户名或密码错误"); } // 3. 生成双 token String accessToken = jwtUtil.createAccessToken(user.getId()); String refreshToken = jwtUtil.createRefreshToken(user.getId()); // 4. 缓存 refreshToken,7 天过期 redisTemplate.opsForValue() .set(RedisKey.REFRESH + user.getId(), refreshToken, Duration.ofDays(7)); return new TokenInfo(accessToken, refreshToken); }3.3 购物车合并:登录前后双端同步
游客态购物车存在存在 localStorage,登录后调/cart/merge接口:
- 前端把匿名 cartKey 带上来;
- 后端根据 userId 查库,Redis 缓存做交集合并;
- 返回最新条数,前端清空 localStorage。
合并逻辑伪代码:
public int merge(String anonymousKey, Long userId){ List<CartItem> guest = redisTemplate.opsForList() .range(anonymousKey, 0, -1); List<CartItem> login = cartMapper.listByUserId(userId); // 以 skuId 为 key 聚合数量 Map<Long, Integer> group = Stream.concat(guest.stream(), login.stream()) .collect(Collectors.groupingBy( CartItem::getSkuId, Collectors.summingInt(CartItem::getQuantity) )); // 写回 DB 并清缓存 cartMapper.replaceBatch(userId, group); redisTemplate.delete(anonymousKey); return group.size(); }3.3 订单创建:防超卖 + 幂等 + 异步扣库存
时序图:用户提交 → 网关限流 → 订单域先写“订单流水”(状态 UNPAID) → 发 RabbitMQ → 库存域消费扣减 → 回写订单状态。
关键三件套:
- 乐观锁:库存表加 version 字段,MyBatis-Plus 用
@Version即可。 - 分布式锁:Redis + Redisson,防止 10 w 并发一起扣同一件商品。
- 幂等令牌:订单域先写唯一索引(order_no, user_id),重复提交直接抛 DuplicateKeyException,前端捕获后提示“订单已提交”。
// OrderService.java @GlobalTransactional // Seata 分布式事务 public Long createOrder(CreateOrderDTO dto){ // 0. 幂等校验 String orderNo = generator.generateOrderNo(dto.getUserId()); try{ orderMapper.insertSelective(buildOrder(orderNo, dto)); }catch(DuplicateKeyException e){ throw new BizException("订单已提交,请勿重复下单"); } // 1. 分布式锁,锁商品维度 RLock lock = redisson.getFairLock("decStock:" + dto.getSkuId()); if(!lock.tryLock(3, TimeUnit.SECONDS)){ throw new BizException("系统繁忙,请稍后再试"); } try{ // 2. 扣库存(乐观锁) int affected = skuMapper.decreaseStock(dto.getSkuId(), dto.getQuantity()); if(affected == 0){ throw new BizException("库存不足"); } // 3. 发消息,异步落库 rabbitTemplate.convertAndSend("order.event", new StockDeducedEvent(orderNo)); }finally{ lock.unlock(); } return orderNo; }4. 性能与安全考量:把“能跑”升级成“抗揍”
- 接口幂等性:除数据库唯一索引,所有写操作带 UUID(前端生成)+ 后端 Redis SETNX,2 min 过期,防止网络重试。
- SQL 注入:MyBatis-Plus #{} 占位符已预编译,额外打开全局关键字过滤,把
sleep、benchmark等函数拉黑。 - JWT 令牌刷新:前端拦截 401,自动调用
/auth/refresh,后端验证 RefreshToken 后颁发新 AccessToken,用户无感续期。 - 关键接口限流:网关层用 Bucket4j,按 IP+接口维度 10 r/s,超阈值返回 429,保护下游。
5. 生产环境避坑指南:老师问“你们真上线了吗?”也能对答如流
- MySQL 连接池:HikariCP 默认 10 个连接,压测时一定改到 50~100,并打开
leakDetectionThreshold=5s,慢 SQL 立即报警。 - 日志脱敏:统一 Logback 过滤器,把手机号、邮箱、身份证用
DesensitizeStrategy正则脱敏,日志文件即使被拷贝也不会泄露隐私。 - 静态资源 CDN:图片、CSS、JS 走 OSS + CDN,回源流量 0.12 元/GB,比带宽省钱,还能隐藏真实服务器 IP。
- 灰度发布:用 Nginx
split_clients模块,按 Cookie 比例 5% 流量打到新 Jar,出问题秒级回滚,老师问“如何回滚”可直接演示。 - 监控看板:SpringBoot Actuator + Prometheus + Grafana,JVM 线程、接口 QPS、99 RT 全上墙,答辩现场大屏一投,分数+10。
6. 完整可运行代码片段:乐观锁 + 事件驱动
// SkuMapper.java @Update("update pms_sku set " + "stock = stock - #{quantity}, " + "version = version + 1 " + "where id = #{skuId} and version = #{version} and stock >= #{quantity}") int decreaseStock(@Param("skuId") Long skuId, @Param("quantity") Integer quantity, @Param("version") Integer version);// StockDeducedEventConsumer.java @RabbitListener(queues = "stock.deduced.queue") public void onMessage(StockDeducedEvent event){ // 1. 更新订单状态 orderMapper.updateStatus(event.getOrderNo(), OrderStatus.PAID); // 2. 发送延迟消息,15 min 后关单 rabbitTemplate.convertAndSend( "order.delay", new CloseOrderEvent(event.getOrderNo()), msg -> { msg.getMessageProperties().setDelay(15 * 60 * 000); return msg; }); }7. 动手重构你的毕设:三步走
- 拉分支:把现有单体复制一份叫
monolith-backup,保证能回滚。 - 拆模块:先拆“订单”域,建独立 module,只移表、移接口,不改动业务;跑通测试后再拆“商品”域。
- 上压测:用一台 4C8G 学生机即可,本地起 Docker 版 MySQL + Redis,装个
wrk2脚本,50 线程 5 min 把下单接口压到 300 QPS,观察 CPU、RT、错误率。能抗住 300 QPS 不崩溃,答辩就够用了。
最后留一道思考题:
“在只有 4C8G 的硬件下,如何模拟 1 w 并发并保证压测数据不污染线上?”
—— 提示:影子库 + 参数化脚本 + 关闭消息消费。动手试试,你会对“高并发”有真正的体感。
祝重构顺利,答辩一把过!