秒杀业务

1. 秒杀业务分析

1.1 需求分析

  • 所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。
  • 秒杀商品通常有两种限制:库存限制、时间限制。
  • 需求:
    (1)商家提交秒杀商品申请,录入秒杀商品数据,主要包括:商品标题、原价、秒杀价、商品图片、介绍等信息
    (2)运营商审核秒杀申请
    (3)秒杀频道首页列出秒杀商品(进行中的)点击秒杀商品图片跳转到秒杀商品详细页。
    (4)商品详细页显示秒杀商品信息,点击立即抢购实现秒杀下单,下单时扣减库存。当库存为 0 或不在活动期范围内时无法秒杀。
    (5)秒杀下单成功,直接跳转到支付页面(微信扫码),支付成功,跳转到成功页,填写收货地址、电话、收件人等信息,完成订单。
    (6)当用户秒杀下单 5 分钟内未支付,取消预订单,调用微信支付的关闭订单接口,恢复库存。

1.2 实现方案

1.2.1 高并发下的优化

  • 如果在高并发下防止超卖,确保线程安全
    防止超卖

  • 实现过程如下图

这里写图片描述

1.2.2 排队优化

  • 上面的方案解决了并发情况下超卖的问题,但其实际秒杀中大量并发情况下,这个下单过程是需要很长等待时间的,所以这里我们建议用异步和多线程实现,最好不要让程序处于阻塞状态,而是在用户一下单的时候确认用户是否符合下单条件,如果符合,则开启线程执行创建订单处理,用户则进入支付页面等待查询订单结果, 扫码支付即可。

  • 实现过程如下图

排队优化

2. 定时任务

  1. 定时生成秒杀商品详情页
  2. 将秒杀商品完整信息入库到 Redis (Hash类型)
  3. 每个商品的待售数量队列入库到 Redis (List类型)

3. 结算页面

  1. 展示用户送货地址清单
  2. 用户选择送货地址
  3. 选择扫码支付方式 (微信 / 支付宝)
  4. 展示待结算的秒杀商品明细

3. 秒杀下单

3.1 前端

3.1.1 页面

3.1.2 控制层 orderController.js

    /**
     * 进入秒杀下单
     */
    $scope.submitOrder = function () {
        // 从地址栏获去秒杀商品Id 
        var seckillGoodsId = $location.search()['seckillGoodsId'];

        orderService.submitOrder(seckillGoodsId).success(function (response) {
                if (response.success) {
                    //进入支付页面
                    location.href = "pay.html";
                } else {
                    alert(response.message);
                }
            }
        );
    };

3.1.3 服务层 orderService.js

    /**
     * 进入秒杀下单
     */
    this.submitOrder = function (id) {
        return $http.get("http://localhost:9007/seckillOrder/submitOrder02/"+ id);
    }

3.2 后端

3.2.1 控制层


import com.alibaba.dubbo.config.annotation.Reference;
import com.pyg.seckill.service.SeckillOrderService;
import com.pyg.utils.PygResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

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

    @Reference(timeout = 1000000000)
    private SeckillOrderService seckillOrderService;

    /**
     * 需求:提交订单
     * 请求:../seckillOrder/submitOrder/'+id
     * 参数:@PathVariable Long id : 秒杀商品id
     */
    @RequestMapping("submitOrder/{id}")
    @CrossOrigin(origins = "http://item.pinyougou.com")
    public ResultInfo submitOrder02(@PathVariable Long id, HttpServletRequest request){
        ResultInfo resultInfo = null;
        String userId = request.getRemoteUser();
        try {
            seckillOrderService.submitOrder(id,userId);
            resultInfo = new ResultInfo<Object>(true,null,"秒杀成功");
        } catch (Exception e) {
            e.printStackTrace();
            resultInfo = new ResultInfo<Object>(false,null,e.getMessage());
        }
        return resultInfo;
    }

}

3.2.2 服务层接口

/**
 * 服务层接口
 */
public interface SeckillOrderService {

    /**
     * 需求:提交订单
     * 参数:Long id , String userId
     */
    void submitOrder(Long id, String userId);
}

3.2.3 服务层实现类

  • 配置文件目录
    这里写图片描述

  • 代码


import com.alibaba.dubbo.config.annotation.Service;
import com.pyg.mapper.TbSeckillOrderMapper;
import com.pyg.pojo.TbSeckillGoods;
import com.pyg.pojo.TbSeckillOrder;
import com.pyg.seckill.service.SeckillOrderService;
import com.pyg.utils.SysConstants;
import com.pyg.vo.OrderRecode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * 服务实现层
 */
@Service
public class SeckillOrderServiceImpl implements SeckillOrderService {

    //注入redis模板对象
    @Autowired
    private RedisTemplate redisTemplate;

    //注入多线程对象
    @Autowired
    private CreateOrder createOrder;

    //注入调度对象
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;


