秒杀功能(6)RabbitMQ

这篇讲解的内容较多。先解决业务问题再解决性能问题。

业务问题

之前的代码中有两类业务问题:

  1. 超卖;
  2. 若一个用户用两个平台同时秒杀某一商品,可能会出现该用户秒杀两件商品的情况。

这两个问题虽然业务问题很大,但代码改动特别小。

  • 超卖
    这里利用数据库本身的锁来解决,只需要改动dao层代码,修改GoodsDao的代码。
//改动前的代码
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId}")
public int reduceStock(MiaoshaGoods g);

//改动后的代码
@Update("update miaosha_goods set stock_count = stock_count - 1 where goods_id = #{goodsId} and stock_count > 0")
public int reduceStock(MiaoshaGoods g); //利用数据库的锁来保证不会卖超(数据库本身会保证不会有两个线程同时改一条记录)
  • 一个用户秒杀多件商品的情况
    在数据库层面:把用户id和商品id组合设置成唯一索引,来防止一个用户多秒杀商品的情况;
    在缓存中:把用户id和商品id组合成一个键,下单前查询该键是否存在,若不存在则下单,并在缓存里存入键值对(值是秒杀订单),这样比直接查询数据库要快些。
@Service
public class OrderService {

    @Autowired
    OrderDao orderDao;

    @Autowired
    RedisService redisService;

    public MiaoshaOrder getMiaoshaOrderByUserIdGoodsId(long userId, long goodsId) {
       // return orderDao.getMiaoshaOrderByUserIdGoodsId(userId, goodsId);
        //在缓存中查
        return redisService.get(OrderKey.getMiaoshaOrderByUidGid, "" + userId + "_" + goodsId, MiaoshaOrder.class);
    }

    @Transactional
    public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) {
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCreateDate(new Date());
        orderInfo.setDeliveryAddrId(0L);
        orderInfo.setGoodsCount(1);
        orderInfo.setGoodsId(goods.getId());
        orderInfo.setGoodsName(goods.getGoodsName());
        orderInfo.setGoodsPrice(goods.getMiaoshaPrice());
        orderInfo.setOrderChannel(1);
        orderInfo.setStatus(0);
        orderInfo.setUserId(user.getId());
        orderDao.insert(orderInfo);
        MiaoshaOrder miaoshaOrder = new MiaoshaOrder();
        miaoshaOrder.setGoodsId(goods.getId());
        miaoshaOrder.setOrderId(orderInfo.getId());
        miaoshaOrder.setUserId(user.getId());
        orderDao.insertMiaoshaOrder(miaoshaOrder);
        //放入缓存中
        redisService.set(OrderKey.getMiaoshaOrderByUidGid, "" + user.getId() + "_" + goods.getId(), miaoshaOrder);
        return orderInfo;
    }
}

可以看到,代码中就改动了两点,一是查询重复订单时是在缓存中查,第二点是,在数据库中创建秒杀订单和订单详情后,会在缓存中插入用户id和商品id结合的键(值是秒杀订单),为之后在缓存中查找做准备。

性能问题

秒杀系统的性能瓶颈一般是在对数据库的访问上,若能减少对数据的访问,就能在很大程度上提高并发量。
大致的思路是:

  1. Redis预减库存减少数据库的访问;
  2. 内存标记减少Redis访问;
  3. 请求先入队缓冲,异步下单,增强用户体验。

具体的代码流程是:

  1. 系统初始化,把商品库存数量加载到Redis中;
  2. 收到请求,先在内存标记上判断商品是否秒杀完毕,若没有,则进入下一步;
  3. Redis预减库存,库存不足,直接返回,否则进入下一步;
  4. Redis中判断是否重复下单,若没有进入下一步;
  5. 请求入队,前端页面立即返回排队中;
  6. 请求出队,数据库中判断库存、缓存中判断是否重复秒杀,若都成功进入下一步;
  7. 数据库中减少库存,数据库中生成订单,在缓存中生成用户和商品的键以便之后验证是否重复下单;
  8. 客户端轮询(前端中实现),是否秒杀成功。

Controller层
在这部分主要完成:

  1. 系统的初始化,把秒杀商品的库存数量加载到Redis中;
  2. 内存中判断商品是否秒杀完、Redis预减库存、判断是否重复秒杀、入队操作。
@Controller
@RequestMapping("/miaosha")
public class MiaoshaController implements InitializingBean {

    @Autowired
    MiaoshaUserService userService;

    @Autowired
    RedisService redisService;

    @Autowired
    GoodsService goodsService;

    @Autowired
    OrderService orderService;

    @Autowired
    MiaoshaService miaoshaService;

    @Autowired
    MQSender sender;

    private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>();

