【秒杀系统】秒杀系统和拓展优化(1)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 【秒杀系统】秒杀系统和拓展优化(1)

高并发秒杀系统

分析需求

场景分析


秒杀时大量用户会在同一时间同时进行抢购,网站瞬时访问流量激增。

秒杀一般是访问请求数量远远大于库存数量,只有少部分用户能够秒杀成功。

秒杀业务流程比较简单,一般就是下订单减库存。

问题分析


秒杀系统一般要注意的问题就是 :


库存少卖,超卖问题(原子性)

流量削峰,这里我们设定的时候每个用户只能秒杀一次所以比较好处理

执行流程


初始化数据,提前预热要秒杀的商品(项目里设置为启动,如果秒杀列表有就预热)

使用 redis 缓存秒杀的商品信息,使用redis来承担秒杀的压力最后生产秒杀到的用户,再到mysql生成订单

在秒杀时使用(事务,分布式锁两种方式都实现)对商品库存,保证原子性

设计思路图

2.png



秒杀系统

技术选型

开发语言 : 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这样的分层开发模式

3.png



实体类


这里为了方面展示,就展示实体类的属性,小伙伴们自行补充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";
    }
}


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
16天前
|
缓存 前端开发 NoSQL
如何设计一个秒杀系统?
本文详细介绍了秒杀系统的原理与设计方法,包括高性能、一致性、高可用性和可扩展性等方面的要求。文中通过前端和后端的设计方案,探讨了如何实现秒杀系统的高并发处理,例如页面静态化、限流、降级策略及缓存优化等。此外,还分享了实际项目中的库存系统架构设计经验,并提供了面试中如何回答此类问题的建议。
20 2
|
16天前
|
缓存 NoSQL 应用服务中间件
【开发系列】秒杀系统的设计
【开发系列】秒杀系统的设计
|
5月前
|
监控 NoSQL Java
记一次线上商城系统高并发的优化
记一次线上商城系统高并发的优化
144 0
|
消息中间件 缓存 NoSQL
如何设计一个秒杀系统???
如何设计一个秒杀系统???
179 0
|
消息中间件 缓存 JavaScript
如何设计一个秒杀系统
如何设计一个秒杀系统
|
缓存 NoSQL 安全
秒杀系统的设计思路
你好看官,里面请!今天笔者讲的是秒杀系统的设计思路。不懂或者觉得我写的有问题可以在评论区留言,我看到会及时回复。 注意:本文仅用于学习参考,不可用于商业用途,如需转载请跟我联系。
422 2
|
消息中间件 缓存 运维
如何设计一个秒杀系统(下)
这里我们讲解最后一部分
316 0
如何设计一个秒杀系统(下)
|
数据采集 缓存 前端开发
如何设计一个秒杀系统(上)
秒杀大家都不陌生。自2011年首次出现以来,无论是双十一购物还是 12306 抢票,秒杀场景已随处可见。简单来说,秒杀就是在同一时刻大量请求争抢购买同一商品并完成交易的过程。从架构视角来看,秒杀系统本质是一个高性能、高一致、高可用的三高系统。而打造并维护一个超大流量的秒杀系统需要进行哪些关注,就是本文讨论的话题。
522 0
如何设计一个秒杀系统(上)
|
SQL 存储 缓存
如何设计一个秒杀系统(中)
我们接着上篇继续讲,这篇主要讲一致性
286 0
|
缓存 NoSQL 前端开发
【秒杀系统】秒杀系统和拓展优化(2)
【秒杀系统】秒杀系统和拓展优化(2)
【秒杀系统】秒杀系统和拓展优化(2)