SpringBoot+Redis电商实战:前后端分离、缓存与防超卖设计 简介在Java全栈开发中电商系统是综合检验CRUD、缓存、事务与接口设计能力的经典场景。前后端分离架构让Vue与SpringBoot各司其职接口复用性也大幅提升。作为核心中间件Redis在商品缓存、购物车存储和库存扣减中扮演关键角色但缓存穿透、击穿、雪崩以及并发超卖都是绕不开的工程难题。通过合理设计Maven多模块工程、借助SpringBoot整合MySQL与MyBatis并利用乐观锁或Redis原子操作保障数据一致性才能真正构建一个可应对中小流量的电商后端。本文从基础环境配置到核心模块落地系统梳理缓存策略、订单事务、JWT鉴权与分布式锁等高频面试点帮助开发者将理论转化为可运行的实战经验。 先别急着打开IDE敲代码。我知道你现在可能正盯着一个网上电商项目前后端分离 Java Vue SpringBoot SSM MySQL Maven Redis的标题发呆脑子里想的不是技术栈而是这项目到底怎么起步模块怎么切Redis到底用在哪为什么网上资料一堆合在一起就跑不起来我前后带过好几批新人做这类电商项目也亲手从零搭过好几套踩过的坑比写过的接口都多。这篇文章就按我做项目的习惯把这套组合从选型逻辑、工程结构、核心模块落地到高频问题排查一条线讲清楚。不搞那些只贴代码不解释为啥的教程重点放在为什么这么设计上。这套项目适合谁一句话已经学过Java基础、懂一点SpringBoot和Vue但没完整做过一个全栈项目的人。它对标的不是高并发秒杀系统而是让你把CRUD、缓存、事务、前后端交互这些后端基本功练扎实。做完了你去面中小厂的Java岗聊项目经验完全不虚。1. 项目定位与整体设计思路1.1 为什么要做前后端分离的电商项目一说练手项目很多人第一反应是博客系统、管理系统。不是不行但电商的覆盖面完全是另一个量级。一个电商项目里用户注册登录、商品展示、购物车、下单、库存扣减、后台管理这些模块几乎把后端日常开发的典型场景全占满了。更重要的是电商天然带着高并发的影子虽然我们练手阶段用不到消息队列、分库分表但Redis缓存、分布式锁、接口幂等等这些东西只有在电商业务里才有最自然的落点。前后端分离这个架构我个人认为是现在做项目的默认选择不是可选项。它把前端Vue和后端SpringBoot拆成两个独立进程通过接口通信。这对开发者的好处是关注点彻底分开。前端不用管Controller里return的是ModelAndView还是JSON后端也不用操心页面渲染只管把接口设计好。真实企业里前端组和后端组就是这样协作的你提前习惯这种节奏进了公司不用重新适应。还有一点很多人没意识到前后端分离后后端接口具备了复用性。同一个商品列表接口PC端的Vue页面能用小程序也能调甚至将来的App后端还是同一套。这才是接口设计的正确姿势而不是每个端各写一套。1.2 技术栈选型每个组件都不是凑数的聊到具体技术我见过不少项目是为了用而用——简历上写着Redis实际项目里一个缓存都没用面试官一问就露馅。所以先理清楚这套技术栈里每个角色到底干什么。SpringBoot是项目的底座负责自动装配、启动、内嵌Tomcat。你可能听过SSMSpring SpringMVC MyBatis这两个概念容易绕晕。我的理解是SpringBoot不是替代SSM而是把Spring和SpringMVC的很多繁琐配置给自动化了。你在SpringBoot项目里照样写Service、Controller、Mapper只不过少了一大堆XML配置。严格说SpringBoot MyBatis这套组合就是SSM的现代化形态。简历上写SpringBoot SSM面试官不会觉得矛盾反而会觉得你对SSM时代的东西有认知。MySQL不用多说电商核心数据——用户、商品、订单全部落在这里。Redis的戏份则要重得多商品详情缓存、购物车存储、分布式Session、库存扣减的原子操作、接口限流全都能用它。说白了MySQL管最终一致性Redis管高性能。Maven负责依赖管理和构建。它解决的核心痛点是以前SSM项目要手动下载一堆jar包还要担心版本冲突Maven通过pom.xml统一声明依赖自动拉取、传递依赖、打包构建一套搞定。你说它可有可无等你哪天手贱改了一个依赖版本导致整个项目启动报错你就知道Maven的依赖管理有多重要了。2. 工程架构与业务模块设计2.1 Maven多模块模块划分比写代码更重要多人协作或者项目稍微大一点单模块工程就是灾难。所有人都往一个代码库里塞Controller、Service、MapperGit冲突能让你怀疑人生。所以这个电商项目我建议一开始就按多模块来建。通常的拆分方式是mall-common公共工具类、统一返回结果、异常处理、常量定义mall-mapperMyBatis的Mapper接口和XML映射文件以及Entity实体类mall-service业务逻辑层Service接口和实现类mall-controller接口层Controller和DTO/VOmall-admin后台管理系统入口模块运行在8081端口mall-portal前台商城系统入口模块运行在8080端口这样的好处是清晰依赖方向是自上而下的controller依赖serviceservice依赖mapper谁都不许反向依赖。比如你想给Service层单独写单元测试不需要启动Web容器直接依赖mapper模块就能跑。这里要给个忠告多模块虽然好但别一开始就拆得太碎。三四个模块足够拆到十几个模块只会让自己烦死。我见过最离谱的是把每个Service都拆成独立模块结果改一个字段要改五六个pom.xml纯属自找麻烦。2.2 用户、商品、订单三条业务线怎么串起来电商的后端业务说白了就是三条线用户线、商品线、订单线。三者的关系是用户浏览商品把商品加入购物车最后生成订单支付。用户模块的核心是注册登录和鉴权。练手阶段我建议用JWTJSON Web Token而不是Session。原因很简单前后端分离后后端接口天然无状态JWT把用户信息加密放在Token里前端每次请求带上后端解析验证即可。这比Session更贴合分离架构也是现在企业里的主流做法。很多朋友在面试时被问JWT和Session有什么区别做过这个项目就能答得比较扎实。商品模块就是典型的CRUD但它最容易出彩的地方在缓存策略。一个商品详情页热点商品的访问量可能是普通商品的几十倍如果每次都查MySQL数据库迟早被拖垮。所以商品详情要设计成先查Redis没有就查MySQL然后回填Redis的三级流程。订单模块是整个项目的核心难点。用户下单那一刻要先扣库存再生成订单这两个操作必须在一个事务里。扣库存还不能超卖这就要用到乐观锁或者Redis的原子操作。很多人的项目就死在这一步库存扣了订单没生成或者订单生成了库存变成负数。这些问题到第4部分细说。3. 从零搭建环境配置与项目初始化3.1 基础环境JDK、Maven、MySQL、Redis这一步最枯燥但也是劝退新手最多的地方。很多人项目跑不起来不是代码问题是环境没配好。JDK建议直接用8或者11别追新。虽然现在Java 17、21都出来了但SpringBoot 2.x在JDK 8下最稳资料也最多。如果你用的SpringBoot版本太高编译报错先别慌第一件事看JDK版本是不是匹配。至于Java环境变量配置记得配JAVA_HOME和PATH然后java -version验证一下。这一步出了很多问题是正常的多配几次就熟了。Maven的话重点不是下载安装而是配置文件settings.xml。一定要改本地仓库路径默认在C盘迟早爆掉然后配阿里云镜像。不配镜像的话拉一个SpringBoot依赖能等半小时。配完以后新建项目会快得飞起。MySQL这边版本建议5.7或8.0。安装配置教程网上很多我只提两个关键点一是字符集一定要选utf8mb4不然存个表情符号直接乱码二是记得给root设置密码别用空密码不然之后连数据源总是报错还找不到原因。可视化工具我用的是MySQL Workbench免费够用。Redis在Windows下开发的话直接下载Windows版本解压即用双击redis-server.exe启动。如果用的是macOS或Linuxbrew install redis或apt install redis也行。连接工具推荐Another Redis Desktop Manager开源的比Redis Desktop Manager更好用表格化查看key实在方便。3.2 SpringBoot整合SSM数据源、MyBatis与Redis配置环境配好之后开始搭项目骨架。用Spring Initializr生成基础工程引入Web、MyBatis、MySQL Driver、Redis、Lombok这几个依赖。如果需要JWT再加一个jjwt。然后就是核心配置文件application.yml。直接看示例server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 redis: host: localhost port: 6379 password: database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.mall.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl几个配置点单独说一下。serverTimezoneAsia/Shanghai这一串必须加不然MySQL 8的驱动会报时区错误项目启动直接失败。连接池默认HikariCP不用改实现但参数要调。maximum-pool-size默认10小项目可以设20太大会浪费数据库连接资源太小并发一上来就阻塞。这里不需要精确算知道它管的是同时能拿多少个数据库连接就够了。Redis的timeout: 3000ms是连接超时时间设太短会导致启动时Redis没起来就报错设太长又会让接口超时感很明显。3000毫秒是我试过比较舒服的值。MyBatis的map-underscore-to-camel-case这个配置一定要开。什么意思数据库的字段是user_nameJava属性是userName开启这个自动映射后查询结果能自动对应上不用写一堆resultMap别名。新手常常在这上面写一堆没必要的映射代码其实一个配置就搞定了。3.3 Vue前端与前后端联调前端的搭建我建议直接用Vite Vue 3比Vue CLI轻快很多。npm create vuelatest或者npm create vitelatest生成工程然后npm install装依赖。这里有个大坑建议用国内npm镜像npm config set registry https://registry.npmmirror.com不然装个Vue-router加Pinia要等到天荒地老。前端工程化之后路由用Vue Router状态管理用PiniaHTTP请求用Axios。Vue Router要注意的是采用history模式时刷新页面会404需要后端配合做history fallback如果嫌麻烦用hash模式但URL会多个#不太好看。开发阶段我用的是createWebHistory同时让后端接口支持CORS跨域这样前后端分开跑联调方便。联调环节最核心的坑就是跨域。前后端分离后Vue跑在5173端口SpringBoot跑在8080端口浏览器默认会拦截跨域请求。解决方案有两种一是在SpringBoot里写CORS配置类二是通过Vite代理转发。我个人推荐开发阶段用Vite代理因为生产环境一般用Nginx统一代理更贴近真实部署。SpringBoot的CORS配置也很好写Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意这里要写allowedOriginPatterns而不是allowedOrigins因为当你设置allowCredentials(true)时SpringBoot高版本不允许使用通配符*作为来源直接上线会报错。这个小坑我是硬生生被报错信息教会的。接口联调时记得安装Vue Devtools插件它可以在浏览器里直接查看Vue组件的状态和路由变化排查数据流问题效率翻倍。4. 核心场景实战缓存、购物车与库存4.1 商品缓存穿透、击穿与空值兜底商品详情是电商里Redis缓存最典型的应用场景。但很多人只写了先查缓存没有再查库两层逻辑这在低并发下没问题一旦并发上来三个经典问题立刻暴露。第一个是缓存穿透请求的商品ID在数据库里根本不存在于是每次都绕过缓存直接查库数据库被打爆。解决思路很简单查询结果为空也缓存但过期时间设短一点比如60秒同时value存个空值标记。这样同一个不存在的ID在一分钟内只查一次数据库。第二个是缓存击穿某个热点商品的key刚好过期一瞬间大量请求全部冲到数据库。解决办法是加互斥锁只有拿到锁的线程才去查数据库并回填缓存其他线程先等待然后重新查缓存。这个锁在单机场景下用ReentrantLock就行多机部署就要用Redis分布式锁了。第三个是缓存雪崩大量key同时过期所有请求同一时间打向数据库。解决办法有两个方向一个是给过期时间加随机值让过期时间均匀分布另一个是热点数据永不过期靠后台异步任务刷新。在练手项目里我建议用过期时间加随机值的方式简单有效。我实际写商品查询的代码大概长这样public ProductVO getProduct(Long id) { String key product: id; String json redisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { return JSON.parseObject(json, ProductVO.class); } // 缓存击穿互斥锁处理热点key重建 String lockKey lock:product: id; boolean locked lockService.tryLock(lockKey, 5); if (!locked) { // 没拿到锁短暂休眠后递归重试 try { Thread.sleep(100); } catch (InterruptedException e) { ... } return getProduct(id); } try { // 双重检查拿到锁后可能已经有线程回填了缓存 json redisTemplate.opsForValue().get(key); if (StringUtils.hasText(json)) { return JSON.parseObject(json, ProductVO.class); } ProductVO product productMapper.selectById(id); if (product null) { // 缓存穿透空值兜底 redisTemplate.opsForValue().set(key, , 60, TimeUnit.SECONDS); return null; } redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30 * 60 * 1000 RandomUtils.nextInt(60) * 1000, TimeUnit.MILLISECONDS); return product; } finally { lockService.unlock(lockKey); } }这段代码把穿透、击穿、雪崩三个问题的处理都包含进去了写进简历也是能拿出来聊的。注意缓存过期时间我加了随机值就是为了防止雪崩。4.2 购物车Redis Hash的工程落地购物车这个功能用MySQL表也能做但体验一定不好。你想啊购物车是高频读写的数据用户往购物车加个商品、改个数量每次都去操作数据库没几步就把数据库拖累了。而且购物车的业务特点是用户自己操作自己的天然适合用Redis的Hash结构存储。Hash结构的key是cart:userIdfield是商品SKU的IDvalue是商品数量或者包含数量、商品快照信息的JSON字符串。为什么用Hash不用String因为Hash结构能对单个field做增加、减少、删除操作不需要把整个购物车取出来再序列化回写。比如用户反复点击加购按钮一行redisTemplate.opsForHash().increment(key, String.valueOf(skuId), 1)就搞定了数量累加。购物车还有一个容易被忽略的细节未登录状态下的购物车怎么办很多电商的做法是未登录时存本地localStorage登录后合并到Redis。这个合并逻辑要处理数量累加、重复商品去重。练手项目可以不用做这么复杂但我建议至少把登录状态和未登录状态设计清楚不然面试官一问就答不上来。购物车数据要不要设置过期时间我的建议是要。用户加了购物车但半年不登录这些数据还占着Redis内存这说不过去。设置30天过期是个合理的折中。Redis的过期策略是惰性删除加定期删除你不需要关心底层机制只要知道设置过期时间后访问时如果已过期会被自动清理。4.3 扣库存事务、乐观锁与防超卖扣库存是整个电商项目里最容易写错的地方。常见的错误写法是先SELECT查库存判断库存是否大于0然后UPDATE扣减。这种写法在并发下必出问题——两个请求同时查到库存为1都通过了判断然后都去扣减库存变成-1超卖了。解决超卖的第一层方案是SQL层面的条件更新UPDATE sku_stock SET stock stock - #{count} WHERE sku_id #{skuId} AND stock #{count}关键在这个AND stock #{count}。MySQL的UPDATE是行级锁同一时刻只有一个事务能更新这一行后面的请求会被阻塞等待更新时重新判断条件。如果库存不足影响行数为0我们就能感知到扣减失败然后抛出库存不足的异常让事务回滚。但SQL解决了超卖解决不了效率。高并发场景下所有请求排队等这一行锁吞吐量上不去。所以进阶方案是用Redis做前置库存预扣减提前把库存量同步到Redis下单时先redisTemplate.opsForValue().decrement判断返回值不小于0才继续走数据库扣减如果Redis扣减失败直接返回库存不足。Redis是单线程模型INCR/DECR这类命令是原子性的不会出现超卖。很多面试题会问Redis扣减和MySQL扣减不一致怎么办我的实用答案是Redis做前置校验和流量削峰MySQL做最终一致性校验。即使Redis扣减成功但后续订单生成失败你需要一个补偿机制把Redis里的库存加回来或者依赖后台定时任务做库存对账。订单生成这一连串操作一定记得加Transactional事务注解。但这里有个隐蔽的坑事务方法里的Redis操作不会被回滚。也就是说你的事务是按扣MySQL库存 生成订单来保证原子性的Redis扣减操作是独立于事务之外的。所以我会把Redis扣减放在事务方法启动之前或者用Transactional(propagation Propagation.REQUIRES_NEW)把Redis操作单独隔离出去然后人工补偿。这个小细节面试官问到分布式事务的时候你把这番话讲出来含金量直接拉满。5. 常见问题与排查心得5.1 启动与依赖问题先说最常见的端口占用。SpringBoot默认8080端口被占用启动直接报Port 8080 was already in use。Windows下用netstat -ano | findstr 8080找到占用进程的PID然后任务管理器结束进程或者干脆改项目的server.port。这个报错几乎人人都会遇到不用慌。然后是Maven依赖冲突。SpringBoot项目里最常见的报错是ClassNotFoundException或NoSuchMethodError多半是某个依赖版本不对。比如spring-boot-starter-parent里的Spring版本被你手动覆盖成旧版本导致和SpringBoot不兼容。我排查的时候习惯用mvn dependency:tree看依赖树哪个版本不对一目了然。还有经典的Java堆内存溢出报错java.lang.OutOfMemoryError: Insufficient memory。电商项目导出大列表、批量导入数据时很容易触发。临时解决方法是加启动参数-Xms512m -Xmx1024m调整堆内存根本解法是检查代码有没有一次性加载太多数据到内存。分页查询一定用MySQL的LIMIT别图省事全量查出来再在内存里截取。5.2 联调与序列化问题前端访问后端接口最大的坑就是跨域前面已经写了CORS配置。还有一个容易踩的坑是application.yml里配置了server.servlet.context-path: /api前端请求路径写/api/api/xxx路径重复结果404。出现404先看请求路径再看Controller的RequestMapping一步步排查。另一个高发问题是Redis序列化乱码。如果你没配置RedisTemplate的序列化器存进去一个String类型的key用Redis Desktop Manager一看key前面多了一串\xac\xed\x00\x05t\x00之类的乱码前缀。这是因为RedisTemplate默认用JdkSerializationRedisSerializer字符串被序列化成了二进制。解决办法就是自定义RedisTemplatekey用StringRedisSerializervalue用Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer。代码在第3部分给过类似的直接抄。这里提一个和Vue联调时的经典问题后端返回的时间字段比如2024-01-01T12:00:00在前端显示成了UTC格式比实际时间少了8小时。这是Jackson序列化LocalDateTime时没指定时区导致的。解决办法是在application.yml里配置spring.jackson.time-zoneGMT8同时给时间字段加JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)注解。5.3 并发与性能问题项目做完了模拟并发测试时你会发现各种问题。我的建议是用JMeter或者Postman的Runner功能开100个线程同时请求下单接口观察有没有超卖、有没有大量报错。并发测试常见的问题之一是数据库连接池耗尽。报错信息类似HikariPool-1 - Connection is not available, request timed out。原因通常是连接池最大连接数太小或者某个慢SQL把连接占住了。排查思路用MySQL的SHOW PROCESSLIST看当前所有连接在干什么找出长时间执行的SQL加索引优化。同时把HikariCP的maximum-pool-size适当调大比如20到50但别盲目增大连接数太多反而增加数据库压力。还有Redis连接超时的问题。redis.clients.jedis.exceptions.JedisConnectionException或者Lettuce连接池报错。排查方向Redis服务有没有启动、密码是否配置正确、spring.redis.timeout是否太小。我在项目里遇到过一并发测试就报Redis连接超时的情况最后发现是Lettuce默认在并发高时容易触发连接池饿死把连接池的max-active和max-wait调大之后明显好转。性能问题里还有一个隐藏点MySQL分页深翻页。比如LIMIT 100000, 20MySQL需要先扫描10万行再丢弃前10万行效率极差。优化方法是延迟关联SELECT * FROM order WHERE id (SELECT id FROM order ORDER BY id LIMIT 100000, 1) ORDER BY id LIMIT 20;这种写法在面试里只要提一句面试官就会觉得你做过性能调优。5.4 安全与部署那些事电商项目涉及用户数据和交易安全不能忽视。练手阶段不需要上多复杂的方案但至少要会基础防护。接口鉴权我建议用JWT但在SpringBoot里写一个拦截器校验Token是基本功。同时注意放行接口和拦截接口要区分清楚/user/login、/user/register、/product/**商品浏览不需要登录/order/**、/cart/**需要登录。这个逻辑写错了前端会出现登录了还是报401的奇怪问题。参数校验用Validated注解加分组校验至少保证核心字段不为空、格式正确。至于防SQL注入MyBatis的#{}预编译已经帮你挡住了千万注意别在XML里写${}拼接参数那是自毁长城。部署这块练手项目最省事的方案就是打jar包扔服务器上跑。mvn clean package -DskipTests打成可执行jar然后nohup java -jar mall-portal.jar portal.log 21 日志输出到文件里方便排查。要是服务器内存小还可以用-Xmx256m限制一下堆内存。Redis和MySQL的部署我建议顺手学一下Docker Compose。用docker-compose.yml把MySQL和Redis拉起来比在服务器上一条条命令安装省心太多。如果追求高可用可以研究一下docker安装redis主从或者Redis Sentinel哨兵模式但那是进阶内容先把单机跑通再说。还有一个部署槽点我踩过好多次服务器防火墙没放行端口。明明服务已经起来了浏览器就是访问不到。用curl localhost:8080看本地通不通本地通了但外部访问不了八九不离十是防火墙或安全组没放行端口。这个问题排查起来特别简单但特别打击信心。写在最后的一些个人体会这个项目从我第一次写到现在前后重写了三遍。第一遍所有逻辑堆在Controller里一个方法跑完下单全流程。第二遍开始分层但缓存和事务写得一塌糊涂。第三遍才真正理解缓存是系统性能的杠杆这句话——同样一个商品详情接口优化前平均响应200毫秒加了Redis缓存后变成5毫秒这种成就感是背再多面试题都换不来的。如果你正在做这个项目我的建议是别急着一步到位。先把最朴素的CRUD跑通再逐步加缓存、加JWT、加分布式锁。电商项目的价值不在于功能多炫而在于你能在一次次重构里把每个技术点存在的原因想明白。等你把这个项目的每一个模块都能说出为什么这么设计面试的时候你根本不需要背八股文。最后再分享一个我自己的小技巧项目做完了把核心接口的压测结果截图存下来再把你做过的缓存策略、防超卖方案画成图配到简历里。面试官问项目经验时直接掏出这些实证比简历上干巴巴写熟练掌握Redis有说服力得多。本文还有配套的精品资源点击获取