    /**
     * 系统初始化,初始化的时候把数据库中的库存加载进缓存中
     */
    public void afterPropertiesSet() throws Exception {
        List<GoodsVo> goodsList = goodsService.listGoodsVo();
        if (goodsList == null) {
            return;
        }
        for (GoodsVo goods : goodsList) {
            redisService.set(GoodsKey.getMiaoshaGoodsStock, "" + goods.getId(), goods.getStockCount());
            localOverMap.put(goods.getId(), false);
        }
    }

    //第三版,消息队列的方式
    @RequestMapping(value = "/do_miaosha", method = RequestMethod.POST)
    @ResponseBody
    public Result<Integer> miaosha(Model model, @RequestParam("goodsId") long goodsId, HttpServletResponse response,
                                     @CookieValue(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String cookieToken,
                                     @RequestParam(value = MiaoshaUserService.COOKI_NAME_TOKEN,required = false) String paramToken) {
        if (StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
            return Result.error(CodeMsg.SESSION_ERROR);//token不存在或失效
        }
        String token = StringUtils.isEmpty(paramToken) ? cookieToken : paramToken;
        MiaoshaUser user = userService.getByToken(response, token);//从token中读用户信息

        //内存标记,减少redis访问
        boolean over = localOverMap.get(goodsId);
        if (over) {//库存减去10后不必要访问redis,访问map即可
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //预减库存
        long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);
        if (stock < 0) {
            localOverMap.put(goodsId, true);
            return Result.error(CodeMsg.MIAO_SHA_OVER);
        }
        //判断是否已经秒杀到了
        MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order != null) {
            return Result.error(CodeMsg.REPEATE_MIAOSHA);
        }
        //入队
        MiaoshaMessage mm = new MiaoshaMessage();
        mm.setUser(user);
        mm.setGoodsId(goodsId);
        sender.sendMiaoshaMessage(mm);
        return Result.success(0);//排队中
    }
}

其中,MiaoshaMessage的定义为:

public class MiaoshaMessage {
    private MiaoshaUser user;
    private long goodsId;
    //Getter and Setter...
}

RabbitMQ相关操作

  • MQSender:
@Service
public class MQSender {

    private static Logger log = LoggerFactory.getLogger(MQSender.class);

    @Autowired
    AmqpTemplate amqpTemplate; //操作queue的工具类

    public void sendMiaoshaMessage(MiaoshaMessage mm) {
        String msg = RedisService.beanToString(mm);
        log.info("send message:" + msg);
        amqpTemplate.convertAndSend(MQConfig.MIAOSHA_QUEUE, msg);
    }
}
  • MQReceiver
@Service
public class MQReceiver {

    private static Logger log = LoggerFactory.getLogger(MQReceiver.class);

    @Autowired
    RedisService redisService;

    @Autowired
    GoodsService goodsService;

    @Autowired
    OrderService orderService;

    @Autowired
    MiaoshaService miaoshaService;

    @RabbitListener(queues = MQConfig.MIAOSHA_QUEUE) //监听的指定的队列
    public void receive(String message) {
        log.info("receive message:" + message);
        MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class);
        MiaoshaUser user = mm.getUser();
        long goodsId = mm.getGoodsId();

        //判断库存
        GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId);
        int stock = goods.getStockCount();
        if (stock <= 0) {
            return;
        }
        //判断是否已经秒杀到了
        MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId);
        if (order != null) {
            return;
        }
        //减库存 下订单 写入秒杀订单
        miaoshaService.miaosha(user, goods);
    }
}

其中,miaoshaService.miaosha(user, goods)操作为:

@Service
public class MiaoshaService {

    @Autowired
    GoodsService goodsService;

    @Autowired
    OrderService orderService;

    @Autowired
    RedisService redisService;

    @Transactional
    public OrderInfo miaosha(MiaoshaUser user, GoodsVo goods) {
        //减库存 下订单 写入秒杀订单
        boolean success = goodsService.reduceStock(goods);
        if (success){
            //order_info maiosha_order
            return orderService.createOrder(user, goods);
        }else {
            setGoodsOver(goods.getId());
            return null;
        }
    }
}

目前,优化完成了。当用户请求到达Controller层,在到达数据库之前,内存map和redis会抵挡大部分的数据库访问操作,大概只有与秒杀商品个数差不多的请求会进入队列从而访问数据库。

压测

在之前写的秒杀功能(3)的压测中,初步版本的TPS = 808/sec;

本次的压测结果为:
在这里插入图片描述
TPS = 908/sec,略高一些,没有提高太多,这可能是和硬件条件有关,我的服务器性能不高。

数据库情况

  • 秒杀商品情况
    在这里插入图片描述
  • 秒杀订单情况
    在这里插入图片描述
    正好9个订单,没有出现卖超的现象了。

这篇写完我是奔溃的,,明明在几个小时前我就能写完。。中间开了个组会我掉网了。。我还没保存。。于是写了两遍。。又拖累了今天的进度,offer君又远离我一步。。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值