Spring Boot电商项目实战:从SSM整合到Redis缓存与JWT认证
简介:前后端分离架构是现代Web开发的标配,通过解耦前端展示与后端逻辑,既支持多端复用,也便于团队并行开发。Spring Boot作为Java后端的主流框架,以自动配置简化SSM整合,搭配MyBatis完成数据持久化;Redis凭借内存级读写性能,常被用于商品缓存、分布式锁等高频场景。基于这套技术栈构建的电商系统,天然覆盖用户认证、商品管理、订单一致性、库存扣减等核心业务,是验证后端工程能力的经典项目。本文从技术选型底层逻辑讲起,依次拆解数据库设计、接口规范、JWT无状态登录、缓存旁路模式、下单事务与防超卖实现,并针对缓存穿透、跨域拦截、MySQL时区等实战高频问题给出排查方案。无论是毕业设计还是简历项目,这套完整实践路径都值得参考。 在Java后端这个方向上,电商项目永远是最经典、最不会出错的练手选题。原因很简单:一个像样的电商系统几乎覆盖了后端开发需要面对的所有核心问题——用户认证、商品管理、高并发查询、库存扣减、订单一致性、缓存策略、前后端联调。你把这个项目吃透了,去面试Java岗位时提到的技术点基本都有真实场景支撑,而不是背八股文。
下面我会从技术选型的底层逻辑说起,把整个网上电商项目的模块拆解、核心代码实现、Redis在其中的真实用途,以及我在实际开发中踩过的坑,一次性讲清楚。不管你是拿它做毕业设计,还是作为求职简历上的项目经验,这篇文章都可以直接照着实践。
1. 项目整体设计与技术选型思路
1.1 前后端分离的本质:为什么不用JSP而用Vue
很多同学在学校里做的电商项目还是JSP + Servlet那套,页面和后端代码揉在一起。这个项目明确写了前后端分离,意味着前端静态资源和后端业务逻辑彻底分开部署。前端通过Ajax或Fetch调用后端提供的JSON接口,后端不再返回HTML页面,只处理数据。
这个转变带来的直接好处是:前端和后端可以并行开发,互不阻塞。你在写Spring Boot接口的时候,前端同学(或者你自己的Vue部分)可以用Mock数据先把页面搭起来,等接口联调时再替换成真实请求。而且前后端分离之后,后端接口可以同时服务Web端、小程序端甚至App端,一套接口多端复用。
选Vue而不是React或Angular,核心原因是Vue的上手曲线更平缓。它的模板语法接近原生HTML,数据绑定用双大括号{{ }}包裹,事件绑定用@click,指令系统v-if、v-for非常直观。对于一个以Java为主、前端为辅的开发者来说,Vue是投入产出比最高的选择。
1.2 Spring Boot + SSM的关系:它们不冲突,是进化
这个项目标题里同时写了Spring Boot和SSM,很多初学者会困惑:这不冲突吗?其实不冲突。SSM是Spring + Spring MVC + MyBatis的组合,而Spring Boot并不是替代这三个组件的新框架,它是用来简化Spring全家桶配置的工具。本质上,你在Spring Boot项目里用的还是Spring的IOC容器、Spring MVC的注解式控制器、MyBatis的Mapper持久层,只是省掉了大量XML配置。
传统SSM项目需要配置web.xml、spring-mvc.xml、mybatis-config.xml、数据源、事务管理器,一个环境配半天。Spring Boot通过自动配置机制,把这些默认行为都封装好了,你只需要在application.yml里写数据源地址,MyBatis的SqlSessionFactory就会自动创建,Spring MVC的DispatcherServlet会自动注册。
所以这个项目的技术栈理解为一句话:Spring Boot做骨架,Spring MVC做Web层,MyBatis做持久层,这就是SSM在Spring Boot时代的标准形态。
1.3 Maven在项目中的真实角色:不只是依赖管理
Maven的作用经常被低估。很多人觉得Maven就是pom.xml里粘贴依赖的,其实它承担了三件关键事。
第一是依赖仲裁。电商项目至少需要十几个依赖:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、spring-boot-starter-data-redis、jjwt、lombok等等。Maven能自动拉取这些依赖的传递依赖,并且解决版本冲突。比如spring-boot-starter-parent这个父POM统一锁定了所有Spring生态组件的版本,你不用自己操心Spring Boot 2.7和Spring Framework 5.3是否兼容。
第二是项目生命周期管理。mvn clean package一行命令就能完成编译、测试、打包,生成可执行的jar包。部署到服务器上就是java -jar mall.jar,非常干净。
第三是多模块工程支持。如果项目规模变大,可以把common、pojo、mapper、service、controller拆成多个Maven模块,模块之间用坐标依赖。虽然这个课程级别的项目不需要拆这么多模块,但用Maven管理结构,后面扩展会很方便。
1.4 MySQL与Redis的搭配逻辑:各司其职
MySQL负责持久化核心数据——商品信息、用户信息、订单记录、库存流水。它的强项是事务一致性,ACID特性保证订单和库存不会出错。
Redis负责处理高频访问的数据——商品详情缓存、首页轮播图缓存、验证码、购物车、分布式锁。它的强项是读写速度快,因为数据存储在内存中,单机QPS可以轻松过万。
两者搭配的逻辑是:读请求先查Redis,查不到再查MySQL,把查到的结果回填到Redis;写请求直接操作MySQL,然后主动删除或更新Redis缓存。这套缓存旁路模式是电商系统的性能基石,后面我会详细讲代码实现。
2. 数据库设计与核心模块拆解
2.1 数据库表设计:电商项目到底需要几张表
我设计这个项目时,一共建了8张核心表。这里直接给出建表要点,你可以根据自己的需求增删。
| 表名 | 作用 | 关键字段 | 备注 |
|---|---|---|---|
user | 用户表 | id、username、password、nickname、phone、avatar、create_time | password存MD5后加盐的值 |
category | 商品分类表 | id、name、parent_id、sort_order | 支持两级分类 |
product | 商品表 | id、category_id、name、subtitle、main_image、detail、price、stock、status | status控制上下架 |
cart | 购物车表 | id、user_id、product_id、quantity、checked | 一个用户对应多个商品记录 |
address | 收货地址表 | id、user_id、receiver_name、receiver_phone、province、city、district、detail | 可设置默认地址 |
order | 订单主表 | id、order_no、user_id、total_price、status、receiver_info、create_time | status:待付款/已付款/已发货/已完成/已取消 |
order_item | 订单明细表 | id、order_no、product_id、product_name、product_image、current_price、quantity、total_price | 冗余商品快照信息 |
cart也可以直接用Redis实现 |
商品表里有两个字段容易被忽略:subtitle是商品副标题,用于列表页展示;detail是富文本详情。订单明细表里的product_name、product_image、current_price是故意冗余的,因为商品改名或调价后,订单里必须保留下单那一刻的信息,不能随着商品表变动。
2.2 索引设计:别等数据量大了再后悔
小项目可以不在乎索引,但电商场景商品表几十万条数据很正常,这时候索引就是生命线。
user表的username要建唯一索引,因为登录需要按用户名查询,而且必须保证用户名不重复。product表的category_id建普通索引,支撑分类查询。order表的user_id和order_no都要建索引,前者用于"我的订单"列表,后者用于订单查询接口。
订单主表的数据量增长最快,建议按月分表,但这超出了基础项目的范畴。我做完这个项目后的体会是:建表时把索引设计好,比后续发现慢查询再优化SQL要省心得多。
2.3 功能模块划分:从用户侧到管理侧
整个电商系统的功能可以拆成两大端。用户端是C端买家使用的,包括注册登录、浏览商品、搜索商品、购物车、下单支付(这里用模拟支付)、查看订单、收货地址管理。管理端是B端运营使用的,包括分类管理、商品上下架、订单发货处理。
前后端分离后,管理端和用户端是两套独立的前端工程,但它们调用同一套后端接口。为了权限隔离,后端用Spring MVC拦截器对请求路径做白名单放行和Token校验,区分用户角色。
3. 核心实现环节:从配置到代码,一步步搭起来
3.1 项目搭建与核心配置
用IDEA新建Spring Boot项目时,直接选择Spring Initializr,Group填com.mall,Artifact填mall,Java版本用8或11都行,Spring Boot版本建议用2.7.x。为什么不用3.x?因为Spring Boot 3要求JDK 17,并且javax包名改成了jakarta,很多网上教程的代码拿过来直接会报包名错误。对于学生项目,2.7.x生态最成熟,遇到的坑最少。
pom.xml里的核心依赖如下,直接抄即可:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>application.yml文件里最需要注意的是MySQL连接串,MySQL 8.x驱动要求指定serverTimezone,不然会报时区错误。Redis默认端口6379,本地装了Redis服务端后直接连即可:
spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.mall.pojo configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case: true这个配置特别重要。数据库字段是create_time,Java属性是createTime,开启这个选项后MyBatis会自动映射,不用手写ResultMap。
3.2 前后端分离的接口规范与跨域处理
前后端分离开发中,接口约定是重中之重。我采用的是RESTful风格,返回体统一封装。
定义统一返回对象时要使用泛型,这样强类型语言里的类型转换是安全的:
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }接口写好后,前端项目跨域访问后端是必然遇到的问题。浏览器出于安全策略会阻止跨域请求,解决方案是在Spring Boot里配置全局跨域:
@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); } }注意这里的坑:如果用了自定义拦截器做Token校验,OPTIONS预检请求会被拦截器拦住。因为浏览器在正式请求前会先发一个OPTIONS请求探测服务器是否允许跨域,这个请求不带Token,校验必然失败。所以拦截器里必须直接放行OPTIONS请求。
3.3 用户认证:JWT的无状态登录设计
电商系统的用户登录状态不能依赖Session,因为前后端分离后,后端服务可能是多实例部署的,Session在A机器上创建、B机器上就找不到。Redis可以用来解决Session共享问题,但更现代的做法是直接用JWT。
我选择用一个独立工具类来处理JWT的生成与解析:
public class JwtUtil { private static final String SECRET_KEY = "mall-secret-key"; private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; public static String generateToken(Integer userId, String username, Integer role) { return Jwts.builder() .claim("userId", userId) .claim("username", username) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody(); } }JWT的机制是:用户登录成功后,后端生成一个包含用户信息的签名字符串返回给前端;前端把Token存在localStorage里,每次请求在请求头加上Authorization: Bearer <token>;后端拦截器解析Token得到用户信息,放行请求。因为JWT自带签名,服务端不需要保存任何会话状态。
实际操作中要注意几个细节。Token放在请求头而不是URL参数中,避免被浏览器历史记录或服务器日志泄漏。用自定义注解@RequireLogin加在需要登录的Controller方法上,拦截器判断方法上是否有这个注解,比按路径匹配灵活得多。JWT的密钥要配置在环境变量或者配置中心,不要硬编码提交到Git仓库,否则等于把用户登录凭证交给了别人。
3.4 商品查询与Redis缓存的接入
商品详情和首页数据是访问量最大的接口,不能每次都查MySQL。我在这里演示一个商品详情接口的缓存实现。为了不侵入业务代码,使用Spring AOP或者直接用简单的缓存逻辑都行:
@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Autowired private StringRedisTemplate stringRedisTemplate; private static final long CACHE_TTL = 30 * 60; @Override public ProductVO getProductDetail(Integer productId) { String cacheKey = "product:detail:" + productId; String cacheValue = stringRedisTemplate.opsForValue().get(cacheKey); if (StringUtils.hasText(cacheValue)) { return JSON.parseObject(cacheValue, ProductVO.class); } ProductVO product = productMapper.selectDetailById(productId); if (product != null) { stringRedisTemplate.opsForValue() .set(cacheKey, JSON.toJSONString(product), CACHE_TTL, TimeUnit.SECONDS); } return product; } }这里有几个关键点。Redis存的必须是JSON字符串,所以用StringRedisTemplate,它默认的序列化器就是String,不会出现乱码问题。缓存Key要设计成业务:类别:唯一标识的格式,方便后续清理。过期时间一定要给,避免商品下架后缓存还活着。另外,存进去之前要判断product != null,不然空值会被缓存,形成缓存穿透。
还有一个容易被忽视的问题:更新商品信息后必须主动删除缓存,而不是等待缓存过期。在更新商品的Service方法里加一行redisTemplate.delete("product:detail:" + productId),这样才能保证用户看到的信息是最新的。
3.5 购物车实现:选MySQL还是Redis
购物车有两种实现方式。未登录状态下用前端的localStorage存储,但是换浏览器或者清缓存就丢失了,体验不好。我建议做成登录后同步到Redis,这样换设备购物车也能同步。
Redis实现购物车非常顺手,用Hash结构,key是cart:{userId},field是商品ID,value是数量。每次加购就是一条HSET命令,查询购物车就是HGETALL,性能极高。但如果要在购物车里展示商品的价格和图片,Redis里只存了商品ID和数量,还需要批量查询MySQL拿到商品详情。
所以实际项目中我更推荐MySQL存储购物车,表结构是userId、productId、quantity、checked。原因很简单:购物车涉及商品价格变化,在用户结算时要实时验证库存和价格,放在MySQL里和商品表做关联查询更方便。对于课程设计来说,MySQL版的购物车逻辑更清晰,而且能写出CRUD的完整代码,面评时有的讲。
3.6 下单扣库存:事务与并发控制
下单是整个项目中逻辑最复杂的环节,需要同时操作订单表、订单明细表、库存扣减,三步必须保证原子性。用Spring的@Transactional注解实现事务管理:
@Override @Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Integer userId, List<CartItemVO> cartItems, Integer addressId) { // 1. 生成订单号,格式:时间戳 + 用户ID + 随机数 String orderNo = generateOrderNo(userId); // 2. 计算订单总额 BigDecimal totalPrice = new BigDecimal("0"); // 3. 扣减库存(含乐观锁防超卖) for (CartItemVO item : cartItems) { int result = productMapper.deductStock(item.getProductId(), item.getQuantity()); if (result == 0) { throw new RuntimeException("商品库存不足:" + item.getProductName()); } // 累计价格、写入明细... } // 4. 插入订单主记录 // 5. 清空购物车 // 6. 返回订单数据 }防超卖的悲观锁可以在SQL里用SELECT ... FOR UPDATE,但对这个场景有点重。我采用乐观锁方案:商品表的stock字段上加上version或者直接在更新SQL里加库存条件判断。
问题在于:执行下单操作时Redis分布式锁已经在这里没用了——答案是有用,而且不同级别对应不同方案。你如果只做最基础的版本,上面的乐观锁就够;但如果考虑到一个用户频繁点击提交订单按钮,就需要在入口处加一个防重复提交的分布式锁。我在项目里结合Redis实现了一个简单版本:
public boolean tryLock(String lockKey, String requestId, long expireTime) { return stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS); } public void unlock(String lockKey, String requestId) { String value = stringRedisTemplate.opsForValue().get(lockKey); if (requestId.equals(value)) { stringRedisTemplate.delete(lockKey); } }setIfAbsent是原子操作,多个线程同时请求时只有一个能获取锁。requestId的作用是防止误删别人的锁,释放前先判断是不是自己加的。这个锁用在同一个用户短时间内的重复下单请求上,锁的Key设计成lock:order:{userId},过期时间设1秒就足够了。
3.7 Vue前端工程与接口联调
Vue部分用Vue CLI或Vite创建工程,推荐使用Vite,启动速度比Webpack快很多。脚手架初始化命令:
npm create vue@latest mall-web前端最重要的工作是封装axios请求实例。统一处理BaseURL、Token注入、响应码拦截:
import axios from 'axios' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) }, error => { return Promise.reject(error) } ) export default request这里解决了前后端联调中最常见的两个问题。一个是Token统一注入,不用在每个请求方法里手动带Token。另一个是401统一跳登录,当Token过期时自动踢回登录页。
跨域问题在开发环境下可以用Vite的代理配置解决,不用每次都去改后端CORS配置:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } }Vue路由配置时使用路由懒加载,组件按需加载,减少首屏白屏时间:
const routes = [ { path: '/', component: () => import('@/views/Home.vue') }, { path: '/product/:id', component: () => import('@/views/ProductDetail.vue') }, { path: '/cart', component: () => import('@/views/Cart.vue'), meta: { requiresAuth: true } }, { path: '/order/confirm', component: () => import('@/views/OrderConfirm.vue'), meta: { requiresAuth: true } } ] router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !localStorage.getItem('token')) { next('/login') } else { next() } })4. 实战中的常见问题与排查技巧
4.1 MySQL时区与连接问题
连接MySQL时最容易碰到的是The server time zone value 'Öйú±ê׼ʱ¼ä'这样的乱码时区错误。解决方案两种:MySQL命令行执行set global time_zone = '+8:00'修改数据库时区,或者在JDBC连接串里加serverTimezone=Asia/Shanghai。我建议两种都做,因为只改连接串,数据库服务器本身的时区仍然是错的,后面做时间统计、定时任务时也会踩坑。
MySQL 8.x还要注意使用com.mysql.cj.jdbc.Driver而不是旧的com.mysql.jdbc.Driver,否则会直接报ClassNotFound。
4.2 Redis连接不上或启动闪退
本地装了Redis Desktop Manager连不上Redis时,先检查服务端是否真正启动了。Windows环境下Redis服务端默认是前台运行,关掉窗口就会停。用命令redis-server --service-install redis.windows.conf注册成Windows服务,再用net start redis启动,这样就不会被误关了。
Redis 5.0以上版本直接运行redis-server.exe接redis.windows.conf时,配置文件里没有appendonly yes配置的话,启动后数据不会持久化。开发阶段可以不关持久化,但部署前一定要开启AOF或RDB中的至少一种。
还有一个小坑:新版的Redis默认绑定127.0.0.1,并且需要密码认证。本地开发没问题,如果要部署到云服务器,记得修改bind 0.0.0.0和设置requirepass。但注意不要使用明文弱密码,否则服务器会被扫描爆破,这已经是重灾区。
4.3 Spring Boot项目启动报端口被占用
开发阶段最常见的错误是Port 8080 was already in use。排查方法:
netstat -ano | findstr 8080 taskkill /f /pid 对应进程号如果是多个Spring Boot实例同时启动,也可以直接改端口号:
server: port: 90904.4 缓存穿透、击穿、雪崩的应对
当Redis缓存失效后,大量请求同时打进数据库,会造成缓存雪崩。解决办法有几个层次。最简单的给过期时间加随机偏移量,比如基础30分钟加上0到5分钟的随机值,避免同一时刻集体失效。加锁的方案是使用互斥锁,缓存失效时只放行一个线程去查数据库,其他线程等待后重新查缓存,上面的Redis分布式锁在这里也可以复用。
缓存穿透是查询一个根本不存在的数据,每次都会落到数据库。我前面代码里已经提到了预防方法:写入时判断空值,把空值也缓存起来,并且设置较短的过期时间。更严谨的办法是在Controller入口使用布隆过滤器,先用布隆过滤器判断商品ID是否存在,不存在直接返回,但这需要额外的容器维护,课程设计阶段可以先不加。
4.5 前后端联调时的跨域和404
联调时最常见的问题就是前端请求404。排查步骤固定:直接打开浏览器开发者工具,查看Network面板中请求的URL和状态码。如果请求到达了后端但返回404,大概率是路径不对,检查Controller的@RequestMapping和前端请求路径是否完全一致。如果请求根本没到达后端就返回404,检查前端代理配置或后端CORS配置。
另一个老大难问题是POST请求返回403或405,通常是Spring Security拦截了CSRF攻击。如果项目没引入Spring Security却出现403,检查是不是自定义拦截器拦掉了,或者前端发送的Content-Type是application/json但后端接口参数用@RequestParam接收,Spring MVC解析不到参数也会报400。
4.6 数据库并发下的死锁
多用户同时购买同一款商品时,可能触发数据库死锁。死锁场景通常发生在事务中多条SQL语句对资源的锁定顺序不一致。比如A事务先更新商品表再插入订单表,B事务先插入订单表再更新商品表,两边相互等待就死锁了。
排查方法是用SHOW ENGINE INNODB STATUS查看死锁日志,看最后等待的资源。解决的思路是要保持所有事务按照相同的顺序访问资源,先锁商品再插入订单。还有就是缩短事务执行时间,不要在事务里调用外部接口、发送短信等耗时操作,这些应该放到事务外异步执行。
5. 项目复盘:做完这个电商项目后我的几点体会
5.1 这个项目在简历上应该怎么写
很多同学项目做完了,简历上只写一句"基于Spring Boot和Vue的电商系统",这等于白做。你需要把项目拆成可量化的描述,重点突出你解决过的问题,而不是罗列技术名词。
我建议的写法是:项目描述部分写清楚系统的功能范围(前台商品展示、购物车、订单管理、后台管理系统),技术架构部分写清前后端分离、Spring Boot + MyBatis + Redis + Vue。然后单独列出你最有亮点的工作,比如"设计并实现了基于Redis的商品详情缓存,缓存命中率达到xx%"、"使用Redis分布式锁解决了订单并发下的库存超卖问题"、"基于JWT实现无状态登录认证,支持跨域访问"。这些描述才真正能引起面试官的兴趣。
5.2 关键技术点如何应对面试提问
面试官看到这个项目,大概率会追问几个方向。Redis相关的问题是最多的,你要能讲清楚:Redis为什么快(纯内存操作、单线程避免了锁竞争、IO多路复用)、缓存和数据库的一致性如何保证(先更新数据库再删缓存)、缓存穿透和击穿的区别及应对方案。
Spring Boot相关的问题会集中在自动配置原理上。你要能说清楚@SpringBootApplication组合了哪几个注解,@EnableAutoConfiguration是怎么通过META-INF/spring.factories加载自动配置类的。这个知识点是Java后端面试的高频考点,建议专门花时间深入一下。
MySQL相关的问题集中在索引和事务隔离级别。你要能说出聚簇索引和二级索引的区别、explain执行计划怎么分析、InnoDB的默认隔离级别为什么是可重复读以及如何处理幻读。
5.3 线上环境与本地开发的差异
如果这个项目要部署到线上,有几件事必须提前考虑。本地用的MySQL账号是root,线上必须创建独立的应用程序账号,只授予业务库的最小权限。Redis要设置密码,并且不能使用默认端口。配置文件里的数据库密码、Redis密码不能写死在application.yml里,可以用环境变量注入,或者使用Spring Cloud Config、Nacos这类配置中心。
打包部署的时候,前端Vue项目执行npm run build会生成dist目录,把dist里的静态文件交给Nginx托管,Nginx配置反向代理,把/api路径的请求转发到后端的Java服务。这样一个标准的全栈部署就完成了。实际部署踩过几次坑之后,我才真正理解为什么前后端分离项目要用Nginx这种Web服务器来托管静态资源、做请求转发和负载均衡。
5.4 从课程设计到生产级项目的差距
做完这个项目只是第一步。如果要往生产级方向发展,你还需要考虑很多当前没做的东西:秒杀场景下的队列削峰、订单超时自动取消的定时任务、消息队列做订单创建和库存扣减的解耦、分布式事务的最终一致性方案、ELK日志收集、Prometheus监控报警。
但我是比较务实的人,对于一个毕业生或者转行的开发者来说,**把基础版本吃透,能在面试中讲清楚每个技术点的来龙去脉,比在简历上堆砌分布式微服务更有说服力。**先把这版做出来,跑起来,出了问题能定位,这就是实打实的能力。
我个人的建议是,做完这个项目后,选一个方向做深做实。比如往高并发方向扩展,用JMeter压测商品详情接口,找出性能瓶颈,然后有针对性的优化缓存策略和SQL;或者往工程化方向扩展,写单元测试、写接口文档、加日志链路追踪。任何一个方向的深入,都会让你的简历在几十份候选者里脱颖而出。
本文还有配套的精品资源,点击获取