    /**
     * 需求:提交订单
     * 参数:Long id String userId
     * 1),从redis服务器中获取入库的秒杀商品
     * 2),判断商品是否存在,商品库存是否小于等于0 
     * 3), 是否用户正在排队, 是否有未支付的订单
     * 4), 是否超出秒杀人数限制 
     * 5),满足条件,添加入秒杀用户队列 
     * 6),把待处理用户详情(userId, seckillgoodsId)存储在redis服务器中
     * 7),调用多线程创建秒杀订单, 此订单此时处于未支付状态
     */
    public void submitOrder(Long id, String userId) {

        //1),从redis服务器中获取入库的秒杀商品
        TbSeckillGoods seckillGoods =
                (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);
        // 2),判断商品是否存在,商品库存是否小于等于0
        if (seckillGoods == null || seckillGoods.getStockCount() < 1) {
            throw new RuntimeException("已售罄");
        }

        // 3), 是否用户正在排队, 是否有未支付的订单,  
        //从用户队列获取用户对象
        Boolean member = redisTemplate.boundSetOps(SysConstants.SECKILL_USER_SET + id).isMember(userId);
        //如果为true,表示此用户正在排队
        if (member) {
            //查询用户是否有订单
            Object order = redisTemplate.boundHashOps(TbSeckillOrder.class.getSimpleName()).get(userId);
            //判断订单是否为空
            if (order != null) {
                throw new RuntimeException("您还有订单未支付!");
            }

            throw new RuntimeException("您正在排队中.......");

        }

        // 4), 是否超出秒杀人数限制 
        //获取抢购此商品的人数
        int persons = redisTemplate.boundSetOps(SysConstants.SECKILL_USER_SET + id).members().size();
        //判断商品库存是否满足抢购人数需求
        if(persons >= seckillGoods.getStockCount()+200){
            throw new RuntimeException("排队人数过多...");
        }

        // 5),满足条件,添加入秒杀用户队列 
        // 6),把待处理用户详情(userId, seckillgoodsId)存入list集合进行排队
       redisTemplate.boundListOps(SysConstants.SECKILL_USER_QUEUE).leftPush(new OrderRecode(userId,id));
        //使用set集合记录用户排队
        redisTemplate.boundSetOps(SysConstants.SECKILL_USER_SET + id).add(userId);

        // 7),调用多线程创建秒杀订单, 此订单此时处于未支付状态
        taskExecutor.execute(createOrder);

    }

}

3.2.4 创建订单类 CreateOrder.java

import com.pyg.mapper.TbSeckillGoodsMapper;
import com.pyg.pojo.TbSeckillGoods;
import com.pyg.pojo.TbSeckillOrder;
import com.pyg.utils.IdWorker;
import com.pyg.utils.SysConstants;
import com.pyg.vo.OrderRecode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class CreateOrder implements Runnable {

    //注入redis模板对象
    @Autowired
    private RedisTemplate redisTemplate;

    //注入秒杀商品mapper接口代理对象
    @Autowired
    private TbSeckillGoodsMapper seckillGoodsMapper;

    /**
     * 多线程实现下单
     */
    public void run() {

        //1,从Redis队列中获取用户排队信息
        OrderRecode orderRecode =
                (OrderRecode) redisTemplate.boundListOps(SysConstants.SECKILL_USER_QUEUE).rightPop();

        //2,判断用户排队信息是否存在
        if (orderRecode != null) {
            // 3,如果用户存在,判断秒杀商品是否存在
            Long seckillGoodsId = (Long) redisTemplate.boundListOps(SysConstants.SECKILL_GOODSID_LIST + orderRecode.getSeckillId()).rightPop();
            // 4,如果秒杀商品不存在,表示秒杀商品已售罄
            if (seckillGoodsId == null) {
                throw new RuntimeException("已售罄");
            }
        }

        // 秒杀商品Id
        Long id = orderRecode.getSeckillId();
        // 用户 userId
        String userId = orderRecode.getUserId();

        // 5,秒杀下单
        // 5.1 从redis服务器中获取入库的秒杀产品
        TbSeckillGoods seckillGoods = (TbSeckillGoods) redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).get(id);

        // 5.2 如果秒杀产品存在, 创建秒杀订单, 此订单此时处于未支付状态
        // 5.2.1 创建秒杀订单对象
        TbSeckillOrder seckillOrder = new TbSeckillOrder();
        // 5.2.2 创建idWorder
        IdWorker idWorker = new IdWorker();
        seckillOrder.setId(idWorker.nextId());
        // 5.2.3 设置秒杀订单属性值
        seckillOrder.setSellerId(seckillGoods.getSellerId());
        seckillOrder.setSeckillId(id);
        // 5.2.4 设置状态为未支付
        seckillOrder.setStatus("0");
        // 5.2.5 设置用户ID, 订单price, 下单时间
        seckillOrder.setUserId(userId);
        seckillOrder.setMoney(seckillGoods.getCostPrice());
        seckillOrder.setCreateTime(new Date());

        // 5.3 把新增订单储存在 Redis 服务器中
        // 参数1 : 订单唯一标识, 标识此数据时秒杀订单
        // 参数2 : userId , 用来标识此订单属于哪个用户
        // 参数3 : 秒杀订单数据
        redisTemplate.boundHashOps(TbSeckillOrder.class.getSimpleName()).put(userId,seckillOrder);

        // 5.4 下单后, 把秒杀商品库存减一
        seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);

        // 5.5 判断库存是否小于0 , 卖完需要同步数据库
        if(seckillGoods.getStockCount() < 1){
            seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);
            redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).delete(seckillGoods.getId());
        }else {
            // 5.6 否则把库存减少(但此时没有减为0) 的秒杀商品同步 Redis
            redisTemplate.boundHashOps(TbSeckillGoods.class.getSimpleName()).put(seckillGoods.getId(),seckillGoods);
        }

        // 5.7 已经创建订单的从Set 集合中移除
        redisTemplate.boundSetOps(SysConstants.SECKILL_USER_SET + id).remove(userId);
    }
}

工具类

SysConstants.java

/**
 * 标识
 */
public class SysConstants {

    //秒杀商品队列唯一标识常量
    public static final String SECKILL_GOODSID_LIST = "SECKILL_GOODSID_LIST";
    //用户排队Set集合唯一标识
    public static final String SECKILL_USER_SET = "SECKILL_USER_SET";
    //标识用户排队队列
    public static final String SECKILL_USER_QUEUE = "SECKILL_USER_QUEUE";

}

猜你喜欢

转载自blog.csdn.net/qq_42806915/article/details/82666254