基于SpringBoot的Java毕设电商平台实战从模块解耦到高并发下单优化1. 学生项目常见痛点为什么跑完演示就崩了毕设答辩现场老师一句“并发 100 下单试试”往往让系统直接 502。把最常见、也最容易被忽视的三颗雷先拎出来事务失效Service 层方法被同类内部调用Transactional 注解被 Spring AOP 绕过库存扣减与订单写入不在同一事务出现“超卖”或“少卖”。重复提交前端没做防抖F5 刷新一下订单表瞬间多两条记录后端也没做幂等老师一压测就穿帮。N1 查询商品列表接口用 MyBatis 默认懒加载1 次查 20 条商品再循环 20 次查库存、20 次查店铺信息接口 RT 直接飙到 2 s。这三颗雷只要爆一颗答辩分数就“原地蒸发”。下文所有设计都围绕“先排雷、再提速”展开。2. 技术选型对比别在毕设里“炫技”要“稳”把容易纠结的几组选型拉个表结论先行再讲原因。维度方案 A方案 B推荐毕设场景理由ORMMyBatis-PlusSpring Data JPAMyBatis-Plus手写 SQL 灵活后期加索引、联表不踩坑JPA 在复杂查询下容易 N1调优门槛高。缓存Caffeine 本地缓存Redis 远程缓存Redis本地缓存无法横向扩展压测时多实例数据不一致Redis 还能当分布式锁用一举两得。消息组件JDK 线程池RabbitMQRabbitMQ线程池在重启后任务全丢老师一句“断电恢复”就翻车RabbitMQ 持久化ACK 更稳。安全框架ShiroSpring SecuritySpring 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.UserlambdaQuery() .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. 缓存 refreshToken7 天过期 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){ ListCartItem guest redisTemplate.opsForList() .range(anonymousKey, 0, -1); ListCartItem login cartMapper.listByUserId(userId); // 以 skuId 为 key 聚合数量 MapLong, 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 SETNX2 min 过期防止网络重试。SQL 注入MyBatis-Plus #{} 占位符已预编译额外打开全局关键字过滤把sleep、benchmark等函数拉黑。JWT 令牌刷新前端拦截 401自动调用/auth/refresh后端验证 RefreshToken 后颁发新 AccessToken用户无感续期。关键接口限流网关层用 Bucket4j按 IP接口维度 10 r/s超阈值返回 429保护下游。5. 生产环境避坑指南老师问“你们真上线了吗”也能对答如流MySQL 连接池HikariCP 默认 10 个连接压测时一定改到 50~100并打开leakDetectionThreshold5s慢 SQL 立即报警。日志脱敏统一 Logback 过滤器把手机号、邮箱、身份证用DesensitizeStrategy正则脱敏日志文件即使被拷贝也不会泄露隐私。静态资源 CDN图片、CSS、JS 走 OSS CDN回源流量 0.12 元/GB比带宽省钱还能隐藏真实服务器 IP。灰度发布用 Nginxsplit_clients模块按 Cookie 比例 5% 流量打到新 Jar出问题秒级回滚老师问“如何回滚”可直接演示。监控看板SpringBoot Actuator Prometheus GrafanaJVM 线程、接口 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 并发并保证压测数据不污染线上”—— 提示影子库 参数化脚本 关闭消息消费。动手试试你会对“高并发”有真正的体感。祝重构顺利答辩一把过