做计算机设计类项目,最怕的就是选题“看着简单,做起来全是坑”。很多同学一上来就选商城、选管理系统,结果答辩时被打断:你的并发怎么处理?库存怎么保证不超卖?秒杀接口怎么做限流?当场愣住。反过来,如果你把题目聚焦到“抢购”这个具体的高并发场景,再配上SpringBoot和Vue这套主流技术栈,项目含金量和答辩说服力完全不一样。这篇博文就围绕“基于SpringBoot与Vue的数码产品抢购系统”展开,把从选题拆解、数据库设计、后端并发控制、前端交互到万字文档撰写的完整链路讲清楚,项目里带全套源码,适合计算机专业毕业设计、课程设计,也适合想系统学习前后端分离开发的同学。
我做这类抢购项目已经不是第一次了,早些年用SSH(Struts2+Spring+Hibernate)写过一版,后来换到SpringBoot + Vue重构,中间踩过的坑不少,比如库存超卖、重复下单、倒计时被客户端篡改、Redis连接池被打爆等等。这篇博文我会把关键问题和对应解法全部整理出来,尽量做到“文档里能写清楚,代码里能跑得通,答辩时能讲得明白”。
1. 抢购项目到底在考验什么
1.1 别把它当成普通CRUD
很多同学会觉得,抢购系统不就是商品表加订单表,用户点一下“立即抢购”,后台insert一条订单,update一下库存,完事。如果只是这个程度,那这个项目毫无竞争力,顶多算增删改查练习。
抢购系统的核心特征是“瞬时高并发”。举个例子,某个数码产品限量50台,开抢时间一到,可能几千甚至上万用户同时点击。这个时候,如果代码还是简单的先查库存、再减库存、再生成订单,在并发场景下一定会出现“超卖”,也就是明明只剩1台,结果10个人都下单成功。
所以,抢购系统真正考验的是三件事:
- 库存扣减的原子性:多人同时操作时,库存数值不能错乱。
- 接口的幂等性:同一个用户反复点击抢购按钮,只能成功一次。
- 系统整体的削峰能力:短时间高流量不能直接把数据库打崩。
这一套东西,正好是面试和答辩时最容易追着问的技术点。也就是说,这个项目的价值不在于“做了哪些功能”,而在于“如何在高并发下保证数据正确和系统稳定”。
1.2 为什么选SpringBoot + Vue
当前企业级前后端分离开发中,SpringBoot + Vue已经是绝对主流,选这个组合做计算机设计项目,好处非常明显。
后端用SpringBoot,极大简化了Spring的配置流程。以前用SSH或SSM,光XML配置就要写一大坨,新手光搞环境就得折腾两三天。SpringBoot通过自动配置和Starter依赖,真正做到了“开箱即用”,把精力花在业务逻辑而不是环境搭建上。而且SpringBoot生态极其成熟,集成Redis、RabbitMQ、MyBatis-Plus等组件都有非常成熟的方案。
前端用Vue,单页应用开发体验好,组件化结构让代码维护更轻松。搭配Vue Router做页面跳转,Vuex或Pinia做状态管理,Axios做HTTP请求,Element UI或Ant Design Vue做界面组件,整个前端工程质量很高。抢购场景下,Vue的响应式机制处理倒计时、按钮状态切换等交互也特别顺手。
这个组合还有一个实际好处:市面上资料极多,真遇到问题搜索一下基本都能找到答案。对做项目的同学来说,技术栈过于冷门意味着遇到坑没人帮你踩,而SpringBoot + Vue的社区资源是最大的保障。
2. 系统整体设计与数据库建模
2.1 功能模块怎么划分
数码产品抢购系统,我的建议是分成两个端:用户端和管理端。这样既展示了完整的业务闭环,又能在答辩时体现“系统思维”。
用户端的核心功能包括:
- 注册登录:手机号或用户名注册,登录后才有抢购资格。
- 商品列表:展示数码产品,比如手机、耳机、智能手表、相机等。
- 商品详情:展示商品介绍、库存量、抢购场次。
- 抢购倒计时:未开始时显示倒计时,开始时按钮变为可点击。
- 立即抢购:点击后走完整的下单流程。
- 订单列表:查看自己已抢到的订单。
- 订单状态:待支付、已支付、已取消。
管理端(后台管理)的核心功能包括:
- 商品管理:新增、编辑、下架数码产品。
- 秒杀活动管理:设置某款商品参与抢购的时间段、总量。
- 订单管理:查看所有用户的订单、处理异常订单。
- 数据统计:简单统计参与人数、抢购成功人数、订单转化率。
功能划分并不复杂,但每一块都能体现不同的技术点,比如图片上传、时间管理、状态枚举、分页查询等等。
2.2 数据表如何设计
数据库设计是我每做一个项目都反复强调的环节,表结构设计得好不好,直接决定了后期代码好不好写,性能能不能扛住并发。
一个核心原则:把商品信息和秒杀信息分离。不要在商品表里直接塞秒杀价格和秒杀库存,而是单独建一张秒杀商品表。因为商品本身有日常价格、日常库存,秒杀活动是一次性的、有时效的,混在一起会让逻辑变得混乱。
我建议的核心表如下:
用户表(t_user)
CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_name` varchar(32) NOT NULL COMMENT '用户名', `password` varchar(128) NOT NULL COMMENT '密码(加盐加密)', `phone` varchar(11) DEFAULT NULL COMMENT '手机号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;商品表(t_product)
CREATE TABLE `t_product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_name` varchar(64) NOT NULL, `product_desc` varchar(255) DEFAULT NULL, `product_img` varchar(255) DEFAULT NULL, `product_price` decimal(10,2) NOT NULL COMMENT '日常价格', `stock` int(11) DEFAULT '0' COMMENT '日常库存', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;秒杀商品表(t_seckill_product)
CREATE TABLE `t_seckill_product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_id` bigint(20) NOT NULL, `seckill_price` decimal(10,2) NOT NULL COMMENT '秒杀价格', `seckill_stock` int(11) NOT NULL COMMENT '秒杀库存', `start_time` datetime NOT NULL COMMENT '开始时间', `end_time` datetime NOT NULL COMMENT '结束时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单表(t_order)
CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `seckill_id` bigint(20) NOT NULL, `order_no` varchar(32) NOT NULL COMMENT '订单编号', `order_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待支付 1已支付 2已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_seckill` (`user_id`, `seckill_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有一个非常重要的细节,订单表里加了唯一索引uk_user_seckill,它保证同一个用户针对同一个秒杀活动只能生成一条订单。这是防重复下单的数据库兜底方案,哪怕代码层面漏了判断,数据库也会拒绝重复插入。记住这个设计,后面讲接口幂等还会再次提到。
3. 后端核心逻辑:高并发秒杀究竟怎么实现
3.1 先明白库存超卖是怎么发生的
要解决问题,先要理解问题的根源。假设库存只剩1台,两个用户同时发起请求,代码是这样写的:
int stock = seckillProductMapper.getStock(seckillId); if (stock > 0) { seckillProductMapper.reduceStock(seckillId); createOrder(userId, seckillId); }两个请求同时读到stock = 1,都进入if条件,都执行库存减一,结果数据库里的库存变成 -1,而订单生成了两笔。这就是典型的超卖。
解决方案有几条路线,难度递增,效果也递增:
- 方案一:数据库乐观锁,update时带上库存条件。
- 方案二:数据库悲观锁,select加上
for update。 - 方案三:Redis预扣减库存,加Lua脚本保证原子性。
对一个计算机设计项目来说,我推荐至少掌握方案一和方案三。方案一代码简单,逻辑直观;方案三是生产环境里真正的秒杀思路,答辩时讲出来是加分项。
3.2 基于数据库乐观锁的扣减方案
首先来看最简单的、也最容易理解的方案:乐观锁。
@Transactional public boolean seckillByOptimisticLock(Long userId, Long seckillId) { SeckillProduct sp = seckillProductMapper.selectById(seckillId); if (sp == null || sp.getEndTime().isBefore(LocalDateTime.now())) { throw new BizException("抢购已结束"); } if (sp.getSeckillStock() <= 0) { throw new BizException("手慢了,商品已被抢完"); } int rows = seckillProductMapper.reduceStockByVersion(seckillId, sp.getVersion()); if (rows == 0) { throw new BizException("抢购拥挤,请重试"); } Order order = new Order(); order.setUserId(userId); order.setProductId(sp.getProductId()); order.setSeckillId(seckillId); order.setOrderNo(generateOrderNo()); order.setOrderStatus(0); orderMapper.insert(order); return true; }对应的SQL可以这样写:
UPDATE t_seckill_product SET seckill_stock = seckill_stock - 1, version = version + 1 WHERE id = #{seckillId} AND seckill_stock > 0 AND version = #{version}seckill_stock > 0这个条件本身就是一道防线,确保不会扣成负数。version = #{version}则保证只有一个线程能在当前版本下更新成功。这个方法在并发量不是特别极端时非常有效,代码清晰,容易讲解。
3.3 基于Redis + Lua的高并发方案
如果项目强调“高并发”,数据库乐观锁虽然正确,但每次请求都要打一次数据库,数据库承受的压力很大。生产环境里的秒杀系统,通常采用Redis作为前置库存层。
核心思路是:系统启动时或活动开始前,把秒杀库存加载到Redis中,用字符串或Hash存储剩余库存。用户发起秒杀请求时,先用Lua脚本原子地检查库存并扣减,扣减成功后再发送MQ消息,由MQ消费者异步创建数据库订单。
Lua脚本长这样:
-- KEYS[1]: seckill:stock:{seckillId} -- KEYS[2]: seckill:user:{seckillId} -- ARGV[1]: userId local stock = tonumber(redis.call('get', KEYS[1])) local hasBought = redis.call('sismember', KEYS[2], ARGV[1]) if hasBought == 1 then return -1 end if stock <= 0 then return 0 end redis.call('decr', KEYS[1]) redis.call('sadd', KEYS[2], ARGV[1]) return 1这段脚本通过两个Redis操作完成了一个原子事务:检查是否重复购买、检查库存、扣减库存、记录用户。结合SpringBoot的RedisTemplate,代码如下:
@Autowired private StringRedisTemplate redisTemplate; public long seckillByRedis(Long userId, Long seckillId) { String stockKey = "seckill:stock:" + seckillId; String userKey = "seckill:user:" + seckillId; DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setLocation(new ClassPathResource("seckill.lua")); script.setResultType(Long.class); Long result = redisTemplate.execute(script, Arrays.asList(stockKey, userKey), String.valueOf(userId)); if (result == null || result == -1) { throw new BizException("您已参与本次抢购,请勿重复提交"); } if (result == 0) { throw new BizException("库存不足"); } // 发送MQ消息,异步创建订单 sendMsgToMq(userId, seckillId); return result; }注意一个小细节,RedisTemplate默认的序列化器会把Key变成\xac\xed...之类的二进制形式,非常恶心。直接使用StringRedisTemplate或者确保Key都存String,能省去很多调试麻烦。
还有一个需要注意的点是:Redis扣减成功后,用户返回的“抢购成功”是预占库存成功,真正订单是在MQ消费者里创建的。如果MQ消费失败或者数据库写入失败,会出现Redis显示用户已抢到,但数据库没有订单的情况。所以消费者里要做重试补偿,这也是答辩时可以说的一个“进阶考虑”。
3.4 接口幂等与防重复提交
抢购场景里,用户手速快,或者网络不好后刷新页面,可能连续发出好几个同样的请求。即便Redis里用集合去重了,还是应该在前端和接口层同时做防护。
前端层面,点击抢购后按钮立即置灰,显示“排队中”,禁止再次点击。
handleSeckill() { if (this.isSeckilling) return this.isSeckilling = true this.countdownTimer && clearInterval(this.countdownTimer) seckillApi.submit({ seckillId: this.seckill.id }) .then(res => { this.$message.success('抢购成功,请尽快支付') this.getOrderStatus() }) .catch(err => { this.$message.error(err.message || '抢购失败') }) .finally(() => { setTimeout(() => { this.isSeckilling = false }, 1000) }) }后端层面,除了数据库唯一索引兜底,还可以用Redis的SETNX做接口级去重。用户请求到来时,尝试设置一个不带过期时间的锁,Key为seckill:lock:userId:seckillId,如果设置成功说明是第一次请求,继续处理;设置失败说明用户已在处理中,直接返回“请勿重复提交”。
Boolean locked = redisTemplate.opsForValue().setIfAbsent("seckill:lock:" + userId + ":" + seckillId, "1", 10, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { throw new BizException("请求处理中,请勿重复操作"); }这里设置10秒过期时间是为了防止进程崩溃后锁无法释放的问题,造成用户永久被拒。
3.5 后端架构分层与核心代码结构
后端项目建议按标准分层来组织,Controller -> Service -> Mapper,清晰易读。
com.example.seckill ├── controller │ ├── LoginController.java │ ├── ProductController.java │ ├── SeckillController.java │ └── OrderController.java ├── service │ ├── impl │ │ ├── UserServiceImpl.java │ │ ├── ProductServiceImpl.java │ │ ├── SeckillServiceImpl.java │ │ └── OrderServiceImpl.java │ └── SeckillService.java ├── mapper │ ├── UserMapper.java │ ├── ProductMapper.java │ ├── SeckillProductMapper.java │ └── OrderMapper.java ├── entity │ ├── User.java │ ├── Product.java │ ├── SeckillProduct.java │ └── Order.java ├── common │ ├── Result.java │ ├── BizException.java │ └── GlobalExceptionHandler.java └── config ├── RedisConfig.java ├── WebMvcConfig.java └── RabbitMqConfig.javaResult.java是统一响应体,所有接口都返回这个结构,前端处理起来非常一致。格式大概是这样:
@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.code = 200; result.message = "success"; result.data = data; return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.code = 500; result.message = message; return result; } }4. 前端Vue实现要点
4.1 工程初始化与项目结构
前端我建议用Vue CLI或Vite搭建,Vite构建速度更快。安装依赖时常见的坑是node版本不匹配,npm install报错一大片。我的经验是先把node -v看一下,Vite项目建议Node 14.18以上,Vue CLI项目建议Node 12以上。如果版本冲突,用nvm切换版本,比到处百度报错信息高效得多。
基础项目结构:
src ├── api │ ├── product.js │ ├── seckill.js │ └── user.js ├── assets │ └── logo.png ├── components │ ├── CountDown.vue │ ├── ProductCard.vue │ └── OrderList.vue ├── router │ └── index.js ├── store │ └── index.js ├── views │ ├── Login.vue │ ├── Home.vue │ ├── Detail.vue │ ├── OrderList.vue │ └── admin │ ├── AdminLogin.vue │ ├── ProductManage.vue │ └── OrderManage.vue ├── utils │ └── request.js └── main.js注意,utils/request.js里封装Axios实例,统一设置baseURL和拦截器。业务页面尽量不要直接写axios.get,全部通过api模块调用,这样改动后端地址时只需要改一个文件,维护成本低。
import axios from 'axios' import { Message } from 'element-ui' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_URL || 'http://localhost:8080', timeout: 10000 }) service.interceptors.request.use(config => { const token = sessionStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) } ) export default service4.2 倒计时组件的正确写法
抢购页面里最关键的交互就是倒计时。很多同学写倒计时喜欢在浏览器本地做:拿到结束时间,然后setInterval每秒减一。但这样做有问题,用户改了本机时间或者网络延迟,倒计时就会不准。
正确做法是,后端返回服务器时间和活动结束时间,前端用两者的差值来计算剩余毫秒。
后端接口可以这样写:
@GetMapping("/serverTime") public Result<Map<String, Object>> serverTime() { Map<String, Object> map = new HashMap<>(); map.put("serverTime", System.currentTimeMillis()); return Result.success(map); }前端倒计时处理:
// 获取服务器时间与结束时间的差值 async initCountdown() { const { data } = await getServerTime() const serverTime = data.serverTime const endTime = new Date(this.seckill.endTime).getTime() this.remainMs = endTime - serverTime this.timer = setInterval(() => { this.remainMs -= 1000 if (this.remainMs <= 0) { clearInterval(this.timer) this.remainMs = 0 this.seckillStarted = true } }, 1000) }网上有很多倒计时插件可以用,但自己做一遍能更好地理解时间同步问题。我当时写这个组件时踩了一个很傻的坑:用setInterval每隔一秒减1000,但其实 JavaScript 的定时器并不精确,特别是浏览器标签页切到后台时,定时器会变慢。如果只按减法累积,时间差会越来越大。想更精准,可以在每个回调里重新用当前时间和结束时间计算差值,而不是盲目减1000,这个细节写进文档里也很有亮点。
4.3 路由和状态管理
路由需要区分用户端和管理端。可以用Vue Router的嵌套路由和前置守卫配合。
const router = new VueRouter({ routes: [ { path: '/', component: Layout, children: [ { path: '', component: Home, meta: { requiresAuth: true } }, { path: 'detail/:id', component: Detail, meta: { requiresAuth: true } }, { path: 'orders', component: OrderList, meta: { requiresAuth: true } } ] }, { path: '/login', component: Login }, { path: '/admin', component: AdminLayout, children: [ { path: 'products', component: ProductManage }, { path: 'orders', component: OrderManage } ] } ] }) router.beforeEach((to, from, next) => { const token = sessionStorage.getItem('token') if (to.matched.some(record => record.meta.requiresAuth) && !token) { next('/login') } else { next() } })抢购系统里的用户状态管理,用Vuex存token和userInfo就够了,不需要搞太复杂。用户抢购成功后要刷新订单状态,可以调用/order/status接口,轮询几次或者用WebSocket推送,两个方案都可以,但WebSocket对项目复杂度和代码量提升明显,我自己实现的是轮询方案,因为简单可控。
5. 常见问题排查与项目优化实录
5.1 跨域问题一劳永逸的解法
前后端分离后,第一个拦路虎就是跨域。开发环境下,前端跑在8081,后端跑在8080,浏览器默认会拦截跨域请求。网上的教程五花八门,有说用JSONP的,有说装插件的,其实对项目来说最标准的方案就是后端全局CORS配置。
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }加了统一CORS配置后,前端Axios不需要做任何额外处理,跨域问题根治。这里有个细节:allowedOriginPatterns和allowedOrigins的区别是,前者支持通配符,后者在早期的Spring版本中配合allowCredentials(true)会报错。如果你用的是SpringBoot 2.4以上版本,直接用allowedOriginPatterns("*")最省事。
5.2 Redis连接被撑爆的教训
我第一次做秒杀时,请求量稍微大一点,Redis连接数就飙到几百,然后报Could not get a resource from the pool。排查了半天发现是配置问题。SpringBoot的Redis连接池默认配置比较保守,连接池不够用。后来在application.yml里调整了参数:
spring: redis: host: localhost port: 6379 lettuce: pool: max-active: 100 max-idle: 20 min-idle: 5 max-wait: 3000ms调整之后连接池不再是瓶颈。但要注意,max-active并不是越大越好,开太多连接反而浪费资源,还会增加Redis服务端压力。对毕设或课设规模的项目,100个连接完全够用了。
5.3 数据库连接池同样需要合理配置
高并发场景下,HikariCP默认的maximum-pool-size是10,对秒杀来说有点小,我调到了20。不要小看这个参数,它可能是你压测时接口吞吐量的瓶颈之一。调大后,如果还出现连接不够,就要考虑是不是代码里事务开启时间过长或者连接泄漏,而不是一味地调大参数。排查连接泄漏的办法是设置leak-detection-threshold: 60000,超过60秒的连接会被打印告警日志,相当好用。
5.4 压测与性能实录
我在本地笔记本上用JMeter做了简单压测:对一个查询商品详情的接口,200线程并发循环100次,调整前后对比明显。没做任何优化时(每次都查数据库),平均响应时间接近800ms。加了Redis缓存之后(商品详情先查Redis,缓存没有再查数据库),平均响应时间降到150ms左右。这个数据虽然不比生产环境,但足以证明缓存的作用,答辩时拿出一张压测对比图,说服力拉满。
秒杀接口本身不压测不知道,压测才发现@Transactional事务范围过大导致的性能问题。事务里的所有操作都在同一个数据库连接上,事务时间越长,连接占用越久,池子越容易耗尽。优化思路是减少事务范围,远程调用、消息发送尽量放在事务之后执行。
5.5 常见BUG速查表
给出几个我在开发过程里真实遇到的BUG,你可以对照检查自己的项目:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 前端请求报404,后端接口明明存在 | 拦截器拦截了 OPTIONS 预检请求 | CORS配置里放行OPTIONS,或在拦截器里直接放行非业务路径 |
| 库存显示负数 | 扣减库存的SQL没有加库存大于0条件 | 在update语句里补AND stock > 0 |
| 倒计时归零后点抢购仍报“未开始” | 前后端时间不同步 | 使用服务器时间计算,不依赖本地时间 |
| 同一用户重复下单成功 | 缺少唯一约束或幂等性检查 | 数据库加唯一索引 + Redis SETNX防重 |
| Redis启动后数据全部失效 | 没开启持久化或缓存没预热 | 启动时用@PostConstruct或CommandLineRunner从数据库加载库存到Redis |
| 前端打包后接口地址错乱 | 使用了相对路径或写死地址 | 用.env配置文件区分开发和生产环境 |
6. 万字文档和答辩准备
6.1 技术文档的写法
计算机设计项目学习里,文档和源码同样重要。很多同学代码写得不错,一写文档就硬挤,最后交上去一篇东拼西凑的“论文”。我的建议是,文档的每章内容应该和代码组件一一对应,而不是空泛地讲概念。
整体文档大纲可以这样设置:
- 第一章 引言:项目背景、意义。
- 第二章 需求分析:用例图、功能需求、非功能需求。
- 第三章 系统设计:架构图、功能模块图、数据库ER图、表结构说明。
- 第四章 详细设计与实现:前端每个页面、后端每个模块的关键代码和说明。
- 第五章 系统测试:测试用例表、压测结果、测试结论。
- 第六章 总结与展望:项目亮点、遇到的问题、后续优化方向。
写详细设计时,不要贴一长串代码就完事,要配合文字说明核心逻辑,尤其要把“防超卖方案”和“接口幂等设计”这两个技术亮点单独列小节,多写设计思路和对比方案。比如为什么选MySQL不选Oracle,为什么用Redis做库存而不直接操作数据库,这些对比能体现你对技术选型的思考深度,阅卷老师和答辩老师最吃这一套。
6.2 答辩演示的几个关键点
答辩前准备一个演示脚本,按顺序走完主要流程,不要让老师看到你在现场手忙脚乱。
演示流程我建议这样安排:
- 打开系统,注册一个新用户。
- 打开商品列表页,展示数码产品。
- 进入一个未开始的秒杀场次,展示倒计时。
- 时间到了,点击抢购,演示成功下单和商品库存减少。
- 再点一次抢购,演示“您已参与过本次抢购”的提示。
- 展示订单列表,确认刚才的订单出现在列表里。
- (可选)打开用户中心,查看订单状态。
答辩老师最关心的还是核心逻辑,主动讲讲“怎么防止超卖”和“怎么防止重复提交”,基本就能掌握主动权。如果老师追问“如果QPS更高怎么办”,可以从布式锁、MQ削峰、Sentinel限流、分库分表这几个方向回答,不一定要实现过,能讲清楚思路就很有优势。
我自己在项目里还预留了一个可以快速展示的压测入口,用JMeter脚本演示秒杀接口在500并发下库存依然准确,这个环节很多老师都会感兴趣,问的问题也会往并发控制方向走,反而容易出彩。
6.3 项目可以扩展的方向
如果你的时间充裕,想让项目更上一个台阶,可以在现有基础上尝试以下几个扩展:
- 使用RabbitMQ或Kafka做异步下单,把秒杀接口的响应时间进一步降低。
- 使用Sentinel或Redisson实现分布式限流。
- 使用WebSocket向用户推送秒杀结果。
- 使用Nginx做负载均衡,部署两个后端实例,演示水平扩展能力。
- 加一个简单的数据可视化看板,展示每个场次的热度、参与人数和峰值流量。
这些扩展说明不一定要全部写进代码,哪怕只是写了设计文档和原型图,也能让项目“看起来”完整度和思考深度高出一截。
7. 写在最后
这套数码产品抢购系统,我从需求拆解、技术选型、数据库设计到后端并发控制、前端交互,再到文档编写和答辩演示,全部围绕“高并发秒杀”这个核心场景展开。它和普通的商城管理系统最大的区别在于,你不只是在写CRUD,而是在思考并发、一致性、限流、缓存,这些都是真实业务里最有价值的部分。
我个人带过的不少学生项目里,能真正讲清楚Redis预扣减和数据库兜底的人非常少。大多数项目都能跑,但经不起问。如果你按照这篇博文的思路一步一步做下来,把库存防超卖、接口幂等、缓存加速这几个点彻底吃透,不管是课设答辩还是毕业答辩,都能从“做了个项目”提升到“解决了问题”的层次。
最后分享一个实操过程中的小心得:项目里尽量多写注释,特别是关键逻辑处的注释。不是给老师看的,是给三天后的自己看的。很多学生写完代码第二天回来调试,看到自己以前写的代码一脸懵,注释多一点能省下大量复盘时间。而且,注释也是文档的重要素材,等到最后写万字文档时,注释可以直接帮你回忆起每一行代码的设计意图。这个习惯我一直保留到现在,做任何项目都受益。