高并发-秒杀(1)

  • 秒杀就是抢购的功能,会有很多人抢购,所以要求扛得住高并发

  • 秒杀的商品信息存入Redis缓存(因为访问数据量大,所以不直接对数据库进行操作)

    秒杀商品存入Redis来提升访问速度。
    1.秒杀列表数据
    2.秒杀详情页数据
    
  • spring定时任务-定时将秒杀商品存入Redis中。

    定时将秒杀商品存入Redis缓存
    
  • 秒杀商品频道页实现-秒杀商品列表页。(从Redis里面查数据)

  • 秒杀商品详情页实现(从Redis里面取数据)

  • 下单实现(普通下单,订单添加到数据库,对并发支持不好,容易造成雪崩)

  • 多线程异步抢单实现-队列削峰(普通下单在并发上不靠谱,所以要队列削峰)

一、秒杀业务分析

秒杀就是平台上的商品低价出售的一些抢购活动。(如:淘宝的双11)

所以访问的人数很多 ,对服务器压力大,所以需要实现高并发。

需求:

(1)录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍、秒杀时段等信息
(2)秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详细页。
(3)商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为0或不在活动期范围内时无法秒杀。
(4)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
(5)当用户秒杀下单5分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。
二、表结构

秒杀商品表(商品信息)

