拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring Boot+MySQL网上商城系统:从建表到高并发事务实战

简介这是一份基于Spring Boot与MySQL的网上商城购物系统实现项目面向Java初学者、课程设计与毕业设计学生帮助理解真实电商平台从后端服务到数据持久化的完整构建过程。功能覆盖用户注册登录、商品浏览、购物车管理、订单处理、支付等核心模块前端涉及HTML/CSS/JavaScript及常用框架后端以RESTful API提供CRUD操作并采用MVC分层设计提升扩展性。配套资料包括可运行源码、设计文档、readme使用说明、演示PPT及开发环境配置清单压缩包大小约25.79MB能够系统学习Spring Boot、MySQL、Spring Security安全机制、缓存优化等关键知识点。已有2586人学习下载适合作为课程设计或毕业设计的参考范本也可在此基础上二次开发为完整商城项目。1. 网上商城购物系统难点从来不在CRUD拿到“基于Spring BootMySQL的网上商城购物系统设计与实现源码文档”这个项目第一反应是把它当成一个增删改查练习——用户表、商品表、订单表CRUD 接口一写系统就“完成”了。真这样做的人通常会在联调阶段被同一个问题卡住加购、下单、库存扣减分开看都没问题串起来跑就出超卖或者订单状态和支付流水对不上。网上商城的核心不是 Spring Boot 的接口有多快而是 MySQL 里的表结构能不能支撑一条完整的交易链路。购物车、订单、库存、支付流水这几张表之间的关系决定了后续所有业务代码的上限。帖子标题里带着“设计与实现”意味着你要交付的不只是能跑的代码还有一套能解释“为什么这么设计”的文档。本文按四层架构Controller / Service / Mapper / Entity重新梳理这套系统的落地路径从建表、写接口到压测排查每一步都给可直接抄的参数和命令。2. 先把 MySQL 表结构钉死购物系统的数据基石2.1 用户、商品、订单三组表的字段与索引设计购物系统最少需要六张核心表用户表、商品分类表、商品表、购物车表、订单表、订单明细表。很多教程会把购物车表砍掉用 Redis 替代但源码包的场景里最好把cart表留住——它能让“未登录也能加购、登录后合并购物车”这类需求从数据库层面直接支持而不用依赖外部缓存组件。用户表和商品表按常规设计即可用户表带username、passwordBCrypt 加密后存储、phone商品表带category_id外键、price用 DECIMAL(10,2) 而不是 FLOAT、stock作为库存字段。订单表和订单明细表是关联查询的绝对热点主表存总金额和状态子表存商品快照商品名称、下单时单价、数量——注意快照字段是必须的商品改价后历史订单不能被影响。CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已发货 3-已完成 4-已取消, address VARCHAR(255) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_items ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, product_name VARCHAR(100) NOT NULL COMMENT 商品快照, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明order_no唯一索引用来保证业务幂等create_time用数据库默认值而不是在 Java 代码里new Date()避免多台服务器时间不一致。订单明细冗余商品名称和单价这是典型的“用空间换一致性”做法查询订单详情时不需要 JOIN 商品表读压力直接从 MySQL 转移到内存。建表时还需要注意字符集统一用utf8mb4不要为了省空间用utf8——电商商品标题里出现 emoji 或生僻字会导致写入报错。数据库连接参数里也要显式加上characterEncodingutf8和serverTimezoneAsia/Shanghai这两个配置不加本地跑通一到服务器就报时间差 8 小时或中文乱码。2.2 Spring Boot 数据源连接 MySQL 8.x 的参数清单Spring Boot 2.7.x 配 MySQL 8.0 是最常见的组合pom.xml里引入spring-boot-starter-jdbc和mysql-connector-j两个依赖即可注意 MySQL 8.x 驱动类名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver。application.yml里除了基础的四项url、username、password、driver-class-name连接池参数必须显式设置。Spring Boot 默认连接池是 HikariCP性能很好但默认配置不一定适合购物系统的突发流量建议把最大连接数和空闲超时调出来spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000参数说明useSSLfalse是本地开发用的线上如果有 SSL 证书需求再打开allowPublicKeyRetrievaltrue是 MySQL 8.x 用 caching_sha2_password 插件时必需的不加会报Public Key Retrieval is not allowed。连接池部分maximum-pool-size建议按(CPU核心数 × 2) 磁盘数估算一个 4 核 8G 的服务器单实例跑购物系统20 是稳妥值堆到 100 反而会因为 MySQL 端线程切换导致吞吐下降。max-lifetime要小于 MySQL 的wait_timeout否则连接被服务端断开后客户端还在用出现Communications link failure。3. 从加购到下单Spring Boot 核心业务链路的实现与事务边界3.1 登录态与购物车接口的实现顺序购物系统最常见的实现顺序是用户注册登录 - 商品浏览 - 加购 - 下单支付。登录建议用 Token 方案而非 SessionSpring Boot 的HandlerInterceptor拦截非白名单接口生成 UUID 作为 Token 存到 Redis 或内存 Map请求头带Authorization: token即可。用 Redis 存 Token 的话要设置过期时间比如 2 小时并用拦截器做滑动续期。购物车表设计成(user_id, product_id)联合唯一索引加购接口用INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1一条 SQL 搞定新增和累加不需要先 SELECT 判断有没有再决定 INSERT 还是 UPDATE。这样能省一次数据库往返在高并发加购时效果明显。3.2 下单扣库存Transactional 与悲观锁的正确用法下单是购物系统里事务最重的操作创建订单主表记录、插入订单明细、扣减商品库存、清空购物车四步要么全成功要么全回滚。用 Spring 的Transactional注解包住这几个 Mapper 调用是最基本的但真正容易写错的是扣库存这一步。常见写法是SELECT stock FROM product WHERE id ?在 Java 里判断 stock 是否大于 0再执行UPDATE product SET stock stock - 1这个写法在并发下必出超卖——两个请求同时读到 stock 1都判定可以扣实际扣了两次变负数。正确做法是让扣减操作原子化或者用悲观锁把数据行锁住。先更新后判断影响行数Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, Long productId, Integer quantity) { // 1. 原子扣减库存只有库存充足时影响行数为1 int updated productMapper.deductStock(productId, quantity); if (updated 0) { throw new BusinessException(库存不足); } // 2. 创建订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(productMapper.selectById(productId).getPrice() * quantity); order.setStatus(0); orderMapper.insert(order); // 3. 插入订单明细 OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(productId); item.setQuantity(quantity); orderItemMapper.insert(item); // 4. 清空购物车 cartMapper.deleteByUserIdAndProductId(userId, productId); return order.getId(); }对应 Mapper 里的扣减 SQLUPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}逻辑说明这条 UPDATE 语句本身是行锁stock #{quantity}条件让数据库在原子层面保证不会扣成负数影响行数为 0 就说明库存不够直接抛异常回滚整个事务。比先 SELECT 再 UPDATE 少了一次查询也比SELECT ... FOR UPDATE持锁时间更短适合吞吐优先的购物场景。Transactional注解必须写rollbackFor Exception.class——Spring 默认只在遇到 RuntimeException 时回滚如果代码抛的是自定义检查异常不加这个参数事务不会回滚订单建了库存也扣了数据就歪了。这个坑几乎每一个从 CRUD 项目转过来的开发都会踩手册里如果看到事务失效的排查章节第一条就检查这里。另外事务方法不要写在 Controller 里调用的同类私有方法中自调用不走代理注解直接失效正确姿势是把事务逻辑放在 Service 类的方法里从 Controller 跨类调用。3.3 订单号生成与支付回调的幂等处理订单号不要用数据库自增 ID会暴露每日订单量而且多台应用服务器同时插入会有重复风险。常见做法是yyyyMMddHHmmss 用户ID后四位 随机四位数或者直接用UUID.replace(-, ).toUpperCase()但 UUID 作为订单号在数据库索引层面随机性太高BTree 插入时页分裂严重数据量大后写入性能会下降。折中方案是用雪花算法MyBatis-Plus 自带IdWorker趋势递增且全局唯一。支付回调是购物系统被问得最多的地方因为支付平台会多次回调同一个订单。处理逻辑必须加幂等判断根据order_no查询订单状态如果已经是“已支付”则直接返回成功不再执行加余额或改状态操作否则重复回调会把订单状态从已支付又改成已支付同时把用户账户余额加上两次。4. 跑通之后必须做的三件事索引覆盖、连接池调优和事务失效排查4.1 用 EXPLAIN 验证商品列表页的 SQL 是否走索引商城首页的商品列表和搜索是访问频率最高的查询很多人会在这里用模糊查询WHERE product_name LIKE %手机%。这个写法在 MySQL 里无法命中普通 BTree 索引全表扫描是必然的。如果必须做文本搜索要么用LIKE 手机%走前缀索引要么引入 Elasticsearch——但源码项目里没必要常见的做法是给商品名加前缀索引或者分表字段直接按分类查。用 EXPLAIN 分析慢查询是必学的技能。在 Navicat 或命令行执行EXPLAIN SELECT * FROM product WHERE category_id 1 AND status 1 ORDER BY create_time DESC LIMIT 10;重点看type字段达到ref或range才算正常ALL就是全表扫描。如果排序字段导致 filesort考虑在(category_id, status, create_time)上建联合索引让索引覆盖排序和过滤避免 MySQL 把全表数据加载到临时文件做排序。商品查询最容易用的联合索引是(category_id, status)加购和下单时用的是主键查询WHERE id ?走聚簇索引这两类查询加起来覆盖了 80% 的数据库访问。索引不是越多越好每张表超过 5 个索引会让 INSERT 变慢购物系统的核心是读写混合要把索引建在线上确认慢的查询上不要提前加。4.2 开启 MySQL 慢查询日志的配置与解读slow_query_log ON slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 log_queries_not_using_indexes ON这些配置放在my.cnf的[mysqld]段下重启后生效。long_query_time 1表示超过 1 秒的 SQL 会被记录log_queries_not_using_indexes会把没走索引的查询也写进来哪怕执行时间只有 50 毫秒——因为没走索引的查询会随着数据量增长线性变慢是潜在风险。线上压力测试后看一下这个日志如果购物车合并、订单列表查询高频出现在里面优先检查 WHERE 条件里的字段是否被函数包裹比如WHERE DATE(create_time) 2024-05-20这样写索引就废了要改成WHERE create_time 2024-05-20 00:00:00 AND create_time 2024-05-21 00:00:00。4.3 事务失效的 3 个高频场景对应的修复代码事务失效是面试和实际开发都会被反复问的坑电商系统交易链路数据敏感一个事务没生效可能导致订单有明细但库存没扣用户两边都吃亏。最常见三个场景和对应的修复方式第一个是接口方法直接抛异常且被 catch 住事务感知不到异常自然不回滚。修复方式是 catch 块里throw new RuntimeException(e)重新抛出让 Spring 拦截到。第二个是同类内部调用this.createOrder()Spring 代理对象拦截不到内部直接的方法调用。修复方式是在另外一个 Service 类里注入当前类的代理或者在当前类注入ApplicationContext拿上下文的 Bean 来调用public void outerMethod() { // 错误this.innerMethod() 不走代理 // 正确从容器里拿代理对象再调用 MallOrderService orderService applicationContext.getBean(MallOrderService.class); orderService.innerMethod(); }第三个是Transactional注解加在了 private 方法上Spring 的 CGLIB 代理只能拦截 public 方法private 方法注解直接失效。把这个方法改成 public或者拆到专门的 Service 里。4.4 交易场景的 MySQL 隔离级别选择MySQL 默认隔离级别是可重复读REPEATABLE READ购物系统的订单查询在这个级别下不会出现幻读问题因为 InnoDB 的间隙锁把范围查询锁住了。但要注意下单扣库存用的行锁在可重复读下依然有效不需要为了性能去改 READ COMMITTED——MySQL 在 RR 级别下的并发性能相比 Oracle 并没有断崖式差距改隔离级别带来的收益通常小于间隙锁保护数据一致性的价值。如果你的系统需要同时支持订单表和明细表之间的一致性快照读RR 的开销是值得的。5. 用 Spring Boot Actuator 做一次“没出事前”的验收打开源码包里的pom.xml如果没引入 Actuator建议加上spring-boot-starter-actuator。它能暴露应用的健康状态、内存使用、线程池活动线程数、HTTP 请求耗时分布等关键指标是判断购物系统上线前是否“体力达标”的最快手段。很多开发习惯性地把management.endpoints.web.exposure.include*配成全开然后不管了——这等于把内部信息开放给了所有人。Actuator 在 Spring Boot 2.x 里/actuator/heapdump可以直接下载整个 JVM 堆内存快照配合jvisualvm能分析出所有内存中的用户密码未授权访问的问题在安全扫描里属于高危漏洞。生产环境的暴露策略应该显式收口management: endpoints: web: exposure: include: health,info,metrics,httptrace endpoint: health: show-details: when-authorized参数说明只开放health、info、metrics、httptrace四个端点health详细状态只在登录后可见。验证方法是用浏览器直接访问/actuator/env如果是原本的全开配置如果返回一段 JSON 里包含数据库密码说明配置失效了需要立刻改。再用命令curl http://localhost:8080/actuator/metrics/hikaricp.connections.active看连接池活性连接数如果长时间卡在最大值多半是连接泄漏——Service 层查询没有关闭 Statement或者 HikariCP 的max-lifetime和 MySQL 的wait_timeout不匹配。验收的最后一步是把httptrace端点打开后用 JMeter 模拟 50 个并发用户同时下单再访问/actuator/httptrace检查最近请求里有没有超过 500ms 的接口。那个超过 500ms 的接口往往就是你上线后第一个要优化的 SQL——很可能是商品列表页的LIKE %关键词%或者订单列表的深分页LIMIT 10000, 20。深分页改成WHERE id 上一页最后一条ID ORDER BY id LIMIT 20的键集分页响应时间能从 800ms 降到 50ms 以内这个改动比加任何缓存都来得直接有效。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门