(秒杀项目) 4.4 用户下单与秒杀

一、慢查询分析

慢查询分析是通过开启慢查询日志开关、然后执行某个sql超过了配置的查询最大时间会将这条sql记录到慢查询日志中,以便我们开发人员进行分析检查查询慢的原因,方便进行sql优化。(面试时我们可以编一个故事、我们在做项目中是如何进行sql优化的、比如我们可以假设在商品展示中发现商品列表加载太慢、然后通过使用慢查询分析发现是查询商品活动信息sql语句超时、经过分析是查询没有使用索引、优化后使用索引提高了页面的加载速度。)

打开慢查询日志开关

有两种方式一种是设置参数、一种是修改配置文件。这里使用设置参数、详细看下图分析
在这里插入图片描述

测试慢查询日志

执行一条select sleep(3);表示睡三秒、刚刚设置了慢查询日志的最大查询时间为1秒所以这条语句会超时并被慢查询日志记录到日志中。
在这里插入图片描述

二、用户下单和秒杀

超卖和少卖问题(面试重点)

下单操作会涉及到库存扣减、扣减库存一般发生在下单和付款时、在高并发情况下涉及到而两个问题超卖少卖。这在面试中也是常问的问题

  1. 下单时扣减库存:比如有100件商品、秒杀时有100人下了单,这时候库存已经没有了商品、理论卖出去了100件,但实际付款的只有80人,另外20人没有付款,实际只卖出去了80件,还有20件商品没有卖出去,但库存表中的库存数已经为0了。这就出现了少卖问题。
    怎么解决少卖问题呢、就是通过超时未付款自动取消订单、然后回退库存数。
  2. 付款时扣减库存:还是假如有100件商品、由于是付款时扣减库存、所以就算有100人下单商品没有付款的话还是可以继续下单的,假如有150个人下了单、但只能有前100个付款的人才能购买成功后50个人无法购买,这样给人的体验是很差的,明明已经抢到了。这就是超卖问题

本项目中使用的是下单时扣减库存的方法。

下单操作前端

在商品列表栏单击商品、进入详情页面
在这里插入图片描述在详情页点击购买即可完成下单
在这里插入图片描述由前端页面可知、请求的后端接口是http://127.0.0.1:8080/order/create

后端实现

数据库表解读

  1. order_info表:用于保存生成订单的表。表记录的信息有用户id、商品id、活动id创建时间等。
    需要注意的是这里订单表冗余记录了商品的单价、目的是这是活动的单价、商品的单价是有可能改变的。
    还有就是订单信息的id,比如‘20210807000000000000’前八位20210807表示的是订单日期的年月日、后面表示的就是订单的流水号。这么做的原因是当订单表未来是会一张很大很大的表、一般情况下会进行数据的拆分、比如我们用京东淘宝会有最近订单和历史订单、历史订单是显然会放在另一张表中、因为如果全部保存在这张下单用的订单表中很显然内存很容易不够用的,我们可以根据日期将订单进行拆分、这时候id中的前八位就起到作用了进行拆分时就可以走索引提高效率了,假设不是这样的话就只能使用后面的日期进行拆分那将会很慢。
    在这里插入图片描述2. serial_number表:保存订单表中的流水号自增id的索引增到哪了。
    name:表示的是业务名。
    value:表示的是索引值增加到哪了。
    step:是步长、每次索引增加的大小。
    在这里插入图片描述
    对应的控制处理器层
    根据代码可知、前端会传过来商品id、商品数量、商品活动的id。先通过session获取user对象、然后调用service层的createOrder创建订单方法,将userid、商品id、商品活动id传过去。
    在这里插入图片描述
    对应的service层
    service代码比较多、注释已经很明白,先是验证商品数量、活动id、用户、商品等是否合理,防止黑客假数据攻击。校验都通过后、就会直接扣减库存、生成订单,最后更新销量数据。
	@Override
    @Transactional
    public Order createOrder(int userId, int itemId, int amount, Integer promotionId) {
        // 校验参数
        if (amount < 1 || (promotionId != null && promotionId.intValue() <= 0)) {
            throw new BusinessException(PARAMETER_ERROR, "指定的参数不合法!");
        }

        // 校验用户
        User user = userService.findUserById(userId);
        if (user == null) {
            throw new BusinessException(PARAMETER_ERROR, "指定的用户不存在!");
        }

        // 校验商品
        Item item = itemService.findItemById(itemId);
        if (item == null) {
            throw new BusinessException(PARAMETER_ERROR, "指定的商品不存在!");
        }

        // 校验库存
        int stock = item.getItemStock().getStock();
        if (amount > stock) {
            throw new BusinessException(STOCK_NOT_ENOUGH, "库存不足!");
        }

        // 校验活动
        if (promotionId != null) {
            if (item.getPromotion() == null) {
                throw new BusinessException(PARAMETER_ERROR, "指定的商品无活动!");
            } else if (!item.getPromotion().getId().equals(promotionId)) {
                throw new BusinessException(PARAMETER_ERROR, "指定的活动不存在!");
            } else if (item.getPromotion().getStatus() == 1) {
                throw new BusinessException(PARAMETER_ERROR, "指定的活动未开始!");
            }
        }

        // 扣减库存
        boolean successful = itemService.decreaseStock(itemId, amount);
        if (!successful) {
            throw new BusinessException(STOCK_NOT_ENOUGH, "库存不足!");
        }

        // 生成订单
        Order order = new Order();
        order.setId(this.generateOrderID());
        order.setUserId(userId);
        order.setItemId(itemId);
        order.setPromotionId(promotionId);
        order.setOrderPrice(promotionId != null ? item.getPromotion().getPromotionPrice() : item.getPrice());
        order.setOrderAmount(amount);
        order.setOrderTotal(order.getOrderPrice().multiply(new BigDecimal(amount)));
        order.setOrderTime(new Timestamp(System.currentTimeMillis()));
        orderMapper.insert(order);

        // 更新销量
        itemService.increaseSales(itemId, amount);

        return order;
    }

需要注意的是订单id是怎么生成的,id是通过service层的generateOrderID()生成的,看代码

    /**
     * 格式:日期 + 流水
     * 示例:20210123000000000001
     *
     * @return
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private String generateOrderID() {
        StringBuilder sb = new StringBuilder();

        // 拼入日期
        sb.append(Toolbox.format(new Date(), "yyyyMMdd"));

        // 获取流水号
        SerialNumber serial = serialNumberMapper.selectByPrimaryKey("order_serial");
        Integer value = serial.getValue();

        // 更新流水号
        serial.setValue(value + serial.getStep());
        serialNumberMapper.updateByPrimaryKey(serial);

        // 拼入流水号
        String prefix = "000000000000".substring(value.toString().length());
        sb.append(prefix).append(value);

        return sb.toString();
    }

根据代码可知通过一个stringBuilder拼接字符串

  1. 先是拼接格式化当前日期的年月日格式是yyyyMM、
  2. 然后获取serial_number表中的索引序号也就是流水号、
  3. 获取后更新流水号,步长是serial_number中的step字段的值
  4. 使用substring除去value长度、拼接value到prefix后面

对应的daomapper()层
其他都没什么好说的、都是一些常规的增删改查。
需要注意的是在查询流水号时增加了一个for update排它锁、目的是防止在并发情况下会发生读到同一个数据的情况、进而导致流水订单id一样的情况
在这里插入图片描述

总结

面试时这一块主要会问到、mysql索引、事务相关的知识,要重点去看下这两块的八股文。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值