CREATE TABLE `tb_seckill_goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `sup_id` bigint(20) DEFAULT NULL COMMENT 'spu ID',
  `sku_id` bigint(20) DEFAULT NULL COMMENT 'sku ID',
  `name` varchar(100) DEFAULT NULL COMMENT '标题',
  `small_pic` varchar(150) DEFAULT NULL COMMENT '商品图片',
  `price` decimal(10,2) DEFAULT NULL COMMENT '原价格',
  `cost_price` decimal(10,2) DEFAULT NULL COMMENT '秒杀价格',
  `create_time` datetime DEFAULT NULL COMMENT '添加日期',
  `check_time` datetime DEFAULT NULL COMMENT '审核日期',
  `status` char(1) DEFAULT NULL COMMENT '审核状态,0未审核,1审核通过,2审核不通过',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '结束时间',
  `num` int(11) DEFAULT NULL COMMENT '秒杀商品数',
  `stock_count` int(11) DEFAULT NULL COMMENT '剩余库存数',
  `introduction` varchar(2000) DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

秒杀订单表(订单信息)

CREATE TABLE `tb_seckill_order` (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `seckill_id` bigint(20) DEFAULT NULL COMMENT '秒杀商品ID',
  `money` decimal(10,2) DEFAULT NULL COMMENT '支付金额',
  `user_id` varchar(50) DEFAULT NULL COMMENT '用户',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `status` char(1) DEFAULT NULL COMMENT '状态,0未支付,1已支付',
  `receiver_address` varchar(200) DEFAULT NULL COMMENT '收货人地址',
  `receiver_mobile` varchar(20) DEFAULT NULL COMMENT '收货人电话',
  `receiver` varchar(20) DEFAULT NULL COMMENT '收货人',
  `transaction_id` varchar(30) DEFAULT NULL COMMENT '交易流水',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
三、需求分析


1、搭建秒杀微服务工程

配置文件application.yml

server:
  port: 18091
spring:
  application:
    name: seckill
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://服务器IP:3306/数据库?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: 123456
  rabbitmq:
    host: 服务器IP地址 #mq的服务器地址
    username: guest #账号
    password: guest #密码
  main:
    allow-bean-definition-overriding: true
  redis:
    host: 192.168.142.111
    port: 6379
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        timeout:
          #如果enabled设置为false,则请求超时交给ribbon控制
          enabled: true
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE

启动类SeckillApplication

/**
 * @Author TeaBowl
 * @Date 2021/2/14 18:47
 * @Version 1.0
 */
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
@EnableScheduling
public class SeckillApplication {


    public static void main(String[] args) {
        SpringApplication.run(SeckillApplication.class,args);
    }

    @Bean
    public IdWorker idWorker(){
        return new IdWorker(1,1);
    }
}

用代码生成工具生成controller、service、dao、pojo,这步就不赘述了。

2、定时任务

我们采用Spring的定时任务定时将符合参与秒杀的商品查询出来再存入到Redis缓存,所以这里需要使用到定时任务。

这里我们了解下定时任务相关的配置,配置步骤如下:

1)在定时任务类的指定方法上加上@Scheduled开启定时任务
2)定时任务表达式:使用cron属性来配置定时任务执行时间

2.1 定时任务方法配置

/**
 * @Author TeaBowl
 * @Date 2021/2/18 13:18
 * @Version 1.0
 * 定时任务
 * 定时将秒杀商品存入Redis缓存
 */
@Component
public class SeckillGoodsPushTask {
    /**
     * 定时操作
     * 从第0秒开始,每5秒执行一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void loadGoodsPushRedis(){
        System.out.println("task demo");
    }
}

2.2 定时任务常用时间表达式

CronTrigger配置完整格式为:秒、分、小时、日、月、周、年

序号说明是否必填允许填写的值允许的通配符
10-59, - * /
20-59, - * /
3小时0-23, - * /
41-31, - * ? / L W
51-12或JAN-DEC, - * /
61-7或SUN-SAT, - * ? / L W
7empty 或1970-2099, - * /

使用说明:

通配符说明:
* 表示所有值. 例如:在分的字段上设置 "*",表示每一分钟都会触发。

? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。

例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为"?" 具体设置为 0 0 0 10 * ?

- 表示区间。例如 在小时上设置 "10-12",表示 10,11,12点都会触发。

, 表示指定多个值,例如在周字段上设置 "MON,WED,FRI" 表示周一,周三和周五触发  12,14,19

/ 用于递增触发。如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)。 在月字段上设置'1/3'所示每月1号开始,每隔三天触发一次。

L 表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于"7"或"SAT"。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示“本月最后一个星期五"

W 表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 "1W",它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,"W"前只能设置具体的数字,不允许区间"-").

# 序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六.注意如果指定"#5",正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;

常用表达式

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 
0 0 12 ? * WED 表示每个星期三中午12点 
"0 0 12 * * ?" 每天中午12点触发 
"0 15 10 ? * *" 每天上午10:15触发 
"0 15 10 * * ?" 每天上午10:15触发 
"0 15 10 * * ? *" 每天上午10:15触发 
"0 15 10 * * ? 2005" 2005年的每天上午10:15触发 
"0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发 
"0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发 
"0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 
"0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 
"0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 
"0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发 
"0 15 10 15 * ?" 每月15日上午10:15触发 
"0 15 10 L * ?" 每月最后一日的上午10:15触发 
"0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 
"0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发

2.3 秒杀商品压入缓存实现

2.3.1 数据检索条件分析

按照2.1中的几个步骤实现将秒杀商品从数据库中查询出来,并存入到Redis缓存

1.查询活动没结束的所有秒杀商品
	1)计算秒杀时间段
	2)状态必须为审核通过 status=1
	3)商品库存个数>0
	4)活动没有结束  endTime>=now()
	5)在Redis中没有该商品的缓存
	6)执行查询获取对应的结果集
2.将活动没有结束的秒杀商品入库

上面这里会涉及到时间操作,所以这里提前准备了一个时间工具包DateUtil。

2.3.2 时间菜单分析

我们将商品数据从数据库中查询出来,并存入Redis缓存,但页面每次显示的时候,只显示当前正在秒杀以及往后延时2个小时、4个小时、6个小时、8个小时的秒杀商品数据。我们要做的第一个事是计算出秒杀时间菜单,这个菜单是从后台获取的。

这个时间菜单的计算我们来分析下,可以先求出当前时间的凌晨,然后每2个小时后作为下一个抢购的开始时间,这样可以分出12个抢购时间段,如下:

00:00-02:00
02:00-04:00
04:00-06:00
06:00-08:00
08:00-10:00
10:00-12:00
12:00-14:00
14:00-16:00
16:00-18:00
18:00-20:00
20:00-22:00
22:00-00:00

而现实的菜单只需要计算出当前时间在哪个时间段范围,该时间段范围就属于正在秒杀的时间段,而后面即将开始的秒杀时间段的计算也就出来了,可以在当前时间段基础之上+2小时、+4小时、+6小时、+8小时。

关于时间菜单的运算,在给出的DateUtil包里已经实现,代码如下:

/***
 * 获取时间菜单
 * @return
 */
public static List<Date> getDateMenus(){
    //定义一个List<Date>集合,存储所有时间段
    List<Date> dates = getDates(12);
    //判断当前时间属于哪个时间范围
    Date now = new Date();
    for (Date cdate : dates) {
        //开始时间<=当前时间<开始时间+2小时
        if(cdate.getTime()<=now.getTime() && now.getTime()<addDateHour(cdate,2).getTime()){
            now = cdate;
            break;
        }
    }

    //当前需要显示的时间菜单
    List<Date> dateMenus = new ArrayList<Date>();
    for (int i = 0; i <5 ; i++) {
        dateMenus.add(addDateHour(now,i*2));
    }
    return dateMenus;
}

/***
 * 指定时间往后N个时间间隔
 * @param hours
 * @return
 */
public static List<Date> getDates(int hours) {
    List<Date> dates = new ArrayList<Date>();
    //循环12次
    Date date = toDayStartHour(new Date()); //凌晨
    for (int i = 0; i <hours ; i++) {
        //每次递增2小时,将每次递增的时间存入到List<Date>集合中
        dates.add(addDateHour(date,i*2));
    }
    return dates;
}

2.3.3 查询秒杀商品导入Redis
我们可以写个定时任务,查询从当前时间开始,往后延续4个时间菜单间隔,也就是一共只查询5个时间段抢购商品数据,并压入缓存,实现代码如下:

/**
 * @Author TeaBowl
 * @Date 2021/2/18 13:18
 * @Version 1.0
 * 定时任务
 * 定时将秒杀商品存入Redis缓存
 *
 * 1. 查询符合当前秒杀商品的时间菜单
 * 2. 秒杀商品库存>0 stock_count
 * 3. 审核状态->审核通过  status:1
 * 4. 开始时间 start_time,结束时间end_time
 *      时间菜单的开始时间=<start_time && end_time<时间菜单的开始时间+2小时
 *
 *   时间:时间菜单
 *      求出整个时间菜单
 *      确定每个时间菜单的区间值 如:14-16
 *      根据菜单时间的区间值,求对应的秒杀商品数据
 *      将对应的时间区间的秒杀商品数据,存入到Redis
 */
@Component
public class SeckillGoodsPushTask {

    @Autowired
    private SeckillGoodsMapper seckillGoodsMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 定时操作
     * 从第0秒开始,每5秒执行一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void loadGoodsPushRedis(){
        //1. 查询符合当前秒杀商品的时间菜单
        //获取时间菜单   5个区间
        List<Date> dateMenus = DateUtil.getDateMenus();
        //循环查询每个时间区间的秒杀商品
        for (Date dateMenu : dateMenus) {
            //时间的字符串格式  时间->字符串
            String timeSpace = "SeckillGoods_"+DateUtil.data2str(dateMenu, "yyyyMMddHH");
            //System.out.println("timeSpace:"+timeSpace);

            //创建模板
            Example example = new Example(SeckillGoods.class);
            //创建条件
            Example.Criteria criteria = example.createCriteria();
            //构建条件
            //2. 设置审核状态:1  审核通过
            criteria.andEqualTo("status", "1");
            //3. 秒杀商品库存:剩余库存>0
            criteria.andGreaterThan("stockCount",0);
            //4.开始时间 start_time,结束时间end_time
            // 时间菜单的开始时间=<start_time
            criteria.andGreaterThanOrEqualTo("startTime",dateMenu);
            //end_time<时间菜单的开始时间+2小时
            criteria.andLessThan("endTime",DateUtil.addDateHour(dateMenu,2));

            /**
             * 排除已经存入Redis中的秒杀商品seckillGoods
             * 获取当前命名空间下所有的key(商品id)
             * 每次添加时,根据key,排除已经存在的秒杀商品
             */

            //获取命名空间中所有的keys(商品id)
            Set keys = redisTemplate.boundHashOps(timeSpace).keys();
            //如果有数据,排除已经存入Redis的数据
            if (keys!=null&&keys.size()>0){
                //排除
                criteria.andNotIn("id",keys);
            }


            //查询数据  根据构建的模板信息,查询
            List<SeckillGoods> seckillGoods = seckillGoodsMapper.selectByExample(example);
            //System.out.println("seckillGoods:"+seckillGoods);

            //秒杀商品循环存入Redis
            for (SeckillGoods seckillGood : seckillGoods) {

                System.out.println("商品id:"+seckillGood.getId()+"-----存入到了Redis-----"+timeSpace);
                /**
                 * 数据存入到Redis
                 * .boundHashOps(命名空间)
                 * .put(商品id,商品信息)
                 */
                redisTemplate.boundHashOps(timeSpace).put(seckillGood.getId(),seckillGood);
            }
        }
    }
}

Redis数据如下:

3、秒杀频道页

3.1 根据时间区间查询秒杀商品频道列表数据

service业务层接口:

 /**
  * 根据时间区间查询秒杀商品频道列表数据
  * @param time
  * @return
  */
List<SeckillGoods> list(String time);

service业务层实现类:

@Autowired
private RedisTemplate redisTemplate;

/**
 * 根据时间区间查询秒杀商品频道列表数据
 * @param time
 * @return
 */
 @Override
 public List<SeckillGoods> list(String time) {
   //获取Redis中,相应时间下的所有秒杀商品数据
   return redisTemplate.boundHashOps("SeckillGoods_"+time).values();
 }

controller控制层:

/**
 * 根据时间区间查询秒杀商品频道列表数据
 * @param time
 * @return
 */
@GetMapping(value = "/list")
public Result<List<SeckillGoods>> list(String time){
    //调用Service查询
    List<SeckillGoods> seckillGoods = seckillGoodsService.list(time);
    return new Result<List<SeckillGoods>>(true,StatusCode.OK,"秒杀商品列表数据查询成功!",seckillGoods);
}

浏览器输入:http://localhost:18091/seckillGoods/list?time=2021021912
查询结果:

3.2 查询秒杀商品时间菜单

/**
 * 查询秒杀商品时间菜单
 * @return
 */
@GetMapping(value = "/menus")
public Result<List<Date>> menus(){
    //使用DateUtil工具查询时间菜单
    List<Date> dateMenus = DateUtil.getDateMenus();
    return new Result<>(true,StatusCode.OK,"查询秒杀时间菜单成功!",dateMenus);
}

浏览器输入:http://localhost:18091/seckillGoods/menus
查询结果:

3.3 秒杀商品详情获取

a.确定命名空间
b.根据key(商品id)获取value(商品信息)

根据时间查询秒杀商品信息

service应用层接口:

/**
     * 根据时间和秒杀商品id查询秒杀商品信息
     * @param time
     * @param id
     * @return
     */
SeckillGoods one(String time,Long id);

service应用层实现:

/**
     * 根据时间和秒杀商品id查询秒杀商品信息
     * @param time
     * @param id
     * @return
     */
@Override
public SeckillGoods one(String time, Long id) {
    return  (SeckillGoods) redisTemplate.boundHashOps("SeckillGoods_"+time).get(id);
}

controller控制层:

 /**
     * 根据时间和秒杀商品id查询秒杀商品信息
     * @param time
     * @param id
     * @return
     */
@GetMapping(value = "/one")
public Result<SeckillGoods> one(String time, Long id) {
    SeckillGoods seckillGoods = seckillGoodsService.one(time, id);
    return new Result<>(true, StatusCode.OK, "查询秒杀商品详情成功!", seckillGoods);
}

浏览器输入:http://localhost:18091/seckill/goods/one?id=1131814837488324608&time=2021022020
查询结果:

4、秒杀商品下单实现

4.1 方式一:直接下单【淘汰】

控制层controller:

/**
 * 秒杀订单相关操作
 */

@RestController
@RequestMapping("/seckillOrder")
@CrossOrigin
public class SeckillOrderController {

    @Autowired
    private SeckillOrderService seckillOrderService;
    
	/**
     * 根据时间区间和商品id添加订单
     * @param time:时间区间
     * @param id:商品id
     * @return
     */
    public Result add(String time,Long id){
        //模拟用户登录信息
        String username = "acd";
        seckillOrderService.add(time,id,username);
        return new Result(true, StatusCode.OK,"下单成功!");
    }

}

应用层Service接口:

/**
     * 添加订单
     * @param time:时间区间
     * @param id:商品id
     * @param username:用户登录名
     */
void add(String time, Long id, String username);

应用层实现类:

@Autowired
private SeckillOrderMapper seckillOrderMapper;

@Autowired
private SeckillGoodsMapper seckillGoodsMapper;

@Autowired
private RedisTemplate redisTemplate;
/**
     * 添加订单:
     * @param time:时间区间
     * @param id:商品id
     * @param username:用户登录名
     */
@Override
public Boolean add(String time, Long id, String username) {
    //查询秒杀商品    命名空间
    String namespace = "SeckillGoods_"+time;
    //根据命名空间和商品id,查询商品
    SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace).get(id);

    //判断有没有库存 (没查到 或 库存数量少于1)
    if (seckillGoods==null||seckillGoods.getStockCount()<1){
        //如果没有库存
        throw new RuntimeException("已售罄!");
    }


    //创建订单对象    打包订单信息
    SeckillOrder seckillOrder = new SeckillOrder();
    //商品id
    seckillOrder.setSeckillId(id);
    //支付金额  秒杀价格:seckillGoods.getCostPrice()
    seckillOrder.setMoney(seckillGoods.getCostPrice());
    //用户登录名
    seckillOrder.setUserId(username);
    //订单创建时间
    seckillOrder.setCreateTime(new Date());
    //订单支付状态    0未支付,1已支付
    seckillOrder.setStatus("0");


    /**
         * 将订单对象存储起来
         * 分析:
         * 1.一个用户只允许有一个未支付秒杀订单
         * 2.订单存入Redis
         *      类型存储:Hash
         *      命名空间:namespace->SeckillOrder
         *      每个用户的订单:
         *          key:username
         *          value:SeckillOrder
         */
    redisTemplate.boundHashOps("SeckillOrder").put(username,seckillOrder);

    /**
         * 库存递减
         * 分析:
         * 1.商品信息(SeckillGoods)中有个字段->剩余库存数量:stockCount
         * 2.商品有可能是最后一个,则在Redis中删除此商品的商品信息,并同步到MySQL
         *      即:秒杀只能抢购1件,如果库存为1,被一位用户抢购并下订单之后,
         *      就在Redis和Mysql中删除此商品的信息,表示已经没有库存了
         */

    //秒杀只能抢一件商品,所以商品库存数量减1
    seckillGoods.setStockCount(seckillGoods.getStockCount()-1);

    //如果此时商品被抢购完了,库存<1,也就是没有库存了
    if (seckillGoods.getStockCount()<1){
        //同步数据到MySQL
        seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);

        //删除Redis中的商品数据
        redisTemplate.boundHashOps(namespace).delete(id);

    }else {
        //同步数据到Redis
        redisTemplate.boundHashOps(namespace).put(id,seckillGoods);
    }

    return true;
}

浏览器输入:http://localhost:18091/seckillOrder/add?time=2021022222&id=1131814839010856960

然后查看商品详情,注意看库存变化:http://localhost:18091/seckill/goods/one?id=1131814839010856960&time=2021022222

查看Redis,发现多了一条订单信息

当然这种抢单方式我们不建议,有很大的弊端。它没有解决并发相关的问题
例如:并发、超卖现象,这块甚至有可能产生雪崩问题。

4.2 方式二:多线程抢单

在审视秒杀中,操作一般都是比较复杂的,而且并发量特别高,比如,检查当前账号操作是否已经秒杀过该商品,检查该账号是否存在存在刷单行为,记录用户操作日志等。

下订单这里,我们一般采用多线程下单,但多线程中我们又需要保证用户抢单的公平性,也就是先抢先下单。我们可以这样实现,用户进入秒杀抢单,如果用户复合抢单资格,只需要记录用户抢单数据,存入队列,多线程从队列中进行消费即可,存入队列采用左压,多线程下单采用右取的方式。

4.2.1 异步实现

先整一个配置类

/**
 * @Author TeaBowl
 * @Date 2021/2/23 4:32
 * @Version 1.0
 */
@Component
public class MultiThreadingCreateOrder {
    /**
     * 多线程下单
     * @Async 异步执行(底层:多线程)
     */
    @Async
    public void creatOrder(){
        try {
            System.out.println("多线程执行开始!");
            //休眠 10s
            Thread.sleep(10000);
            System.out.println("多线程执行结束!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在启动类上开启异步支持

/**
 * @Author TeaBowl
 * @Date 2021/2/14 18:47
 * @Version 1.0
 */
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
@EnableScheduling
@EnableAsync    //开启异步支持
public class SeckillApplication {


    public static void main(String[] args) {
        SpringApplication.run(SeckillApplication.class,args);
    }

    @Bean
    public IdWorker idWorker(){
        return new IdWorker(1,1);
    }
}

在秒杀订单操作的应用层实现类SeckillOrderServiceImpl中,注入多线程下单配置类MultiThreadingCreateOrder;

然后在添加订单方法中开启异步执行;

@Autowired
private MultiThreadingCreateOrder multiThreadingCreateOrder;

/**
     * 添加订单:
     * @param time:时间区间
     * @param id:商品id
     * @param username:用户登录名
     */
@Override
public Boolean add(String time, Long id, String username) {
    /**
         * 异步执行  也就是多线程方式执行
         */
    multiThreadingCreateOrder.creatOrder();

    System.out.println("不等“多线程执行结束”那句话");

    //查询秒杀商品    命名空间
    String namespace = "SeckillGoods_"+time;
    //根据命名空间和商品id,查询商品
    SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace).get(id);

    //判断有没有库存 (没查到 或 库存数量少于1)
    if (seckillGoods==null||seckillGoods.getStockCount()<1){
        //如果没有库存
        throw new RuntimeException("已售罄!");
    }


    //创建订单对象    打包订单信息
    SeckillOrder seckillOrder = new SeckillOrder();
    //商品id
    seckillOrder.setSeckillId(id);
    //支付金额  秒杀价格:seckillGoods.getCostPrice()
    seckillOrder.setMoney(seckillGoods.getCostPrice());
    //用户登录名
    seckillOrder.setUserId(username);
    //订单创建时间
    seckillOrder.setCreateTime(new Date());
    //订单支付状态    0未支付,1已支付
    seckillOrder.setStatus("0");


    /**
         * 将订单对象存储起来
         * 分析:
         * 1.一个用户只允许有一个未支付秒杀订单
         * 2.订单存入Redis
         *      类型存储:Hash
         *      命名空间:namespace->SeckillOrder
         *      每个用户的订单:
         *          key:username
         *          value:SeckillOrder
         */
    redisTemplate.boundHashOps("SeckillOrder").put(username,seckillOrder);

    /**
         * 库存递减
         * 分析:
         * 1.商品信息(SeckillGoods)中有个字段->剩余库存数量:stockCount
         * 2.商品有可能是最后一个,则在Redis中删除此商品的商品信息,并同步到MySQL
         *      即:秒杀只能抢购1件,如果库存为1,被一位用户抢购并下订单之后,
         *      就在Redis和Mysql中删除此商品的信息,表示已经没有库存了
         */

    //秒杀只能抢一件商品,所以商品库存数量减1
    seckillGoods.setStockCount(seckillGoods.getStockCount()-1);

    //如果此时商品被抢购完了,库存<1,也就是没有库存了
    if (seckillGoods.getStockCount()<1){
        //同步数据到MySQL
        seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);

        //删除Redis中的商品数据
        redisTemplate.boundHashOps(namespace).delete(id);

    }else {
        //同步数据到Redis
        redisTemplate.boundHashOps(namespace).put(id,seckillGoods);
    }

    System.out.println("执行完毕");
    return true;
}

运行查看控制台:

将SeckillOrderServiceImpl中添加订单方法中,部分代码挪到creatOrder中,稍加修改

修改后SeckillOrderServiceImpl中的添加订单方法:

/**
     * 添加订单:
     * @param time:时间区间
     * @param id:商品id
     * @param username:用户登录名
     */
    @Override
    public Boolean add(String time, Long id, String username) {
        /**
         * 异步执行  也就是多线程方式执行
         */
        multiThreadingCreateOrder.creatOrder();

        return true;
    }

修改后的MultiThreadingCreateOrder中的多线程下单方法:

@Autowired
    private SeckillGoodsMapper seckillGoodsMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IdWorker idWorker;

    /**
     * 多线程下单
     *
     * @Async 异步执行(底层:多线程)
     */
    @Async
    public void creatOrder() {
        try {
            //定义要购买的商品的id和时区以及用户登录名
            String time = "2021022316";
            Long id = 1131814839459647488L;
            String username = "acd";

            System.out.println("准备睡会儿再下单!");
            //休眠 10s
            Thread.sleep(10000);

            //查询秒杀商品    命名空间
            String namespace = "SeckillGoods_" + time;
            //根据命名空间和商品id,查询商品
            SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace).get(id);

            //判断有没有库存 (没查到 或 库存数量少于1)
            if (seckillGoods == null || seckillGoods.getStockCount() <= 0) {
                //如果没有库存
                throw new RuntimeException("已售罄!");
            }


            //创建订单对象    打包订单信息
            SeckillOrder seckillOrder = new SeckillOrder();
            //订单id
            seckillOrder.setId(idWorker.nextId());
            //商品id
            seckillOrder.setSeckillId(id);
            //支付金额  秒杀价格:seckillGoods.getCostPrice()
            seckillOrder.setMoney(seckillGoods.getCostPrice());
            //用户登录名
            seckillOrder.setUserId(username);
            //订单创建时间
            seckillOrder.setCreateTime(new Date());
            //订单支付状态    0未支付,1已支付
            seckillOrder.setStatus("0");


            /**
             * 将订单对象存储起来
             * 分析:
             * 1.一个用户只允许有一个未支付秒杀订单
             * 2.订单存入Redis
             *      类型存储:Hash
             *      命名空间:namespace->SeckillOrder
             *      每个用户的订单:
             *          key:username
             *          value:SeckillOrder
             */
            redisTemplate.boundHashOps("SeckillOrder").put(username, seckillOrder);

            /**
             * 库存递减
             * 分析:
             * 1.商品信息(SeckillGoods)中有个字段->剩余库存数量:stockCount
             * 2.商品有可能是最后一个,则在Redis中删除此商品的商品信息,并同步到MySQL
             *      即:秒杀只能抢购1件,如果库存为1,被一位用户抢购并下订单之后,
             *      就在Redis和Mysql中删除此商品的信息,表示已经没有库存了
             */

            //秒杀只能抢一件商品,所以商品库存数量减1
            seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);

            //如果此时商品被抢购完了,库存<=0,也就是没有库存了
            if (seckillGoods.getStockCount() <= 0) {
                //同步数据到MySQL
                seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);

                //删除Redis中的商品数据
                redisTemplate.boundHashOps(namespace).delete(id);

            } else {
                //同步数据到Redis
                redisTemplate.boundHashOps(namespace).put(id, seckillGoods);
            }

            System.out.println("下单完成!");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

上面“要购买的商品的id和时区以及用户登录名”都是写死的模拟数据,下面接着改造。

创建用户排队抢单信息封装类:SeckillStatus

/**
 * @Author TeaBowl
 * @Date 2021/2/23 19:41
 * @Version 1.0
 * 用户排队抢单信息封装
 */
@Data
public class SeckillStatus implements Serializable {
    //秒杀用户名
    private String username;
    //创建时间
    private Date createTime;
    //秒杀状态  1:排队中,2:秒杀等待支付(已下单),3:支付超时,4:秒杀失败,5:支付完成
    private Integer status;
    //秒杀的商品ID   用于订单库存回滚
    private Long goodsId;

    //应付金额
    private Float money;

    //订单号
    private Long orderId;
    //时间段
    private String time;

    public SeckillStatus() {
    }

    public SeckillStatus(String username, Date createTime, Integer status, Long goodsId, String time) {
        this.username = username;
        this.createTime = createTime;
        this.status = status;
        this.goodsId = goodsId;
        this.time = time;
    }
}

修改SeckillOrderServiceImpl中的添加订单方法,加入排队操作

@Autowired
    private SeckillOrderMapper seckillOrderMapper;

    @Autowired
    private MultiThreadingCreateOrder multiThreadingCreateOrder;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 添加订单:排队
     * @param time:时间区间
     * @param id:商品id
     * @param username:用户登录名
     */
    @Override
    public Boolean add(String time, Long id, String username) {
        //抢单状态信息: 1排队中
        SeckillStatus seckillStatus = new SeckillStatus(username, new Date(), 1, id, time);
        //排队  list有序队列        从左边存入  右边取出
        redisTemplate.boundListOps("SeckillOrderQueue").leftPush(seckillStatus);
        /**
         * 异步执行  也就是多线程方式执行
         */
        multiThreadingCreateOrder.creatOrder();

        return true;
    }

修改MultiThreadingCreateOrder中的多线程下单方法,动态获取”商品的id和时区以及用户登录名“。

@Autowired
    private SeckillGoodsMapper seckillGoodsMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 多线程下单
     *
     * @Async 异步执行(底层:多线程)
     */
    @Async
    public void creatOrder() {
        try {
        	System.out.println("准备睡会儿再下单!");
            //休眠 10s
            Thread.sleep(10000);

            //获取用户排队状态
            // list有序队列   从左边存入  右边取出
            SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundListOps("SeckillOrderQueue").rightPop();

            //如果没有获取到排队队列
            if (seckillStatus == null) {
                return;
            }

            //获取商品的id和时区以及用户登录名
            String time = seckillStatus.getTime();
            Long id = seckillStatus.getGoodsId();
            String username = seckillStatus.getUsername();

            //查询秒杀商品    命名空间
            String namespace = "SeckillGoods_" + time;
            //根据命名空间和商品id,查询商品
            SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace).get(id);

            //判断有没有库存 (没查到 或 库存数量少于1)
            if (seckillGoods == null || seckillGoods.getStockCount() < 1) {
                //如果没有库存
                throw new RuntimeException("已售罄!");
            }


            //创建订单对象    打包订单信息
            SeckillOrder seckillOrder = new SeckillOrder();
            //商品id
            seckillOrder.setSeckillId(id);
            //支付金额  秒杀价格:seckillGoods.getCostPrice()
            seckillOrder.setMoney(seckillGoods.getCostPrice());
            //用户登录名
            seckillOrder.setUserId(username);
            //订单创建时间
            seckillOrder.setCreateTime(new Date());
            //订单支付状态    0未支付,1已支付
            seckillOrder.setStatus("0");


            /**
             * 将订单对象存储起来
             * 分析:
             * 1.一个用户只允许有一个未支付秒杀订单
             * 2.订单存入Redis
             *      类型存储:Hash
             *      命名空间:namespace->SeckillOrder
             *      每个用户的订单:
             *          key:username
             *          value:SeckillOrder
             */
            redisTemplate.boundHashOps("SeckillOrder").put(username, seckillOrder);

            /**
             * 库存递减
             * 分析:
             * 1.商品信息(SeckillGoods)中有个字段->剩余库存数量:stockCount
             * 2.商品有可能是最后一个,则在Redis中删除此商品的商品信息,并同步到MySQL
             *      即:秒杀只能抢购1件,如果库存为1,被一位用户抢购并下订单之后,
             *      就在Redis和Mysql中删除此商品的信息,表示已经没有库存了
             */

            //秒杀只能抢一件商品,所以商品库存数量减1
            seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);

            //如果此时商品被抢购完了,库存<1,也就是没有库存了
            if (seckillGoods.getStockCount() < 1) {
                //同步数据到MySQL
                seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);

                //删除Redis中的商品数据
                redisTemplate.boundHashOps(namespace).delete(id);

            } else {
                //同步数据到Redis
                redisTemplate.boundHashOps(namespace).put(id, seckillGoods);
            }

            System.out.println("下单完成!");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

根据时间查询商品列表

根据时间和商品id下单



SeckillOrderQueue是排队队列,多线程执行下单时先将队列存入redis,然后排队(多线程)执行,订单将被SeckillOrde存入redis中,SeckillOrderQueue队列执行完毕后销毁。

由此可见,排队已经下单成功。

下单状态查询:

秒杀订单控制层中的添加订单方法

@RestController
@RequestMapping("/seckillOrder")
@CrossOrigin
public class SeckillOrderController {

    @Autowired
    private SeckillOrderService seckillOrderService;

    /**
     * 根据时间区间和商品id添加订单
     * @param time:时间区间
     * @param id:商品id
     * @return
     */
    @GetMapping(value = "/add")
    public Result add(String time,Long id){
        //模拟用户登录信息
        String username = "acd";
        seckillOrderService.add(time,id,username);
        return new Result(true, StatusCode.OK,"正在排队……");
    }

}

修改秒杀订单业务层实现类中的添加订单方法,加了一个用户抢单状态

/**
     * 添加订单:排队
     * @param time:时间区间
     * @param id:商品id
     * @param username:用户登录名
     */
    @Override
    public Boolean add(String time, Long id, String username) {
        //抢单状态信息: 1排队中
        SeckillStatus seckillStatus = new SeckillStatus(username, new Date(), 1, id, time);
        //排队  list有序队列        从左边存入  右边取出
        redisTemplate.boundListOps("SeckillOrderQueue").leftPush(seckillStatus);

        /**
         * 用户抢单状态->用于查询
         * 一个用户只能有一个排队信息,也就是只能有一个未支付订单
         */
        redisTemplate.boundHashOps("UserQueueStatus").put(username,seckillStatus);

        /**
         * 异步执行  也就是多线程方式执行
         */
        multiThreadingCreateOrder.creatOrder();

        return true;
    }

多线程下单中更新下单状态

@Autowired
    private SeckillGoodsMapper seckillGoodsMapper;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IdWorker idWorker;

    /**
     * 多线程下单
     *
     * @Async 异步执行(底层:多线程)
     */
    @Async
    public void creatOrder() {
        try {
            System.out.println("准备睡会儿再下单!");
            //休眠 10s
            Thread.sleep(10000);

            //获取用户排队状态
            // list有序队列   从左边存入  右边取出
            SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundListOps("SeckillOrderQueue").rightPop();

            //如果没有获取到排队队列
            if (seckillStatus == null) {
                return;
            }

            //获取商品的id和时区以及用户登录名
            String time = seckillStatus.getTime();
            Long id = seckillStatus.getGoodsId();
            String username = seckillStatus.getUsername();


            //查询秒杀商品    命名空间
            String namespace = "SeckillGoods_" + time;
            //根据命名空间和商品id,查询商品
            SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace).get(id);

            //判断有没有库存 (没查到 或 库存数量少于1)
            if (seckillGoods == null || seckillGoods.getStockCount() < 1) {
                //如果没有库存
                throw new RuntimeException("已售罄!");
            }


            //创建订单对象    打包订单信息
            SeckillOrder seckillOrder = new SeckillOrder();
            //订单id
            seckillOrder.setId(idWorker.nextId());
            //商品id
            seckillOrder.setSeckillId(id);
            //支付金额  秒杀价格:seckillGoods.getCostPrice()
            seckillOrder.setMoney(seckillGoods.getCostPrice());
            //用户登录名
            seckillOrder.setUserId(username);
            //订单创建时间
            seckillOrder.setCreateTime(new Date());
            //订单支付状态    0未支付,1已支付
            seckillOrder.setStatus("0");


            /**
             * 将订单对象存储起来
             * 分析:
             * 1.一个用户只允许有一个未支付秒杀订单
             * 2.订单存入Redis
             *      类型存储:Hash
             *      命名空间:namespace->SeckillOrder
             *      每个用户的订单:
             *          key:username
             *          value:SeckillOrder
             */
            redisTemplate.boundHashOps("SeckillOrder").put(username, seckillOrder);

            /**
             * 库存递减
             * 分析:
             * 1.商品信息(SeckillGoods)中有个字段->剩余库存数量:stockCount
             * 2.商品有可能是最后一个,则在Redis中删除此商品的商品信息,并同步到MySQL
             *      即:秒杀只能抢购1件,如果库存为1,被一位用户抢购并下订单之后,
             *      就在Redis和Mysql中删除此商品的信息,表示已经没有库存了
             */

            //秒杀只能抢一件商品,所以商品库存数量减1
            seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);

            //如果此时商品被抢购完了,库存<1,也就是没有库存了
            if (seckillGoods.getStockCount() < 1) {
                //同步数据到MySQL
                seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);

                //删除Redis中的商品数据
                redisTemplate.boundHashOps(namespace).delete(id);

            } else {
                //同步数据到Redis
                redisTemplate.boundHashOps(namespace).put(id, seckillGoods);
            }

            //更新下单状态    订单状态中
            //订单id
            seckillStatus.setOrderId(seckillOrder.getId());
            //支付金额      金额转为Float类型
            seckillStatus.setMoney(Float.valueOf(seckillGoods.getCostPrice()));
            //订单支付状态    待支付
            // 1:排队中,2:秒杀等待支付(已下单),3:支付超时,4:秒杀失败,5:支付完成
            seckillStatus.setStatus(2);
           //订单状态状态更新至redis
 redisTemplate.boundHashOps("UserQueueStatus").put(username,seckillStatus);

            System.out.println("下单完成!");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

秒杀订单业务层接口中,写订单状态查询方法

/**
     * 订单状态查询
     * @param username:用户登录名
     * @return
     */
    SeckillStatus queryStatus(String username);

秒杀订单业务层实现类中,写订单状态查询方法

/**
     * 订单状态查询
     * @param username:用户登录名
     * @return
     */
@Override
public SeckillStatus queryStatus(String username) {
    //根据用户登录名,从redis中获取订单状态信息
    return (SeckillStatus) redisTemplate.boundHashOps("UserQueueStatus").get(username);
}

秒杀订单控制层中,写订单状态查询方法的调用

/**
     * 订单状态查询
     * @return
     */
@GetMapping(value = "/query")
public Result queryStatus(){
    //模拟用户登录名
    String username = "acd";

    //根据用户名查询订单状态
    SeckillStatus seckillStatus = seckillOrderService.queryStatus(username);

    //如果查询到了订单状态信息
    if (seckillStatus!=null){
        //返回查询到的订单状态
        return new Result(true,StatusCode.OK,"查询订单状态成功",seckillStatus);
    }

    //如果没查到订单状态信息
    return new Result(false,StatusCode.NOTFOUNDERROR,"抢单失败!");
}

开始下单

查看订单状态


SeckillOrder是订单详细信息,UserQueueStatus是订单状态,由此可见,排队抢单操作成功,查询订单状态也成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值