博客地址: 手摸手带你写项目----秒杀系统(一)
所有文章会第一时间在博客更新!

后面的时间我会手摸手带大家一起写几个实战性的项目。主要希望能应用上之前梳理的那些知识点,同时让没有写过项目的同学对实战项目有一定的认识。

小明问:手摸手?你这项目是正经项目吗?
我:我做这个的,能教给你不正经项目?doge

当然基于个人的认识不足,肯定有写的不好的地方,希望同学们能在评论区指出0 0。

1. 项目介绍

秒杀系统其实大家在日常生活中接触很多,12306抢票、特价商品抢购、拼多多拼团等等等等。秒杀系统的特点就是短时间高并发大流量。特别是类似于12306、淘宝等客量大的平台,对于部分热门活动的QPS可能会高达上千万次,甚至在春节期间,12306的QPS会高达上亿次。

而这对于我们一般使用的利于MySQL等关系型数据库来说,它们的QPS也就在数万级(单机情况下),肯定是远远无法达到系统需求的。

那么这个时候,系统的技术选型以及系统设计就非常重要了。同时为了应对短时间、大流量的高并发请求,代码的实现细节也十分重要。

一旦代码实现出现逻辑上的问题,轻则系统崩溃,活动无法进行下去,出现线上事故;重则出现商品大量超卖情况,那只能自己主动辞职了0 0。

有的同学说,那我是不是只要解决了超卖问题,系统能正常运行就没问题了呢?也不尽然,我们还要防黄牛党用脚本抢购商品。

本来公司组织的秒杀活动是用来引流的,赚不到什么钱,结果还全被黄牛抢完了,正常用户抢不到,用户粘性变差,达不到引流效果,同样不行。

所以一个秒杀系统看似简单,里面需要注意的细节非常多。

那么,一个完整的秒杀系统的架构是怎样的呢?这里偷一张敖丙大佬的图:

图片出处: 敖丙带你设计【秒杀系统】

下面我们开始一步一步的来实现一个秒杀系统。其中可能会用到乐观锁、Redis缓存、令牌桶限流等技术手段。

注意,这里我们的重点放在实现秒杀系统的流程上,所以对于一个正常的商城系统中的权限管理、用户管理等这里暂不考虑。

1.项目创建

首先我们创建一个IDEA项目,这里我的项目环境如下:

  • 系统环境:WIndows10专业版
  • JDK版本:JDK1.8
  • MySQL版本:MySQL 5.7.28
  • Redis版本:redis-3.0.504
  • JMeter版本(用于项目压测):apache-jmeter-5.4.1

第一个版本在pom.xml中加入了lombok,以及druid用于做数据库连接池。完整的pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.5</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>cn.codinglemon</groupId><artifactId>demo</artifactId><version>0.0.1-SNAPSHOT</version><name>demo</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><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.2.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.6</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

application.yml的配置参数如下:

server:port: 8989servlet:context-path: /miaosha
spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/miaoshausername: rootpassword: ****
mybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: cn.codinglemon.demo.entitylogging:level:root: infocn:codinglemon:dao: debug

项目基础搭建完成之后,下面我们来创建数据库。

2. 数据库创建

其实正常来分析一个项目,应该从系统设计出发,确定好多个主体的属性以及关系之后,再根据这些属性和关系来划分成数据库中的一个一个表,这里由于篇幅原因,就不再做具体分析,感兴趣的同学可以找我私聊0 0。为了尽可能的简化项目,这里只有两张表,库存表stock和订单表stock_order。

具体SQL代码如下:

