「秒杀系统」从零开始打造简易秒杀系统(一):防止超卖
liuian 2025-01-04 21:27 45 浏览
前言
大家好,好久不发文章了。(快一个月了- -)最近有很多学习的新知识想和大家分享,但无奈最近项目蛮忙的,很多文章写了一半搁置在了笔记里,待以后慢慢补充发布。
本文主要是通过实际代码讲解,帮助你一步步搭建一个简易的秒杀系统。从而快速的了解秒杀系统的主要难点,并且迅速上手实际项目。
我对秒杀系统文章的规划:
- 从零开始打造简易秒杀系统:乐观锁防止超卖
- 从零开始打造简易秒杀系统:令牌桶限流
- 从零开始打造简易秒杀系统:Redis 缓存
- 从零开始打造简易秒杀系统:消息队列异步处理订单
- …
欢迎关注我的公众号:后端技术漫谈(二维码见底部)
秒杀系统
秒杀系统介绍
秒杀系统相信网上已经介绍了很多了,我也不想黏贴很多定义过来了。
废话少说,秒杀系统主要应用在商品抢购的场景,比如:
- 电商抢购限量商品
- 卖周董演唱会的门票
- 火车票抢座
- …
秒杀系统抽象来说就是以下几个步骤:
- 用户选定商品下单
- 校验库存
- 扣库存
- 创建用户订单
- 用户支付等后续步骤…
听起来就是个用户买商品的流程而已嘛,确实,所以我们为啥要说他是个专门的系统呢。。
为什么要做所谓的“系统”
如果你的项目流量非常小,完全不用担心有并发的购买请求,那么做这样一个系统意义不大。
但如果你的系统要像12306那样,接受高并发访问和下单的考验,那么你就需要一套完整的流程保护措施,来保证你系统在用户流量高峰期不会被搞挂了。(就像12306刚开始网络售票那几年一样)
这些措施有什么呢:
- 严格防止超卖:库存100件你卖了120件,等着辞职吧
- 防止黑产:防止不怀好意的人群通过各种技术手段把你本该下发给群众的利益全收入了囊中。
- 保证用户体验:高并发下,别网页打不开了,支付不成功了,购物车进不去了,地址改不了了。这个问题非常之大,涉及到各种技术,也不是一下子就能讲完的,甚至根本就没法讲完。
我们先从“防止超卖”开始吧
毕竟,你网页可以卡住,最多是大家没参与到活动,上网口吐芬芳,骂你一波。但是你要是卖多了,本该拿到商品的用户可就不乐意了,轻则投诉你,重则找漏洞起诉赔偿。让你吃不了兜着走。
不能再说下去了,我这篇文章可是打着实战文章的名头,为什么我老是要讲废话啊啊啊啊啊啊。
上代码。
说好的做“简易”的秒杀系统,所以我们只用最简单的SpringBoot项目
建立“简易”的数据库表结构
一开始我们先来张最最最简易的结构表,参考了crossoverjie的秒杀系统文章。
等未来我们需要解决更多的系统问题,再扩展表结构。
一张库存表stock,一张订单表stock_order
-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
`count` int(11) NOT NULL COMMENT '库存',
`sale` int(11) NOT NULL COMMENT '已售',
`version` int(11) NOT NULL COMMENT '乐观锁,版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Table structure for stock_order
-- ----------------------------
DROP TABLE IF EXISTS `stock_order`;
CREATE TABLE `stock_order` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`sid` int(11) NOT NULL COMMENT '库存ID',
`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;通过HTTP接口发起一次购买请求
代码中我们采用最传统的Spring MVC+Mybaits的结构
结构如下图:
Controller层代码
提供一个HTTP接口: 参数为商品的Id
@RequestMapping("/createWrongOrder/{sid}")
@ResponseBody
public String createWrongOrder(@PathVariable int sid) {
LOGGER.info("购买物品编号sid=[{}]", sid);
int id = 0;
try {
id = orderService.createWrongOrder(sid);
LOGGER.info("创建订单id: [{}]", id);
} catch (Exception e) {
LOGGER.error("Exception", e);
}
return String.valueOf(id);
}Service层代码
@Override
public int createWrongOrder(int sid) throws Exception {
//校验库存
Stock stock = checkStock(sid);
//扣库存
saleStock(stock);
//创建订单
int id = createOrder(stock);
return id;
}
private Stock checkStock(int sid) {
Stock stock = stockService.getStockById(sid);
if (stock.getSale().equals(stock.getCount())) {
throw new RuntimeException("库存不足");
}
return stock;
}
private int saleStock(Stock stock) {
stock.setSale(stock.getSale() + 1);
return stockService.updateStockById(stock);
}
private int createOrder(Stock stock) {
StockOrder order = new StockOrder();
order.setSid(stock.getId());
order.setName(stock.getName());
int id = orderMapper.insertSelective(order);
return id;
}发起并发购买请求
我们通过JMeter(https://jmeter.apache.org/) 这个并发请求工具来模拟大量用户同时请求购买接口的场景。
注意:POSTMAN并不支持并发请求,其请求是顺序的,而JMeter是多线程请求。希望以后PostMan能够支持吧,毕竟JMeter还在倔强的用Java UI框架。毕竟是亲儿子呢。
如何通过JMeter进行压力测试,请参考下文,讲的非常入门但详细,包教包会:
https://www.cnblogs.com/stulzq/p/8971531.html
我们在表里添加一个Iphone,库存100。(请忽略订单表里的数据,开始前我清空了)
在JMeter里启动1000个线程,无延迟同时访问接口。模拟1000个人,抢购100个产品的场景。点击启动:
你猜会卖出多少个呢,先想一想。。。
答案是:
卖出了14个,库存减少了14个,但是每个请求Spring都处理了,创建了1000个订单。
我这里该夸Spring强大的并发处理能力,还是该骂MySQL已经是个成熟的数据库,却都不会自己锁库存?
避免超卖问题:更新商品库存的版本号
为了解决上面的超卖问题,我们当然可以在Service层给更新表添加一个事务,这样每个线程更新请求的时候都会先去锁表的这一行(悲观锁),更新完库存后再释放锁。可这样就太慢了,1000个线程可等不及。
我们需要乐观锁。
一个最简单的办法就是,给每个商品库存一个版本号version字段
我们修改代码:
Controller层
/**
* 乐观锁更新库存
* @param sid
* @return
*/
@RequestMapping("/createOptimisticOrder/{sid}")
@ResponseBody
public String createOptimisticOrder(@PathVariable int sid) {
int id;
try {
id = orderService.createOptimisticOrder(sid);
LOGGER.info("购买成功,剩余库存为: [{}]", id);
} catch (Exception e) {
LOGGER.error("购买失败:[{}]", e.getMessage());
return "购买失败,库存不足";
}
return String.format("购买成功,剩余库存为:%d", id);
}Service层
@Override
public int createOptimisticOrder(int sid) throws Exception {
//校验库存
Stock stock = checkStock(sid);
//乐观锁更新库存
saleStockOptimistic(stock);
//创建订单
int id = createOrder(stock);
return stock.getCount() - (stock.getSale()+1);
}
private void saleStockOptimistic(Stock stock) {
LOGGER.info("查询数据库,尝试更新库存");
int count = stockService.updateStockByOptimistic(stock);
if (count == 0){
throw new RuntimeException("并发更新库存失败,version不匹配") ;
}
}Mapper
<update id="updateByOptimistic" parameterType="cn.monitor4all.miaoshadao.dao.Stock">
update stock
<set>
sale = sale + 1,
version = version + 1,
</set>
WHERE id = #{id,jdbcType=INTEGER}
AND version = #{version,jdbcType=INTEGER}
</update>我们在实际减库存的SQL操作中,首先判断version是否是我们查询库存时候的version,如果是,扣减库存,成功抢购。如果发现version变了,则不更新数据库,返回抢购失败。
发起并发购买请求
这次,我们能成功吗?
再次打开JMeter,把库存恢复为100,清空订单表,发起1000次请求。
这次的结果是:
卖出去了39个,version更新为了39,同时创建了39个订单。我们没有超卖,可喜可贺。
由于并发访问的原因,很多线程更新库存失败了,所以在我们这种设计下,1000个人真要是同时发起购买,只有39个幸运儿能够买到东西,但是我们防止了超卖。
手速快未必好,还得看运气呀!
OK,今天先到这里,之后我们继续一步步完善这个简易的秒杀系统,它总有从树苗变成大树的那一天!
源码
我会随着文章的更新,一直同步更新项目代码,欢迎关注:
https://github.com/qqxx6661/miaosha
参考
- https://cloud.tencent.com/developer/article/1488059
- https://juejin.im/post/5dd09f5af265da0be72aacbd
- https://crossoverjie.top/%2F2018%2F05%2F07%2Fssm%2FSSM18-seconds-kill%2F
关注我
我是一名后端开发工程师。
主要关注后端开发,数据安全,物联网,边缘计算方向,欢迎交流。
各大平台都可以找到我
- 微信公众号:后端技术漫谈
- Github:@qqxx6661
- CSDN:@Rude3knife
- 知乎:@后端技术漫谈
- 简书:@蛮三刀把刀
- 掘金:@蛮三刀把刀
原创博客主要内容
- 后端开发技术
- Java面试知识点
- 设计模式/数据结构
- LeetCode/剑指offer 算法题解析
- SpringBoot/SpringCloud入门实战系列
- 数据分析/数据爬虫
- 逸闻趣事/好书分享/个人生活
个人公众号:后端技术漫谈
公众号:后端技术漫谈.jpg
如果文章对你有帮助,不妨收藏,转发,在看起来~
相关推荐
- 电脑给另一个硬盘装系统(电脑给另一块硬盘装系统)
-
回答如下:以下是在Windows操作系统下的步骤:1.确保你有一个可用的Windows安装光盘或USB安装介质。2.将另一个硬盘连接到计算机上并确保电脑识别到它。3.打开计算机BIOS并确保在启...
- 设计师专用笔记本电脑排行榜
-
惠普战99采用了英特尔第十二代酷睿i7-12700H标压处理器和英伟达T600工作站级专业显卡的配置。存储方面支持双通道3200MHz内存和双M.2接口SSD,配置十分豪华。15.6英寸高色域高清IP...
-
- 打印机故障排查(打印机故障排查提示不接收任务怎么处理)
-
步骤/方式1点击电脑桌面左下角的开始按钮,选择设备和打印机的选项。步骤/方式2找到有故障的打印机,鼠标右键点击选择属性的选项。步骤/方式3在打印机属性页面,我们点击维护的选项。步骤/方式4在维护页面,选择对应的故障进行维护,即可解决故障亮灯...
-
2026-01-09 19:55 liuian
- 内部版本7601激活(内部版本7601影响使用吗)
-
激活方法如下右击计算机--属性,拉倒最下面,会提示该系统未激活,然后点击激活,输入密钥就可以了,需要连接互联网。如果没有互联网,也可以电话激活。下载安装内部专用激活工具激活就可以了Windows7内部...
- microsoft打不开怎么办(microsoftoffice打不开)
-
方法一:1、按Win+S组合键,或点击底部任务栏开始旁的搜索图标,在打开的Windows搜索窗口,搜索框输入服务,然后点击打开系统给出的最佳匹配服务应用;2、服务窗口,找到并双击打开WLA...
- 安卓仿苹果ios主题下载免费(安卓仿苹果ios12主题下载)
-
要把华为手机的主题改成类似iOS的风格,可以按照以下步骤进行操作:1.在华为应用商店中搜索并下载一个适合的iOS主题,例如iOSLauncher等。2.安装完成后,打开该主题应用,在设置中选择启...
- 声卡驱动安装哪个比较好(声卡驱动应该安装在哪里)
-
EXPSoundboard软件使用电脑自带的集成声卡就可以了。只是需要安装一些变声软件。如EXPSoundboard软件,使用这款EXPSoundboard中文版可以让你自由自在的在任何游戏中挂...
- 宽带登录网站(宽带登录网站怎么登录)
-
在浏览器内输入www.10010.com,进入中国联通网上营业厅后,选择“登录”,输入宽带账号与宽带密码,点击登录即可使用联通宽带登录联通网上营业厅。可通过以下方式办理联通宽带预约服务:1、登录联通网...
- tenda官网登入(tenda官网192.168.0.1登录)
-
腾达无线的路由器登录入口是:tplogin.cn电信运营商定制款登录地址是:192.168.2.1或者192.168.8.12、华为(容易)路由器华为路由器跟荣耀路由器只有IP地址,没有域名,它是...
- u盘格式化不了怎么回事(u盘格式化不了为什么)
-
第一种情况:u盘本身带有写保护开关的情况。这种情况一般是因为有些朋友借的U盘或者不了解U盘本身的情况,不注意打开了写保护开关,导致的U盘写保护,这种情况的解决方法就很简单了,找到开关关掉写保护即可。第...
- windows7专业版sp1补丁(win7旗舰版sp1补丁)
-
win7sp1补丁安装方法如下:1成后,需要打sp1补丁,不过微软对win7的支持已经结束了,建议升级新系统2到微软官网/zh-cn/download找到win7系统版本的补丁,点击下载3勾选对应系统...
- 无线ap面板哪个品牌好(无线ap面板什么品牌好)
-
作为工程商的我,用过用多牌子做无线覆盖工程,用过大品牌的有华为,H3C,思科,比较贵,性能强大。确实是不错的,就是费用高,老板指定就会用的。用过有中高端牌子有很多,但后面用着用着出现问题,不敢用了。现...
-
- 怎么下载07版的office(下载office2007的步骤)
-
office是每个电脑都必备的一个软件,那office2007免费完整版怎么下载呢?下面就来教教大家具体步骤。1、我们在浏览器输入栏搜索“zol”,然后选择官网进入。2、在页面右上角选择“软件下载”,然后选择“软件分类”,点击“办公软件”。...
-
2026-01-09 14:55 liuian
-
- 拼音五笔两用的输入法叫什么
-
这个是因为你电脑设置了五笔为默认输入法,所以你打开的任何窗口都是五笔输入法。你可以在电脑语言设置里面更改默认设置,如win10系统直接在输入法那里打开语言首选项,选择键盘那个选项直接可以按照自己的使用习惯更改默认输入法。可以在不同的窗口设置...
-
2026-01-09 14:05 liuian
-
- 外置sd卡文件加密软件(sd卡加密怎么破解)
-
不管是给手机sd卡加密还是外置sd卡加密,办法都是一样的,具体操作为以下几步:1、在手机【设置】中找到【安全】功能,不同的手机有不一样的选项,有的在【高级设置】中,而有的则在【设置】中。2、在【安全】选项中,找到【设置SD卡密码】或【加密外...
-
2026-01-09 13:55 liuian
- 一周热门
-
-
飞牛OS入门安装遇到问题,如何解决?
-
如何在 iPhone 和 Android 上恢复已删除的抖音消息
-
Boost高性能并发无锁队列指南:boost::lockfree::queue
-
大模型手册: 保姆级用CherryStudio知识库
-
用什么工具在Win中查看8G大的log文件?
-
如何在 Windows 10 或 11 上通过命令行安装 Node.js 和 NPM
-
威联通NAS安装阿里云盘WebDAV服务并添加到Infuse
-
Trae IDE 如何与 GitHub 无缝对接?
-
idea插件之maven search(工欲善其事,必先利其器)
-
如何修改图片拍摄日期?快速修改图片拍摄日期的6种方法
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)
- mysql刷新权限 (34)
