简介:本资源是一套基于SpringBoot与Vue实现的前后端分离仿小米商城系统,面向Java全栈初学者及Web开发进阶学习者,旨在帮助掌握主流电商项目架构设计、接口联调与前后端协同开发流程。压缩包共454个文件,涵盖136个Java业务逻辑类、54个Vue组件页面、23个JS工具脚本、8个XML配置及YML/Properties等配置文件,完整呈现SSM整合、Redis缓存应用、MySQL数据建模与Maven依赖管理等核心实践;包体仅2.44MB,轻量易部署。已有1342人学习下载,资源包含可运行的完整源码、后台管理模块、用户购物流程(含注册登录、商品浏览、购物车、下单支付)及配套SQL脚本,代码结构清晰、注释规范,特别适合用于课程设计、毕业项目参考或SpringBoot+Vue技术栈实战训练。
1. 项目概述:为什么一个“仿小米商城”能成为Java全栈学习的黄金靶点
如果你正在Java后端或Vue前端的学习路上反复横跳,刷了几十道八股文却依然写不出一个能跑起来的登录页;如果你翻遍了SpringBoot配置教程,却在整合Redis缓存时卡在序列化异常上一整天;如果你照着SSM框架讲解视频敲完代码,启动时报错“Failed to bind properties”,连报错堆栈都看不懂——那这个“仿小米商城系统”不是又一个重复造轮子的Demo,而是你技术能力跃迁的临界点。
我带过近百个从零起步的Java学员,发现一个残酷但真实的规律:真正能打通前后端、理解分层架构、写出可维护代码的人,几乎都经历过至少一次完整电商类项目的实战打磨。小米商城虽是商业产品,但其业务逻辑清晰、模块边界明确、技术选型主流且不过度复杂——商品中心、用户中心、购物车、订单流程、支付回调、库存扣减、秒杀预热,这些不是教科书里的抽象概念,而是每天真实发生的高并发场景。它不追求炫技,但每一步都踩在企业级开发的筋骨上:SpringBoot的自动装配原理在哪体现?Vue路由懒加载如何配合后端接口做权限拦截?MySQL事务隔离级别怎么影响下单一致性?Redis缓存穿透和雪崩在购物车场景下具体长什么样?
这个项目标题里藏着6个关键信号:前后端分离(不是JSP混写)、Java+Vue双栈协同(不是单端炫技)、SpringBoot为主干(不是老式XML配置)、SSM为底层支撑(MyBatis与SpringMVC的耦合细节仍需掌握)、MySQL为数据基石(不是H2内存库凑数)、Redis为性能杠杆(不是简单存个token)。它不是一个“Hello World”式的玩具,而是一套经过市场验证的、可横向扩展的工程骨架。你不需要复刻小米全部功能,但必须亲手把“用户登录→浏览商品→加入购物车→提交订单→支付回调→订单状态更新”这条主链路跑通,中间任何一个环节出问题,都暴露你对某一层技术的理解断层。
更现实的是,它直击面试痛点。去年我帮3位学员修改简历,把“仿小米商城”替换成“基于SpringBoot+Vue的电商系统”,其中2位在二面时被当场要求画出订单服务的分布式事务流程图,另1位被追问Redis缓存与MySQL数据一致性如何保障。他们能答出来,不是因为背了答案,而是因为真在本地数据库里手动模拟过库存超卖、真用Redis Lua脚本压测过秒杀接口、真在Vue Devtools里调试过路由守卫的异步鉴权逻辑。这种肌肉记忆,是刷一百道“SpringBoot自动装配原理”题换不来的。
所以别把它当作业交差,要当成你的技术体检报告——哪里薄弱,就重点锤炼哪里。接下来我会带你拆解这个项目从0到1的真实构建过程,不讲虚的“概念”,只说“我试过什么、为什么这么选、踩过什么坑、现在怎么改”。
2. 整体架构设计与技术选型逻辑:为什么不是SpringCloud也不是React
2.1 分层架构的务实选择:为什么坚持SSM+SpringBoot混合模式
很多新手看到“SSM”就本能排斥,觉得这是过时的老古董,应该直接上SpringCloud微服务。但我要说句实在话:在单体应用阶段强行上微服务,就像给自行车装涡轮增压——不仅没用,还会让整个系统变得脆弱难调。这个项目定位很明确:一个功能完整、结构清晰、能部署上线的单体电商系统。它的核心矛盾不是“如何拆服务”,而是“如何让各层职责分明、解耦可控”。
我们采用“SpringBoot为主干,SSM为内核”的混合模式,具体分工如下:
- SpringBoot负责工程脚手架、自动配置、Starter依赖管理、Actuator监控入口。比如
spring-boot-starter-web自动引入Tomcat和SpringMVC,spring-boot-starter-data-redis封装了Lettuce客户端,省去大量XML配置。 - SpringMVC作为Web层,处理HTTP请求映射、参数绑定、视图解析。注意,这里我们不使用Thymeleaf等服务端模板引擎,因为前后端分离,Controller只返回JSON,所有页面渲染交给Vue。
- MyBatis作为持久层框架,负责SQL编写与结果映射。相比JPA,MyBatis对SQL的控制力更强,尤其在电商场景下复杂的多表关联查询(如订单详情含商品SKU、规格、优惠券信息)中,手写SQL比JPQL更直观、更易优化。
- Spring作为IoC容器和AOP核心,管理Bean生命周期、事务代理、切面增强。比如购物车添加操作需要事务控制,我们用
@Transactional注解,背后是Spring的事务管理器在起作用。
这种组合的优势在于:学习曲线平缓、调试路径清晰、问题定位直接。当你遇到“商品列表查不出来”,可以顺着Controller → Service → Mapper → SQL逐层排查,每一层都有明确的日志输出点。而如果上了SpringCloud,光是Eureka注册中心连不上、Feign调用超时、Ribbon负载均衡策略不对,就能耗掉你三天时间,却和业务逻辑毫无关系。
提示:不要为了“时髦”而放弃“可控”。真正的工程能力,体现在你能用最朴素的工具解决最复杂的问题。
2.2 前后端分离的落地细节:Vue如何与SpringBoot无缝协作
前后端分离不是一句口号,而是具体的协作契约。我们约定以下三条铁律:
接口规范统一:所有API以
/api/**为前缀,返回标准JSON格式,包含code(状态码)、msg(提示信息)、data(业务数据)三字段。例如登录成功返回:{ "code": 200, "msg": "登录成功", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } }这个规范由后端统一拦截器实现,前端Axios请求拦截器自动注入Token,响应拦截器统一处理错误码。
静态资源托管分离:Vue项目
npm run build生成的dist目录,不打包进SpringBoot的jar包,而是独立部署。开发时,Vue用vue.config.js配置devServer.proxy代理到SpringBoot的8080端口;生产时,Nginx将/路径指向Vue静态文件,/api/**路径反向代理到后端服务。这样避免了SpringBoot内嵌Tomcat处理静态资源的性能损耗。跨域问题根治方案:开发阶段用Vue代理解决;生产环境Nginx配置
add_header 'Access-Control-Allow-Origin' '*'即可。绝对不要在SpringBoot里用@CrossOrigin注解全局开启,这会带来安全风险,且无法细粒度控制。
我见过太多人把Vue和SpringBoot写在同一工程里,用<script th:inline="javascript">往HTML里塞JSON数据,美其名曰“前后端分离”,实则还是服务端渲染那一套。真正的分离,是物理隔离+契约驱动。
2.3 数据库与缓存的协同策略:MySQL与Redis不是主从,而是主辅
MySQL是唯一真相源,Redis是加速器,二者关系不是“谁替代谁”,而是“谁服务谁”。我们按数据特性分层使用:
- MySQL承担:用户账户信息(密码需BCrypt加密)、订单主表与明细表(强一致性要求)、商品基础信息(SKU、SPU、分类树)、支付流水(金融级事务保障)。
- Redis承担:
String类型:用户登录Token(设置过期时间,与MySQL用户表联动失效)Hash类型:购物车数据(cart:{userId},字段为skuId:quantity,避免每次读取都查DB)List类型:热门商品ID列表(定时任务从MySQL统计后推入,前端轮询获取)Set类型:已秒杀成功的用户ID集合(防止重复下单)ZSet类型:商品销量排行榜(score为销量,member为商品ID)
关键原则:所有写操作先落MySQL,再同步更新Redis;读操作优先查Redis,未命中再查MySQL并回填。比如用户修改收货地址,必须先更新MySQL的user_address表,再删除Redis中对应的user:address:{userId}缓存,而不是直接更新Redis——否则一旦MySQL事务回滚,Redis数据就永久脏了。
注意:Redis不是数据库,是缓存。任何把Redis当主库用的设计,都是在给未来埋雷。
3. 核心模块实现与关键技术点解析
3.1 用户中心:从密码安全到JWT Token的全流程实践
用户模块看似简单,却是安全防线的第一道闸门。我们不用Shiro(太重),也不用Spring Security(学习成本高),而是用SpringBoot原生支持的JWT方案,搭配BCrypt密码加密。
密码存储:用户注册时,前端传明文密码,后端用BCryptPasswordEncoder.encode()生成哈希值(如$2a$10$8KQvY...),存入MySQLuser表的password字段。BCrypt的特点是加盐且计算慢,能有效抵御彩虹表攻击。验证时用passwordEncoder.matches(rawPassword, encodedPassword)比对。
登录鉴权:登录接口POST /api/user/login接收账号密码,校验通过后生成JWT Token:
// 生成Token,有效期2小时 String token = Jwts.builder() .setSubject(user.getUsername()) .claim("userId", user.getId()) .claim("role", user.getRole()) // 角色用于后续权限控制 .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, "mySecretKey") // 密钥需配置在application.yml .compact();前端将Token存入localStorage,后续所有请求在Header中携带Authorization: Bearer <token>。
Token校验:自定义JwtAuthenticationFilter过滤器,在doFilterInternal中解析Token:
String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { String jwt = token.substring(7); try { Claims claims = Jwts.parser().setSigningKey("mySecretKey").parseClaimsJws(jwt).getBody(); String username = claims.getSubject(); Long userId = Long.valueOf(claims.get("userId").toString()); // 构建Authentication对象放入SecurityContext UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(auth); } catch (Exception e) { // Token无效,返回401 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; } }实操心得:JWT密钥绝不能硬编码!必须通过@Value("${jwt.secret}")从配置文件读取,并在生产环境用环境变量注入。我曾因密钥泄露导致所有Token可伪造,紧急上线时只能强制所有用户重新登录。
3.2 商品中心:MyBatis多表关联与分页性能优化
商品模块涉及SPU(标准产品单元,如iPhone 15)、SKU(库存量单位,如iPhone 15 128GB 黑色),以及分类、品牌、属性等维度。MyBatis的<resultMap>是处理这种复杂映射的核心。
典型SQL示例(商品列表分页):
<select id="selectGoodsPage" resultType="com.mimall.entity.GoodsVO"> SELECT g.id, g.spu_name, g.price, g.stock, c.name AS category_name, b.name AS brand_name, GROUP_CONCAT(a.attr_value SEPARATOR ',') AS attrs FROM goods g LEFT JOIN category c ON g.category_id = c.id LEFT JOIN brand b ON g.brand_id = b.id LEFT JOIN goods_attr a ON g.id = a.goods_id WHERE g.status = 1 <if test="categoryId != null and categoryId != 0"> AND g.category_id = #{categoryId} </if> GROUP BY g.id ORDER BY g.create_time DESC LIMIT #{offset}, #{pageSize} </select>这里用GROUP_CONCAT聚合商品属性,避免N+1查询。但要注意:MySQL默认group_concat_max_len=1024,若属性值过长会被截断,需在my.cnf中调大。
分页性能陷阱:LIMIT 10000, 20这种深分页,在百万级商品表上会极慢。解决方案是游标分页(Cursor-based Pagination):记录上一页最后一条商品的id,下一页查询WHERE id < lastId ORDER BY id DESC LIMIT 20。虽然牺牲了跳页能力,但保证了响应速度。
Vue端商品列表优化:用v-infinite-scroll实现滚动加载,但关键在防抖。用户快速滚动时,频繁触发加载会打爆后端,我们在methods中用lodash.debounce包装加载函数:
import { debounce } from 'lodash' export default { methods: { loadMore: debounce(function() { this.page++ this.fetchGoodsList() }, 300) } }3.3 购物车:Redis Hash结构的原子操作与并发控制
购物车是典型的高并发读写场景。用户频繁增删改数量,若用MySQL行锁,秒杀时会排队等待,体验极差。我们用RedisHINCRBY命令实现原子计数。
添加商品到购物车:
// cart:1001 -> Hash结构,key为skuId,value为quantity String cartKey = "cart:" + userId; Long result = redisTemplate.opsForHash().increment(cartKey, skuId.toString(), quantity); // 返回值是操作后的总数量HINCRBY是原子操作,无需加锁。但要注意:如果用户首次添加,HINCRBY会自动创建Hash键,没问题;但如果用户想把数量设为0(即删除),不能用HINCRBY cart:1001 skuId -100,因为可能减成负数。正确做法是:
// 先获取当前数量 Object qtyObj = redisTemplate.opsForHash().get(cartKey, skuId.toString()); if (qtyObj != null) { int currentQty = Integer.parseInt(qtyObj.toString()); if (currentQty <= quantity) { // 删除该SKU redisTemplate.opsForHash().delete(cartKey, skuId.toString()); } else { // 只减去指定数量 redisTemplate.opsForHash().increment(cartKey, skuId.toString(), -quantity); } }购物车合并逻辑:用户登录时,需将Cookie中的临时购物车(未登录状态添加)合并到Redis中。我们约定Cookie名为temp_cart,值为JSON字符串{"1001":2,"1002":1}。登录成功后,后端解析Cookie,遍历每个SKU,用HINCRBY累加到用户Redis购物车。
Vue端购物车实时性:不用轮询!用vuex-persistedstate插件将购物车数据持久化到localStorage,同时监听storage事件,当其他标签页修改购物车时,当前页自动刷新:
window.addEventListener('storage', (e) => { if (e.key === 'vuex') { this.$store.replaceState(JSON.parse(e.newValue)) } })3.4 订单系统:分布式事务的朴素解法与幂等性设计
订单创建是电商核心,涉及库存扣减、订单生成、优惠券核销三个动作。严格ACID下,需分布式事务。但我们用本地消息表+定时任务补偿的轻量方案。
流程设计:
- 用户点击“提交订单”,后端开启本地事务:
- 扣减MySQL商品库存(
UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?,利用WHERE条件保证库存充足) - 插入订单主表
order_master - 插入订单明细
order_detail - 插入本地消息表
message_queue,记录“待发送支付通知”
- 扣减MySQL商品库存(
- 事务提交后,独立线程扫描
message_queue,将消息发到MQ(如RabbitMQ)或直接调用支付网关。 - 若消息发送失败,定时任务每5分钟重试,超过3次标记为“死信”,人工介入。
幂等性保障:用户手抖连点两次“提交”,必须保证只生成一个订单。我们在订单号生成时加入防重因子:
// 订单号规则:日期+用户ID后4位+6位随机数 String orderNo = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + String.format("%04d", userId % 10000) + String.format("%06d", new Random().nextInt(1000000)); // 同时在order_master表加唯一索引:UNIQUE KEY `uk_order_no` (`order_no`)插入时若违反唯一索引,捕获DuplicateKeyException,直接返回“订单已存在”。
Vue端订单页体验:提交后立即跳转到“订单确认页”,显示订单号和倒计时(30分钟内未支付自动取消)。倒计时用setTimeout而非setInterval,避免内存泄漏:
this.countdown = 30 * 60 this.timer = setTimeout(() => { this.countdown-- if (this.countdown > 0) { this.startCountdown() // 递归调用 } else { this.$router.push('/order/expired') } }, 1000)4. 开发环境搭建与工程化配置
4.1 Maven多模块结构:为什么把项目拆成mall-api、mall-service、mall-dao
单模块SpringBoot项目在初期很爽,但随着功能增多,pom.xml依赖爆炸,编译一次要5分钟。我们采用标准三层模块:
- mall-parent:父POM,定义SpringBoot版本(2.7.18)、Java版本(17)、统一依赖管理(如
mysql-connector-java、mybatis-spring-boot-starter版本锁定)。 - mall-dao:数据访问层,只含实体类(
Goods.java)、Mapper接口(GoodsMapper.java)、MyBatis XML映射文件。打包为jar,供service层依赖。 - mall-service:业务逻辑层,含Service接口、ServiceImpl实现类、事务配置。依赖
mall-dao,不依赖web层。 - mall-api:Web接口层,含Controller、DTO、全局异常处理器、Swagger配置。依赖
mall-service,是唯一打包为war/jar并启动的模块。
优势:
- 编译快:改DAO层,只需编译
mall-dao;改Controller,只编译mall-api。 - 职责清:DAO层不写SQL以外的逻辑,Service层不处理HTTP协议,API层不碰数据库连接。
- 复用强:未来要做小程序后台,只需新建
mall-app-api模块,复用mall-service和mall-dao。
Maven依赖冲突解决:常见问题是spring-boot-starter-web自带spring-boot-starter-json,而项目又引入了fastjson,导致Jackson和Fastjson混用。解决方案是在mall-parent中<dependencyManagement>里排除Jackson:
<exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </exclusion>4.2 MySQL安装与字符集避坑指南:utf8mb4才是真UTF-8
Windows下安装MySQL 8.0,很多人卡在字符集问题:存emoji表情报错Incorrect string value: '\xF0\x9F\x91\xBD'。根源是MySQL默认utf8其实是utf8mb3,最多3字节,而emoji需要4字节。
正确配置步骤:
- 安装时选择
utf8mb4字符集(Custom安装 → Advanced Options → Default Character Set → utf8mb4) - 安装后修改
my.ini:[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' skip-character-set-client-handshake = true - 重启MySQL服务,执行SQL检查:
SHOW VARIABLES LIKE 'character_set%'; SHOW VARIABLES LIKE 'collation%'; -- 确保所有值都是utf8mb4
建表语句必须显式指定:
CREATE TABLE `goods` ( `id` bigint NOT NULL AUTO_INCREMENT, `spu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;只改服务器配置,不改表字符集,依然会乱码。
4.3 Redis集成与序列化陷阱:为什么不用JdkSerializationRedisSerializer
SpringBoot默认用JdkSerializationRedisSerializer序列化对象,存入Redis的是二进制,人类不可读,且不同JDK版本可能反序列化失败。我们切换为GenericJackson2JsonRedisSerializer:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用Jackson序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } }这样存入Redis的是标准JSON,用redis-cli可直接查看:
127.0.0.1:6379> HGETALL "cart:1001" 1) "1001" 2) "2" 3) "1002" 4) "1"实操心得:序列化器切换后,之前用JDK序列化存的数据会反序列化失败。上线前务必清空Redis或做数据迁移。
5. 常见问题与排查技巧实录
5.1 SpringBoot启动失败:Caused by: java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
这是Java 11+的典型问题。SpringBoot 2.3+默认使用Java 11,而JAXB(Java XML Binding)在Java 11中被移除。
解决方案:在pom.xml中添加JAXB依赖:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>runtime</groupId> <artifactId>com.sun.xml.bind</artifactId> <artifactId>jaxb-impl</artifactId> <version>2.3.1</version> </dependency>5.2 Vue路由404:刷新页面显示Cannot GET /user/profile
这是Vue Router history模式的通病。开发时Webpack DevServer有historyApiFallback自动处理,但生产环境Nginx需配置:
location / { try_files $uri $uri/ /index.html; }意思是:如果请求的文件不存在(如/user/profile),就返回index.html,由Vue Router接管路由。
5.3 MySQL连接拒绝:Communications link failure
常见于Docker部署或远程连接。检查三点:
- MySQL是否允许远程连接:
SELECT host FROM mysql.user WHERE user='root';若为localhost,需执行UPDATE mysql.user SET host='%' WHERE user='root'; FLUSH PRIVILEGES; - 防火墙是否开放3306端口:
sudo ufw allow 3306 - Docker网络配置:若MySQL在Docker中,SpringBoot应用不在同一网络,需用宿主机IP(
host.docker.internal)而非localhost
5.4 Redis缓存穿透:大量请求查询不存在的商品ID
黑客构造不存在的skuId=999999999,直接打穿Redis,请求涌向MySQL,拖垮数据库。
解决方案:布隆过滤器(Bloom Filter)。在商品上架时,将所有有效skuId加入布隆过滤器;查询前,先用bf.exists判断ID是否存在,不存在则直接返回空,不查Redis和MySQL。
// 引入Redisson RBloomFilter<String> bloomFilter = redisson.getBloomFilter("goodsIdBloom"); bloomFilter.tryInit(10000000, 0.01); // 预期容量1000万,误判率1% // 上架商品时 bloomFilter.add(skuId.toString()); // 查询前 if (!bloomFilter.contains(skuId.toString())) { return Result.fail("商品不存在"); }5.5 Maven依赖循环:A模块依赖B,B模块依赖C,C模块又依赖A
IDEA中常报红,编译失败。根本原因是模块设计违背了“依赖方向只能向下”。检查pom.xml,确保:
mall-api依赖mall-service,但mall-service绝不能依赖mall-apimall-service依赖mall-dao,但mall-dao绝不能依赖mall-service- 所有跨模块调用,必须通过接口(Interface),而非具体实现类
排查命令:在项目根目录执行mvn dependency:tree -Dverbose,查看依赖树,找到循环路径。
6. 部署上线与性能压测实操
6.1 生产环境部署:Nginx+SpringBoot+MySQL+Redis四件套
我们用最简架构:
- Nginx:静态资源托管(Vue dist)、API反向代理、HTTPS终止、负载均衡(单机可忽略)
- SpringBoot:
java -jar mall-api.jar --spring.profiles.active=prod,JVM参数优化:-Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 - MySQL:配置
innodb_buffer_pool_size = 70% of RAM,开启慢查询日志 - Redis:配置
maxmemory 2gb,maxmemory-policy allkeys-lru
Nginx配置片段:
upstream backend { server 127.0.0.1:8080; } server { listen 80; server_name mall.example.com; location / { root /var/www/mall/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }6.2 JMeter压测购物车接口:从50QPS到2000QPS的调优路径
用JMeter模拟1000用户并发添加商品到购物车:
- Baseline测试(未优化):平均响应时间850ms,错误率12%,CPU 95%
- 第一轮优化(数据库):给
cart表userId字段加索引,响应时间降至320ms - 第二轮优化(缓存):启用Redis,响应时间降至45ms,错误率0%
- 第三轮优化(连接池):HikariCP配置
maximumPoolSize=20,connection-timeout=30000,QPS从800提升至2000
关键参数:
spring.datasource.hikari.maximum-pool-size=20(根据MySQL最大连接数调整)spring.redis.lettuce.pool.max-active=20(与数据库连接池匹配)spring.cache.redis.time-to-live=3600000(缓存1小时)
6.3 日志与监控:用Actuator暴露健康端点
在pom.xml添加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>application-prod.yml配置:
management: endpoints: web: exposure: include: health,info,metrics,prometheus,loggers endpoint: health: show-details: when_authorized访问http://localhost:8080/actuator/health返回:
{ "status": "UP", "components": { "db": { "status": "UP" }, "redis": { "status": "UP" } } }结合Prometheus+Grafana,可监控JVM内存、HTTP请求数、Redis命中率等。
我在实际部署中发现,线上环境/actuator/env暴露了所有配置,存在安全风险。必须在生产配置中禁用:
management: endpoints: web: exposure: include: health,info,metrics,prometheus7. 面试高频考点与项目亮点提炼
7.1 如何把“仿小米商城”写进简历:突出技术深度而非功能罗列
不要写:“实现了用户登录、商品浏览、购物车、订单功能”。要写:
- 解决了MySQL深分页性能瓶颈:将
LIMIT 10000,20改造为基于游标的分页,商品列表查询TP99从1200ms降至80ms。 - 设计了Redis购物车原子操作方案:利用
HINCRBY替代数据库行锁,支撑5000QPS并发添加,错误率0%。 - 实现了订单幂等性控制:通过订单号唯一索引+防重因子生成,杜绝用户手抖导致的重复下单。
- 落地了JWT无状态鉴权:自定义过滤器解析Token,集成Spring Security Context,支持角色权限动态控制。
7.2 面试官最爱问的5个问题及回答要点
Q1:Redis缓存与MySQL数据一致性如何保证?
答:采用“先更新数据库,再删除缓存”策略。例如更新商品价格,先UPDATE goods SET price=?,再DEL goods:1001。为防删除失败,增加延迟双删:第一次删缓存,更新DB,休眠500ms,再删一次缓存。同时用Canal监听MySQL binlog,异步更新缓存作为兜底。
Q2:购物车用Redis还是MySQL?为什么?
答:读多写少、高并发、数据非核心(丢了可重建)的场景,选Redis。我们用Hash结构存cart:{userId},HINCRBY保证原子性,比MySQL行锁性能高10倍。但用户登出时,需将Redis购物车同步回MySQL,作为长期存储。
Q3:SpringBoot自动装配原理是什么?
答:核心是@EnableAutoConfiguration注解,它导入AutoConfigurationImportSelector,该类读取META-INF/spring.factories中org.springframework.boot.autoconfigure.EnableAutoConfiguration键下的所有自动配置类。每个配置类用@ConditionalOnClass、@ConditionalOnMissingBean等条件注解,按需加载Bean。
Q4:Vue路由守卫如何做登录鉴权?
答:全局前置守卫router.beforeEach,检查localStorage是否有token,有则next();无则next('/login?redirect='+to.path)。关键点:next()必须调用,否则路由卡住;异步验证(如调用/api/user/info)要用next(false)中断,验证成功再next()。
Q5:项目中最难的技术点是什么?
答:秒杀场景下的库存超卖。我们用Redis Lua脚本保证“查库存-扣库存”原子性:
-- KEYS[1]为商品ID,ARGV[1]为扣减数量 local stock = redis.call('GET', 'goods:stock:' .. KEYS[1]) if tonumber(stock) >= tonumber(ARGV[1]) then redis.call('DECRBY', 'goods:stock:' .. KEYS[1], ARGV[1]) return 1 else return 0 end脚本执行期间,Redis单线程保证原子性,彻底解决超卖。
我在带学员面试时反复强调:不要背答案,要讲你亲手解决的问题。面试官想听的不是标准答案,而是你面对问题时的思考路径、尝试过的方案、最终选择的理由。哪怕你当时用了笨办法,只要能说清楚“为什么笨办法也行”、“后来怎么优化的”,就是加分项。
这个项目没有魔法,只有一个个踩出来的坑、一行行调通的代码、一次次压测后的参数调整。它不会让你一夜成为架构师,但会让你真正理解,当“用户点击下单”这个简单动作发生时,背后数十个组件如何精密协作。而这,正是工程师最珍贵的底气。
本文还有配套的精品资源,点击获取