-- stock表CREATE TABLE `stock`  (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '商品名称',`count` int(11) NOT NULL DEFAULT 0 COMMENT '库存',`sale` int(11) NOT NULL DEFAULT 0 COMMENT '已售',`price` decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '单价',`version` int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁使用的版本号',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;-- stock_order表CREATE TABLE `stock_order`  (`id` int(11) NOT NULL AUTO_INCREMENT,`sid` int(11) NOT NULL DEFAULT 0 COMMENT '库存id',`name` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '商品名称',`count` int(11) NULL DEFAULT NULL COMMENT '数量',`total_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '总价',`create_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2578 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;

数据库创建完成后,我们在stock表中插入一条测试数据。

注意:这里的count表示商品的总量,sale表示已售商品数,price表明商品单价,version表示版本号,后面用于使用乐观锁来解决超卖问题。

3. 实现 entity层、Dao层

根据数据库的字段,我们在项目中创建entity包,并将两个表对应的类创建,代码如下:

package cn.codinglemon.demo.entity;
import lombok.Data;/*** @author zry* @date 2021-9-29 16:28*/
@Data
public class Stock {private Integer id;private String name;private Integer count;private Integer sale;//这里的价格使用long字段来表示,price的值等于商品输入价格*10,以此来解决计算机浮点数计算精度问题private long price;private Integer version;
}

因为使用了lombok,所以直接在类上使用@Data注解,它会自动帮我们加上getter、setter方法。

另外,这里的商品单价用long字段,是为了解决计算机浮点数计算的精度问题。商品实际价格为price/10。

package cn.codinglemon.demo.entity;import lombok.Data;
import java.util.Date;/*** @author zry* @date 2021-9-29 16:29*/
@Data
public class StockOrder {private Integer id;private Integer sid;private String name;private Integer count;//这里的价格使用long字段来表示,totalPrice的值等于商品输入价格*10,以此来解决计算机浮点数计算精度问题private long totalPrice;private Date createTime;
}

StockOrder 中的totalPrice同理。

entity层实现完成后,这里我们来实现Dao层,具体代码如下:

StockDao

package cn.codinglemon.demo.dao;import cn.codinglemon.demo.entity.Stock;
import org.apache.ibatis.annotations.Param;/*** @author zry* @date 2021-9-29 16:30*/
public interface StockDao {Stock selectById(@Param("id")Integer id);int sale(@Param("id")Integer id,@Param("sale")Integer sale);
}

与之对应的xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.codinglemon.demo.dao.StockDao"><select id="selectById" resultType="cn.codinglemon.demo.entity.Stock">select id,name,count,sale,price,version from stock where id = #{id}</select><update id="sale" >update stock set sale = #{sale} where id = #{id} and count >= #{sale}</update>
</mapper>

StockOrderDao:

package cn.codinglemon.demo.dao;import cn.codinglemon.demo.entity.StockOrder;/*** @author zry* @date 2021-9-29 16:31*/
public interface StockOrderDao {int createOrder(StockOrder stockOrder);
}

与之对应的xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.codinglemon.demo.dao.StockOrderDao"><insert id="createOrder" useGeneratedKeys="true" keyProperty="id">insert into stock_order values(#{id},#{sid},#{name},#{count},#{totalPrice},NOW());</insert></mapper>

4. 实现Contrller层,并根据Controller层的需要实现Service层

注意,Controller层的作用是尽可能的只做数据校验和异常处理,不要将业务逻辑放在Controller层中;业务逻辑应该都在Service层中处理完成。

那么一个正常的秒杀系统中下单的流程是怎样的呢?我认为对于后台逻辑来说,可以简单的分为三步:

  1. 检查库存是否足够
  2. 创建订单
  3. 返回订单信息

那么这三步在Controller层中如何体现呢?实际编写代码如下:

package cn.codinglemon.demo.controller;import cn.codinglemon.demo.Response.StockResponseEnum;
import cn.codinglemon.demo.entity.Stock;
import cn.codinglemon.demo.entity.StockOrder;
import cn.codinglemon.demo.service.StockOrderService;
import cn.codinglemon.demo.service.StockService;
import cn.codinglemon.demo.util.ResponseBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;/*** @author zry* @date 2021-9-29 16:42*/
@RestController
@CrossOrigin
@RequestMapping("/stock")
public class StockController {@Autowiredprivate StockService stockService;@Autowiredprivate StockOrderService stockOrderService;@GetMapping("/kill")public ResponseBean kill(@RequestParam("id")Integer id,@RequestParam("count")Integer count){ResponseBean responseBean = new ResponseBean();//检查库存是否足够if(stockService.checkStock(id,count)){//创建订单Stock stock = stockService.selectById(id);StockOrder stockOrder = new StockOrder();stockOrder.setName(stock.getName());stockOrder.setSid(stock.getId());stockOrder.setTotalPrice(count*stock.getPrice());stockOrder.setCount(count);stockOrder = stockOrderService.createOrder(stockOrder);if(stockOrder !=null){//返回订单信息responseBean.setCode(StockResponseEnum.StOCK_SUCCESS.getCode());responseBean.setMsg(StockResponseEnum.StOCK_SUCCESS.getMessage());responseBean.setData(stockOrder);} else {responseBean.setCode(StockResponseEnum.STOCK_NOT_ENOUGH.getCode());responseBean.setMsg(StockResponseEnum.STOCK_NOT_ENOUGH.getMessage());}}else {responseBean.setCode(StockResponseEnum.STOCK_NOT_ENOUGH.getCode());responseBean.setMsg(StockResponseEnum.STOCK_NOT_ENOUGH.getMessage());}return responseBean;}
}

这里我们创建了一个ResponseBean来做统一的接口返回处理,其代码如下:

package cn.codinglemon.demo.Response;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;/*** @author zry* @date 2020-10-17 15:57:35* 返回给前台的数据对象*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class ResponseBean {private Integer code;private String msg;private Object data;
}

另外创建了一个Enum用于存放状态码和状态信息,代码如下:

package cn.codinglemon.demo.Response;/*** @author zry* @date 2021-9-29 20:16*/
public enum StockResponseEnum {StOCK_SUCCESS(20001,"下单商品成功"),STOCK_NOT_ENOUGH(20002,"商品库存不足");;StockResponseEnum(Integer code,String message) {this.code =code;this.message =message;}private int code;private String message;public int getCode() {return this.code;}public String getMessage() {return this.message;}public StockResponseEnum setMessage(String message) {this.message = message;return this;}
}

这里我们还没有实现的是StockService以及StockOrderService。StockService中分别有两个方法,第一个是检查库存,第二个是根据id获取Stock对象。

StockOrderService中就一个创建订单的方法。

两个service代码如下:

package cn.codinglemon.demo.service;import cn.codinglemon.demo.entity.Stock;/*** @author zry* @date 2021-9-29 18:39*/
public interface StockService {boolean checkStock(Integer id,Integer count);Stock selectById(Integer id);
}

对应的实现类:

package cn.codinglemon.demo.service.impl;import cn.codinglemon.demo.dao.StockDao;
import cn.codinglemon.demo.entity.Stock;
import cn.codinglemon.demo.service.StockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;/*** @author zry* @date 2021-9-29 18:42*/
@Service
public class StockServiceImpl implements StockService {@Autowiredprivate StockDao stockDao;@Overridepublic boolean checkStock(Integer id,Integer count) {Stock stock = stockDao.selectById(id);if(!ObjectUtils.isEmpty(stock)){//判断当前库存是否充足return stock.getCount() >= stock.getSale() + count;}return false;}@Overridepublic Stock selectById(Integer id) {return stockDao.selectById(id);}
}
package cn.codinglemon.demo.service;import cn.codinglemon.demo.entity.StockOrder;/*** @author zry* @date 2021-9-29 18:48*/
public interface StockOrderService {StockOrder createOrder(StockOrder stockOrder);}

对应的实现类:

package cn.codinglemon.demo.service.impl;import cn.codinglemon.demo.dao.StockDao;
import cn.codinglemon.demo.dao.StockOrderDao;
import cn.codinglemon.demo.entity.Stock;
import cn.codinglemon.demo.entity.StockOrder;
import cn.codinglemon.demo.service.StockOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;/*** @author zry* @date 2021-9-29 18:49*/
@Service
public class StockOrderServiceImpl implements StockOrderService {@Autowiredprivate StockOrderDao stockOrderDao;@Autowiredprivate StockDao stockDao;@Override@Transactional(propagation = Propagation.REQUIRED)public StockOrder createOrder(StockOrder stockOrder) {Stock stock = stockDao.selectById(stockOrder.getSid());//查看能否找到商品if( stock != null){boolean changeStock = stockDao.sale(stockOrder.getSid(),stockOrder.getCount()+stock.getSale()) > 0;//判断更新库存成功if(changeStock){boolean result = stockOrderDao.createOrder(stockOrder) >0;//判断订单是否成功存入数据库if(result){return  stockOrder;}}}return null;}
}

5. 启动项目

启动项目前,需要在Application类上添加如下注解,用于扫描Dao层。

package cn.codinglemon.demo;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
//添加注解,扫描dao层
@MapperScan("cn.codinglemon.demo.dao")
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}}

这里第一版的代码基本完成,我们启动项目看看。

如果启动后或访问接口有问题,可以查看控制台错误信息或者对照我的代码来检查一下。所有代码我会在我的github以及gitee上更新,地址在文章末尾。

我们访问接口:

http://localhost:8989/miaosha/stock/kill?id=1&count=1

这里会下单一个id为1的商品。

返回结果如下表明代码编写无误。

多次刷新你会发现,好像整个系统没有问题啊?

但是其实这里只是debug环境、单机情况下访问接口,相当于一个用户单线程下单商品,当然没问题,但是多线程情况下呢?

这里我们就需要使用到JMeter压力测试工具。

这里有关JMeter的介绍以及如何使用不多做赘述,这里有一篇文章介绍的比较详细了。

JMeter教程

我们启动JMeter的GUI界面,创建一个Thread Group:

修改线程数为1000:

然后右键Thread Group,创建一个HTTP请求:

然后填入接口信息:

需要填入的地方我都用红框标注出来了。

然后我们添加BeanShell Sampler,并添加如下语句:

prev.setDataEncoding(“utf-8”);

这里是为了解决返回的http请求的中文乱码问题。

然后添加返回结果树来查看每个线程请求后的返回信息:

好,所有准备工作完成后,这里我们点击左上角的开始按钮,开始测试:

中间会询问是否保存当前测试文件,选否即可。

等待运行结束后,这里我们可以在View Results Tree中看见请求的返回信息:

好,这里我们返回我们的数据库,可以看到,我们的stock表中,sale的值是250没问题:

但是,查看stock_order表就会发现,问题大了,id序号是从2578到2984:

卖出了整整406件!远远超出了我们设定的库存数量250件!

tips:这里每次测试的实际卖出数量都有可能不同,但是发生超卖的情况非常大。测试线程数越多,超卖的数量有可能越多。

这可怎么办?!下一篇,我们将详细介绍,如何解决超卖问题。

我们下期再见0 0!。

6.源代码地址

源代码如下,会根据项目进度不定期更新:

github地址: 秒杀项目实战
gitte地址:秒杀项目实战

手摸手带你写项目----秒杀系统(一)相关推荐

  1. 每天研究一个产品,阿德老师“手摸手”带你写产品分析报告 |

    作为一个产品经理,要高频地去把玩各种最新产品,所以我们想把那些对世界充满好奇心.勇于探索新鲜事物的产品经理都聚在一起.一起深入研究国内外最新/奇产品,一起发现有趣的事情,并把研究心得都整理成文章沉淀下 ...

  2. 手摸手,带你用vue撸后台 系列一(基础篇) - 掘金

    完整项目地址:vue-element-admin 系列文章: 手摸手,带你用 vue 撸后台 系列一(基础篇) 手摸手,带你用 vue 撸后台 系列二(登录权限篇) 手摸手,带你用 vue 撸后台 系 ...

  3. vue 前端显示图片加token_手摸手,带你用vue撸后台 系列二(登录权限篇)

    完整项目地址:vue-element-admin https://github.com/PanJiaChen/vue-element-admin 前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自 ...

  4. 带你手摸手搭建vuepress站点

    vuePress是什么? VuePress 俺简单介绍下,是国内有名大神的尤雨溪发布的全新基于 vue 静态网站的生成器,内置的有 webpack组件,可以拿来写文档,主要是md格式.做出的感觉就是简 ...

  5. 手摸手带你理解 进制 字节 ASCII码 Unicode 与 字节编码(UTF-8 /16)等(下)

    手摸手带你理解 进制 字节 ASCII码 Unicode 与 字节编码(UTF-8 /16)等(上) Unicode 先讲讲这个东西的规则 Unicode 通常(不是所有)用两个字节来表示 一个字符 ...

  6. 手摸手带你用实现vue全屏loading插件

    手摸手带你用实现vue全屏loading插件 前言: 由于我们打开网页时,浏览器与服务器交互需要时间,受限于宽带以及服务器性能,导致用户在访问一个网页时,往往需要一个等待期,才能在浏览器中真正完全展示 ...

  7. 《手摸手带你学ClickHouse》之Oracle同步数据到Clickhouse

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 chaodev 即可关注. 文章目录 前文回顾: <手摸手带你学ClickHouse>之安装部署 <手摸手带你学Cl ...

  8. 手摸手带你学移动端WEB开发

    HTML常用标签总结 手摸手带你学CSS HTML5与CSS3知识点总结 好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP ...

  9. 《手摸手带你学ClickHouse》之安装部署

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 chaodev 即可关注. 文章目录 1.Clickhouse简介 1.1 简介 1.2 应用场景 1.3 架构 2. ClickHo ...

最新文章

  1. 转:【AI每日播报】从TensorFlow到Theano:横向对比七大深度学习框架
  2. KVM中ioeventfd创建与触发的大致流程(十四)
  3. JSP+MySQL实例
  4. .NET Remoting
  5. 微软project服务器搭建,Project Professional 版本(Project Server 2010 设置)
  6. 实施vertex compression所遇到的各种问题和解决办法
  7. 三层神经网络实现手写字母的识别(基于tensorflow)
  8. X3平台制程及工卡开立设计说明
  9. android sqlite数据库 emoji表情,Android的Emoji表情
  10. zebra 斑马打印机 打印图片
  11. nssa和stub_ospf协议stub和nssa区域说明
  12. 华为2018届校园招聘笔试题目以及相应代码分享 软件开发岗位
  13. 华为防火墙配置命令大全,超级详细
  14. 企业微信服务商第三方应用开发流程
  15. php日程提醒,PHPOA日程安排系统,建立井然有序的工作计划
  16. 自己写个基金分析系统,准确率也太高了
  17. 趣味计算机专业比赛,你hua我猜 以梦为马——计算机科学系第三届你画我猜趣味比赛...
  18. 紫砂壶的起源 计算机操作题,紫砂壶的起源与历史发展你知道吗?
  19. 上传服务器后字体文件丢失,详解Vue+elementUI build打包部署后字体图标丢失问题...
  20. DOM案例练习-推荐几个DOM小案例练习有示例代码

热门文章

  1. 计算机数控编程特点,什么是数控图像编程系统有哪些特点
  2. nodejs+vue+elementui图书在线阅读网站系统express
  3. 写给 Linux 初学者的一封信
  4. 在64位的Linux系统使用gcc的-m32选项编译32位的程序得到了多余的代码(多余指令call和add)、有多余的.text.__x86.get_pc_thunk.ax
  5. Python基础——局域网攻防(ARP原理及应用)
  6. deepin 安装vscode
  7. Excel VBA 中有关使用 UBound + CurrentRegion 提示类型不匹配的问题及解决方案
  8. 使用Python播放MIDI音符
  9. Hermez官方文档翻译(二)开发者-开发指南
  10. 视频画面显示单位fps与Hz的区别