
简介这份资源是面向Java/Web初学者与电商项目练手者的购物商城系统源码包围绕用户注册登录、商品浏览搜索、购物车管理与支付结算等核心链路展开适合作为课程设计、毕业设计或全栈入门实战的参考实现。压缩包为zip格式整体约3.29MB内含购物商城相关源码、数据库脚本与配置文件等可用于本地搭建与部署整套电商流程。目前已有219人学习下载说明其在同类练手项目中具备一定参考价值。读者可从中了解用户账户体系、购物车增删改查、订单确认与支付验证等模块的组织方式并借鉴前端界面与后端接口的分层结构快速搭建可运行的购物环境对照排查登录校验、库存与支付流程中的常见问题为后续二次开发与功能扩展提供基础。1. 购物商城从零到一为什么“能跑通”和“能扛住”是两码事很多开发者第一次接触购物商城项目脑子里想的都是“先把商品列表和购物车做出来再说”。我当初也是这么想的结果上线第一天就翻车了——商品详情页的库存数字在并发下单时直接变成负数订单和库存对不上用户投诉像雪片一样飞过来。购物商城这四个字看起来只是“展示商品、加购物车、下单支付”三步走但真正落地时它考验的是你对并发、事务、缓存一致性、幂等这些硬骨头的理解。这篇文章不跟你聊虚的就围绕一个典型的购物商城系统把从环境搭建、核心链路实现、参数调优到避坑排查的完整路径拆开讲。适合谁看如果你正在做课程设计、毕业项目或者想从零搭一个能演示、能压测、能讲清楚技术选型理由的购物商城那这篇笔记就是写给你的。新手能照着步骤跑通最小闭环熟手能直接跳到参数和边界条件那几章看踩坑记录。2. 购物商城的最小可运行骨架技术选型与本地环境搭建2.1 为什么我选 Spring Boot MySQL Redis 这套组合做购物商城第一件事不是写代码是定技术栈。我见过太多人在这步纠结半个月最后选了个自己都不熟的框架后面每一步都是坑。我的建议很直接如果你没有特殊约束就用 Spring Boot 做后端MySQL 做持久化Redis 做缓存和分布式锁。理由有三条。第一Spring Boot 的生态最全购物商城需要的事务管理、连接池、安全框架、消息队列集成都有现成的 starter不用自己造轮子。第二MySQL 的 InnoDB 引擎支持行级锁和事务这是处理库存扣减和订单创建的基础换其他数据库你得额外考虑兼容性。第三Redis 在购物商城里的角色太重要了——商品热点数据缓存、秒杀库存预扣、分布式锁防重复下单这些场景没有 Redis 会非常难做。有人会问“能不能用 MongoDB”可以但购物商城的订单和库存天然是关系型数据强一致性要求高关系型数据库更合适。选型定下来之后本地环境搭建就按下面这个清单走。组件版本建议用途本地启动方式JDK17运行 Spring Boot安装包直接装MySQL8.0订单、商品、库存持久化Docker 或本机安装Redis7.x缓存、分布式锁Docker 启动最省事Maven3.9依赖管理命令行或 IDE 内置2.2 用 Docker Compose 把 MySQL 和 Redis 拉起来本地开发最烦的就是环境不一致我一般用 Docker Compose 把中间件统一管起来。下面这个 compose 文件你直接抄改一下密码就能用。version: 3.8 services: mysql: image: mysql:8.0 container_name: mall-mysql environment: MYSQL_ROOT_PASSWORD: mall123456 MYSQL_DATABASE: mall_db ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql command: --default-authentication-pluginmysql_native_password --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci redis: image: redis:7-alpine container_name: mall-redis ports: - 6379:6379 volumes: - ./redis-data:/data command: redis-server --appendonly yes --requirepass redis123456这段配置里有两个参数值得注意。--default-authentication-pluginmysql_native_password是为了兼容老版本的客户端连接方式避免你用 Navicat 或 DataGrip 连不上。--appendonly yes是开启 Redis 的 AOF 持久化购物商城的库存预扣数据不能丢虽然本地开发丢一次影响不大但养成习惯没坏处。启动命令就一行docker compose up -d。起来之后用docker ps确认两个容器都是 healthy 状态再往下走。2.3 建表商品表、库存表、订单表的最小字段设计购物商城的表结构不用一上来就搞几十个字段先把核心链路跑通。我一般会建三张表商品表、库存表、订单表。商品表存基本信息库存表单独拆出来是为了方便加锁和扣减订单表记录交易。CREATE TABLE product ( id bigint NOT NULL AUTO_INCREMENT, name varchar(128) NOT NULL, price decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 1上架 0下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE stock ( product_id bigint NOT NULL, total int NOT NULL DEFAULT 0, available int NOT NULL DEFAULT 0, PRIMARY KEY (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order ( id bigint NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL, product_id bigint NOT NULL, quantity int NOT NULL, amount decimal(10,2) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节库存表的available字段和total分开是为了支持预占库存的逻辑。下单时先扣available支付成功再扣total取消订单时把available加回去。这样能避免“下单未支付”导致库存被永久占用的问题。订单表的order_no加了唯一索引这是防重复下单的最后一道防线后面讲幂等的时候会展开。3. 购物商城的核心链路从商品查询到下单扣库存的完整实现3.1 商品查询缓存穿透和缓存击穿怎么防商品查询是购物商城读请求最密集的地方如果每次都查 MySQL数据库扛不住。我一般用 Redis 做缓存但缓存用不好会引出两个经典问题缓存穿透和缓存击穿。缓存穿透是查一个数据库里也不存在的商品 ID请求每次都打到数据库缓存击穿是某个热点商品的缓存刚好过期大量请求同时涌向数据库。解决穿透的办法是缓存空值查不到也往 Redis 里写一个短过期的空对象。解决击穿的办法是加互斥锁只让一个线程去查数据库并回填缓存。public Product getProduct(Long productId) { String cacheKey product: productId; String cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { // 空值标记防止穿透 if (EMPTY.equals(cached)) { return null; } return JSON.parseObject(cached, Product.class); } // 互斥锁防止击穿 String lockKey lock:product: productId; try { Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { Product product productMapper.selectById(productId); if (product null) { redisTemplate.opsForValue() .set(cacheKey, EMPTY, 60, TimeUnit.SECONDS); return null; } redisTemplate.opsForValue() .set(cacheKey, JSON.toJSONString(product), 300, TimeUnit.SECONDS); return product; } else { // 没抢到锁短暂休眠后重试 Thread.sleep(50); return getProduct(productId); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } finally { redisTemplate.delete(lockKey); } }这段代码里空值缓存的过期时间设了 60 秒正常商品缓存设了 300 秒。为什么空值要短一些因为如果某个商品后来上架了空值缓存太久会导致用户一直看不到。互斥锁的过期时间设 10 秒是防止持锁线程崩溃后锁一直不释放。注意finally块里删锁的操作严格来说应该用 Lua 脚本保证原子性但本地开发和中小流量场景下这样写够用了后面进阶章节会讲怎么优化。3.2 下单扣库存乐观锁、悲观锁和 Redis 预扣怎么选下单扣库存是购物商城最容易出问题的地方。我见过三种做法悲观锁、乐观锁、Redis 预扣。悲观锁就是SELECT ... FOR UPDATE简单但并发差乐观锁用版本号或available quantity做条件更新并发好但失败率高Redis 预扣是把库存放到 Redis 里用 Lua 脚本原子扣减性能最好但一致性维护复杂。我的建议是中小流量用乐观锁秒杀场景用 Redis 预扣。下面先看乐观锁的实现。UPDATE stock SET available available - #{quantity} WHERE product_id #{productId} AND available #{quantity}这条 SQL 的AND available #{quantity}就是乐观锁的核心它保证库存不会被扣成负数。在 MyBatis 里执行后判断影响行数如果返回 0 说明库存不足直接抛异常回滚事务。这里有个血泪经验很多人只判断影响行数忘了在事务里加Transactional结果扣了库存但订单没创建成功数据不一致。记住扣库存和创建订单必须在同一个事务里。Transactional(rollbackFor Exception.class) public Order createOrder(Long productId, Integer quantity) { // 1. 扣库存 int affected stockMapper.deductStock(productId, quantity); if (affected 0) { throw new BizException(库存不足); } // 2. 创建订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setProductId(productId); order.setQuantity(quantity); order.setAmount(calculateAmount(productId, quantity)); order.setStatus(0); orderMapper.insert(order); return order; }generateOrderNo()我一般用“时间戳 用户ID后四位 随机数”拼不要用 UUID因为 UUID 做唯一索引时插入性能差。calculateAmount要从商品表重新查价格不能信前端传过来的金额这是安全底线。3.3 订单号生成与幂等防止用户连点两次下单用户手抖连点两次下单按钮或者网络重试导致请求重复购物商城必须保证同一请求只创建一个订单。这就是幂等。我的做法是前端传一个requestId后端用 Redis 做去重。第一次请求把requestId写到 Redis 并设过期时间后续相同requestId直接返回已有订单。public Order createOrderIdempotent(String requestId, Long productId, Integer quantity) { String idempotentKey idempotent:order: requestId; Boolean success redisTemplate.opsForValue() .setIfAbsent(idempotentKey, 1, 5, TimeUnit.MINUTES); if (Boolean.FALSE.equals(success)) { // 重复请求查已有订单返回 return orderMapper.selectByRequestId(requestId); } try { return createOrder(productId, quantity); } catch (Exception e) { // 失败要删掉幂等键允许用户重试 redisTemplate.delete(idempotentKey); throw e; } }注意catch块里删幂等键的操作。如果下单失败但不删键用户重试会被当成重复请求永远下不了单。这个坑我踩过排查了半天才发现是幂等键没清理。另外requestId最好由前端生成用 UUID 就行后端不要自己生成否则重试时对不上。4. 购物商城的避坑与排查5 个让我加班到凌晨的坑4.1 库存扣减后订单创建失败数据对不上现象压测时发现库存少了但订单没多查日志看到deductStock成功但insert order抛了异常。原因扣库存和创建订单不在同一个事务里或者事务传播行为配错了。解决确保两个操作在同一个Transactional方法内并且异常类型要触发回滚。默认情况下 Spring 只对RuntimeException回滚如果你抛的是Exception必须写Transactional(rollbackFor Exception.class)。这个坑我踩过两次现在写事务必加rollbackFor。4.2 Redis 缓存和数据库数据不一致现象后台改了商品价格前端刷新还是旧价格要等几分钟才变。原因更新数据库后没有删缓存或者删缓存失败但没重试。解决更新数据库后立即删缓存并且用“先更新数据库再删缓存”的顺序。如果删缓存失败把删缓存的操作发到消息队列里重试。更简单的做法是给缓存设一个较短的过期时间比如 300 秒作为兜底。别用“先删缓存再更新数据库”并发下会读到旧数据写回缓存。4.3 订单号重复导致插入失败现象高并发下偶尔报Duplicate entry for key uk_order_no。原因订单号生成算法有碰撞比如只用时间戳同一毫秒内多个请求生成相同订单号。解决订单号里加用户ID和随机数或者用 Redis 的INCR生成全局自增序列。我现在的做法是时间戳(毫秒) Redis自增序列(4位) 随机数(4位)基本不会碰撞。如果还是担心数据库唯一索引是最后防线插入失败就重试一次。4.4 分布式锁误删别人的锁现象A 线程加的锁被 B 线程删了导致并发问题。原因A 线程业务执行时间超过了锁的过期时间锁自动释放B 线程拿到锁A 执行完后删锁把 B 的锁删了。解决锁的 value 存一个唯一标识比如 UUID删锁前判断是不是自己的锁。更严谨的做法是用 Lua 脚本保证“判断删除”的原子性。这个坑在秒杀场景下特别容易出因为业务执行时间不可控。4.5 压测时数据库连接池被打满现象JMeter 压测到 500 并发时接口大量超时日志报Connection is not available。原因HikariCP 默认最大连接数是 10扛不住高并发。解决把maximum-pool-size调到 50 左右同时检查有没有慢 SQL 导致连接被长时间占用。另外connection-timeout不要设太大否则请求会一直等。我一般设 3000 毫秒超时快速失败比堆积强。调完连接池后记得看 MySQL 的max_connections别超过数据库上限。5. 购物商城的进阶技巧用压测数据反推参数调优5.1 用 JMeter 做一轮基准压测购物商城做完之后你得知道它能扛多少并发。我一般用 JMeter 做基准压测线程数从 50 开始逐步加到 500看 TPS 和响应时间的变化。压测接口选下单接口因为这是最重的链路。压测前把日志级别调到 WARN避免日志 IO 影响结果。下面是一个简单的 JMeter 命令行压测示例。jmeter -n -t order_test.jmx -l result.jtl -e -o report/-n是非 GUI 模式-t指定测试计划-l输出结果文件-e -o生成 HTML 报告。跑完之后打开report/index.html看聚合报告。重点看三个指标TPS、95% 响应时间、错误率。如果 TPS 上不去但 CPU 没跑满多半是数据库连接池或锁竞争的问题。5.2 根据压测结果调 HikariCP 和 Redis 参数压测数据出来之后调参就有依据了。下面这张表是我在某次压测后调整的参数对照你可以参考。参数默认值调整后调整理由HikariCP maximum-pool-size1050500 并发下连接不够用HikariCP connection-timeout300003000快速失败避免请求堆积Redis timeout20001000缓存操作要快超时立即降级Redis lettuce pool max-active832并发读缓存需要更多连接Tomcat max-threads200400配合连接池提升吞吐调完之后再压一轮对比 TPS 和响应时间。如果 TPS 提升明显但错误率也上去了检查是不是数据库扛不住了这时候要考虑加缓存或读写分离。调参不是一劳永逸的每次业务量变化都要重新压测。5.3 一个让我少加班的习惯把关键链路打上埋点最后分享一个习惯。购物商城的核心链路——商品查询、扣库存、创建订单、支付回调——每个环节我都加了埋点记录耗时和成功失败。用 Micrometer 或简单的 AOP 都行。这样出问题时不用翻半天日志直接看埋点数据就知道哪个环节慢了或挂了。比如有一次下单接口突然变慢埋点显示扣库存耗时从 5 毫秒涨到 200 毫秒一查是库存表的行锁竞争加了个 Redis 预扣就解决了。埋点数据还能帮你做容量规划知道什么时候该扩容。这个习惯让我少加了很多班希望帮到你。本文还有配套的精品资源点击获取