高并发秒杀系统
分析需求
场景分析
秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。
秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。
秒杀业务流程比较简单,一般就是下订单减库存。
问题分析
秒杀系统一般要注意的问题就是 :
库存少卖,超卖问题(原子性)
流量削峰,这里我们设定的时候每个用户只能秒杀一次所以比较好处理
执行流程
初始化数据,提前预热要秒杀的商品(项目里设置为启动,如果秒杀列表有就预热)
使用 redis 缓存秒杀的商品信息,使用redis来承担秒杀的压力最后生产秒杀到的用户,再到mysql生成订单
在秒杀时使用(事务,分布式锁两种方式都实现)对商品库存,保证原子性
设计思路图
秒杀系统
技术选型
开发语言 : Java 8 或 Java 11
框架技术: SpringBoot2.x ,Mybatis-plus ,Thymeleaf
中间件: Redis
数据库:MySQL 8.0
数据源: druid 1.16
测试工具: apache jmeter
数据库表设计
三张表,分别是
商品表: id 商品id 商品name 商品图片 商品类别 商品价格 库存
秒杀商品表 : id 商品id 秒杀开始时间 秒杀结束时间 秒杀价 可秒杀的数量
订单表 id 订单id 商品id 秒杀价格 用户id 地址 电话
sql表
CREATE DATABASE /*!32312 IF NOT EXISTS*/`seckill` /*!40100 DEFAULT CHARACTER SET utf8 */ USE `seckill`; DROP TABLE IF EXISTS `goods`; CREATE TABLE `goods` ( `id` int NOT NULL AUTO_INCREMENT, `goods_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品id', `goods_name` varchar(100) DEFAULT NULL COMMENT '商品名字', `goodtype` varchar(100) DEFAULT NULL COMMENT '商品类别', `price` double DEFAULT NULL COMMENT '商品价格', `img_path` varchar(200) DEFAULT NULL COMMENT '商品图片地址', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb3; /*Data for the table `goods` */ insert into `goods`(`id`,`goods_id`,`goods_name`,`goodtype`,`price`,`img_path`) values (5,'1001','华为手机','电子用品',5000,'/img/huaweiphone.jpg'); /*Table structure for table `goodsorder` */ DROP TABLE IF EXISTS `goodsorder`; CREATE TABLE `goodsorder` ( `id` int NOT NULL AUTO_INCREMENT, `order_id` varchar(100) DEFAULT NULL, `user_id` varchar(100) DEFAULT NULL, `goods_id` varchar(100) DEFAULT NULL, `telephone` varchar(20) DEFAULT NULL, `address` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb3; /*Data for the table `goodsorder` */ /*Table structure for table `seckillgoods` */ DROP TABLE IF EXISTS `seckillgoods`; CREATE TABLE `seckillgoods` ( `id` bigint NOT NULL AUTO_INCREMENT, `goods_id` varchar(100) DEFAULT NULL, `seckill_price` double DEFAULT NULL COMMENT '秒杀价格', `stock_num` int DEFAULT NULL COMMENT '秒杀库存', `start_time` datetime DEFAULT NULL COMMENT '开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3; /*Data for the table `seckillgoods` */ insert into `seckillgoods`(`id`,`goods_id`,`seckill_price`,`stock_num`,`start_time`,`end_time`) values (1,'1001',3599,10,'2022-03-07 00:00:00','2022-03-09 00:00:00');
项目配置文件
application.yml
spring: datasource: username: root password: root url: jdbc:mysql://localhost:3306/seckill? userSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 driver-class-name: com.mysql.cj.jdbc.Driver redis: database: 0 host: 120.79.14.203 port: 6379 password: root timeout: 1000 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mybatis-plus: configuration: map-underscore-to-camel-case: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # configuration: # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath:mapper/*
redis 配置类
@Configuration public class RedisConfig { /** * @author 冷环渊 Doomwatcher * @context:redis 存储数据 序列化方式 * @date: 2022/3/7 21:19 * @param redisConnectionFactory * @return: org.springframework.data.redis.core.RedisTemplate<java.lang.String, java.lang.Object> */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //声明 key 和 value 的序列化方式 StringRedisSerializer keySerializer = new StringRedisSerializer(); GenericFastJsonRedisSerializer valueSerializer = new GenericFastJsonRedisSerializer(); redisTemplate.setKeySerializer(keySerializer); redisTemplate.setValueSerializer(valueSerializer); redisTemplate.setHashKeySerializer(keySerializer); redisTemplate.setValueSerializer(valueSerializer); //多种的序列化方式 最好是谁序列化的谁处理 //GenericFastJsonRedisSerializer //Jackson2JsonRedisSerializer return redisTemplate; } }
redis工具类
@Configuration public class RedisConfig { /** * @author 冷环渊 Doomwatcher * @context:redis 存储数据 序列化方式 * @date: 2022/3/7 21:19 * @param redisConnectionFactory * @return: org.springframework.data.redis.core.RedisTemplate<java.lang.String, java.lang.Object> */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //声明 key 和 value 的序列化方式 StringRedisSerializer keySerializer = new StringRedisSerializer(); GenericFastJsonRedisSerializer valueSerializer = new GenericFastJsonRedisSerializer(); redisTemplate.setKeySerializer(keySerializer); redisTemplate.setValueSerializer(valueSerializer); redisTemplate.setHashKeySerializer(keySerializer); redisTemplate.setValueSerializer(valueSerializer); //多种的序列化方式 最好是谁序列化的谁处理 //GenericFastJsonRedisSerializer //Jackson2JsonRedisSerializer return redisTemplate; } }
商品列表
商品列表界面,是比较简单的,设计思路还是基于传统的三层开发
Controller -> servier -> dao这样的分层开发模式
实体类
这里为了方面展示,就展示实体类的属性,小伙伴们自行补充getter/setter以及构造方法等
public class goods { @Id @TableId(type = IdType.AUTO) private Integer id; private String goods_id; private String goods_name; private String goodtype; private double price; private String img_path; @TableField(exist = false) private SeckillGoods seckillgoods; }
DAO
虽然使用了 mybatis-plus 但是我们还是根据自己的查询需求新增了自己的方法,
@Repository public interface GoodsMapper extends BaseMapper<goods> { goods getGoodsByGoodsId(String goodId); List<goods> selectGoods(); List<goods> getGoodList(); }
映射文件
<?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="com.hyc.seckillsystem.dao.GoodsMapper"> <select id="getGoodsByGoodsId" resultType="com.hyc.seckillsystem.pojo.goods"> select seckill.goods.goods_id, seckill.goods.goods_name, seckill.goods.goodtype, seckill.goods.price, seckill.goods.img_path from seckill.goods where goods_id = #{goodId}; </select> <select id="getGoodList" resultType="com.hyc.seckillsystem.pojo.goods"> select seckill.goods.goods_id, seckill.goods.goods_name, seckill.goods.goodtype, seckill.goods.price, seckill.goods.img_path from seckill.goods </select> <resultMap id="goodsResult" type="com.hyc.seckillsystem.pojo.goods"> <id property="id" column="id"/> <result property="goods_id" column="goods_id"/> <result property="goods_name" column="goods_name"/> <result property="goodtype" column="goods_type"/> <result property="price" column="price"/> <result property="img_path" column="img_path"/> <association property="seckillgoods" javaType="com.hyc.seckillsystem.pojo.SeckillGoods"> <result property="seckill_price" column="seckill_price"/> <result property="stock_num" column="stock_num"/> </association> </resultMap> <select id="selectGoods" resultMap="goodsResult"> SELECT a.`goods_id`, a.`goods_name`, a.`goodtype`, a.`price`, a.`img_path`, b.`seckill_price`, b.`stock_num` FROM seckill.goods AS a LEFT JOIN seckill.seckillgoods AS b ON a.`goods_id` = b.`goods_id` </select> </mapper>
service 实现类
两种查找商品数据的方法
getlist:逐个表 匹配查询
selectGoods链表查询
返回商品详情(包括秒杀价格等)VO getGoodsDetail(String goodId)
service 层的设计思路就是 调用DAO层接口 实现对数据库中取出数据的处理,并且提供给controller封装好的接口
@Service public class GoodsServiceImpl implements GoodsService { @Autowired GoodsMapper goodsMapper; @Autowired SeckillGoodsMapper seckillGoodsMapper; /** * @author 冷环渊 Doomwatcher * @context:逐个表 匹配查询 * @date: 2022/3/7 16:20 * @param * @return: java.util.List<com.hyc.seckillsystem.vo.GoodVo> */ @Override public List<GoodVo> getlist() { /* 两种方式 * 每个表单独查询 * 从第一个表查多个数据,再根据数据去第二个表中查询 * */ List<goods> goods = goodsMapper.getGoodList(); ArrayList<GoodVo> result = new ArrayList<>(); for (goods good : goods) { Map<String, Object> map = new HashMap(); map.put("goods_id", good.getGoods_id()); SeckillGoods seckillGoods = seckillGoodsMapper.getSeckillByGoodsId(good.getGoods_id()); GoodVo vo = new GoodVo(); vo.setGoodsId(good.getGoods_id()); vo.setGoodsName(good.getGoods_name()); vo.setGoodType(good.getGoodtype()); vo.setPrice(good.getPrice()); vo.setImgPath(good.getImg_path()); vo.setSeckillPrice(seckillGoods.getSeckill_price()); vo.setStockNum(seckillGoods.getStock_num()); result.add(vo); } return result; } @Override //返回秒杀商品详情页面 public GoodsDetailVo getGoodsDetail(String goodId) { SeckillGoods seckillGoods = seckillGoodsMapper.getSeckillByGoodsId(goodId); goods good = goodsMapper.getGoodsByGoodsId(goodId); //商品表信息1 GoodsDetailVo goodsDetailVo = new GoodsDetailVo(); goodsDetailVo.setGoodsId(good.getGoods_id()); goodsDetailVo.setGoodsName(good.getGoods_name()); goodsDetailVo.setGoodType(good.getGoodtype()); goodsDetailVo.setPrice(good.getPrice()); goodsDetailVo.setImgPath(good.getImg_path()); //秒杀表信息 goodsDetailVo.setSeckillPrice(seckillGoods.getSeckill_price()); goodsDetailVo.setStockNum(seckillGoods.getStock_num()); goodsDetailVo.setStartTime(seckillGoods.getStart_time()); goodsDetailVo.setEndTime(seckillGoods.getEnd_time()); return goodsDetailVo; } /** * @author 冷环渊 Doomwatcher * @context: 联表查询 * @date: 2022/3/7 16:20 * @param * @return: com.hyc.seckillsystem.vo.GoodVo */ @Override public List<GoodVo> selectGoods() { List<goods> goods = goodsMapper.selectGoods(); ArrayList<GoodVo> result = new ArrayList<>(); for (goods good : goods) { GoodVo vo = new GoodVo(); vo.setGoodsId(good.getGoods_id()); vo.setGoodsName(good.getGoods_name()); vo.setGoodType(good.getGoodtype()); vo.setPrice(good.getPrice()); vo.setImgPath(good.getImg_path()); vo.setSeckillPrice(good.getSeckillGoods().getSeckill_price()); vo.setStockNum(good.getSeckillGoods().getStock_num()); result.add(vo); } return result; } }
Controller
Controller层,负责控制接口数据和渲染界面的值传递 , 跳转 , 这里基本上不包含业务代码
尽可能的再service层 封装好方法 让Controller只负责调用
@Controller @Slf4j public class GoodsController { @Autowired GoodsService goodsService; @GetMapping(value = "/index") public String list(Model model) { // 逐层调用 List<GoodVo> getlist = goodsService.getlist(); model.addAttribute("goodslist", getlist); return "list"; } @GetMapping(value = "/goodDetail/{goodsId}") public String getGoodDetail(@PathVariable String goodsId, Model model) { GoodsDetailVo goodsDetail = goodsService.getGoodsDetail(goodsId); model.addAttribute("GoodsDetail", goodsDetail); //获取到时间 Date startTime = goodsDetail.getStartTime(); Date endTime = goodsDetail.getEndTime(); Date nowtime = new Date(); /*设置状态值 * 0 未开始 * 1 正在秒杀 * 2 秒杀结束 * */ int status; int remainSeconds = -1; if (nowtime.before(startTime)) { // 秒杀未开始 status = 0; remainSeconds = (int) ((startTime.getTime() - nowtime.getTime()) / 1000); } else if (nowtime.after(endTime)) { //秒杀已经结束 status = 2; } else { //正在秒杀中 status = 1; } model.addAttribute("status", status); model.addAttribute("remainSeconds", remainSeconds); return "Detail"; } }