方圆的秒杀系统优化方案实战,(四)秒杀下单功能

本文详细介绍了秒杀系统的下单功能,包括核心下单逻辑、取消订单、查询秒杀订单等操作。重点讲解了使用@Transactional确保库存扣减与下单同步,并采用雪花算法生成唯一订单ID,保证在分布式系统中的高效性和无冲突性。后续文章将聚焦于秒杀系统的性能优化。
摘要由CSDN通过智能技术生成

1. 说在前头

大家好,我是方圆。到了这篇就是要写到秒杀的下单动作了,之后大部分的性能优化都将围绕着它来展开。废话不说了,列下订单模块的功能

  1. 下单核心功能
  2. 取消订单
  3. 根据ID获取秒杀订单明细
  4. 多条件查询秒杀订单

在这里还是标记一下GitHub代码库,以base-function分支为准,2,3功能就是CRUD,请将主要精力放在秒杀下单功能上,我在后文中也会解释一下其中的细节,方便大家参考

另外,在此也留下秒杀订单表的DDL语句,所有相关的SQL语句在项目environment的mysql文件夹下

CREATE TABLE IF NOT EXISTS flash_sale.`flash_order` (
    `id`            bigint(20)  NOT NULL AUTO_INCREMENT COMMENT '秒杀订单ID',
    `item_id`       bigint(20)  NOT NULL COMMENT '秒杀品ID',
    `activity_id`   bigint(20)  NOT NULL COMMENT '秒杀活动ID',
    `user_id`       bigint(20)  NOT NULL COMMENT '用户ID',
    `item_title`    varchar(50) NOT NULL COMMENT '秒杀品名称',
    `flash_price`   bigint(20)  NOT NULL COMMENT '秒杀价',
    `quantity`      int(11)     NOT NULL COMMENT '数量',
    `total_amount`  bigint(20)  NOT NULL COMMENT '总价格',
    `status`        tinyint(2)  NOT NULL COMMENT '订单状态 10-已创建 20-已取消',
    `modified_time` datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
    `create_time`   datetime    NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`id`),
    KEY `flash_order_user_id_idx` (`user_id`)
    ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '秒杀订单表';

2. FlashOrderController

  • 仍然是将Controller中的调用入口放在这里,供大家参考,具体源码仍然是以项目代码为准,我们在第3节中解释一下秒杀功能实现的细节
@RestController
public class FlashOrderController {

    @Resource
    private FlashOrderAppService flashOrderAppService;

    /**
     * 秒杀商品下单
     *
     * @param request 秒杀请求
     *                用户ID 这里我们并没有创建用户管理系统,所以仅以此来作为用户唯一标识
     */
    @ApiOperation(value = "秒杀商品下单")
    @PostMapping("/flash-orders/placeOrder")
    public <T> SingleResponse<T> placeOrder(@RequestBody FlashOrderPlaceRequest request) {
        FlashPlaceOrderCommand command = FlashOrderConvertor.toCommand(request);

        AppResult<T> appResult = flashOrderAppService.placeOrder(request.getUserId(), command);

        return ResponseConvertor.with(appResult);
    }

    @ApiOperation(value = "取消秒杀订单")
    @PutMapping("/flash-orders/{orderId}/cancelOrder")
    @ApiImplicitParam(name = "orderId", value = "秒杀订单ID", dataTypeClass = Long.class)
    public <T> SingleResponse<T> cancelOrder(@PathVariable Long orderId) {
        AppResult<T> appResult = flashOrderAppService.cancelOrder(orderId);

        return ResponseConvertor.with(appResult);
    }


    @PostMapping("/flash-orders")
    @ApiOperation(value = "多条件查询秒杀订单")
    public SingleResponse<List<FlashOrderResponse>> getFlashOrders(@RequestBody FlashOrderQueryRequest request) {
        FlashOrderQuery flashOrderQuery = FlashOrderConvertor.toQuery(request);

        return ResponseConvertor.with(flashOrderAppService.getFlashOrders(flashOrderQuery));
    }
}

3. 秒杀的实现细节

  1. 首先,我在APP层创建了一个PlaceOrderService,专门来执行秒杀的具体动作,如下
public interface PlaceOrderService {
    /**
     * 实际下单执行的动作
     *
     * @param userId 用户ID
     */
    <T> AppResult<T> doPlaceOrder(Long userId, FlashPlaceOrderCommand command);
}
  1. 我们看具体的实现类,具体的执行步骤我都标记在代码注释中了
 @Override
 @Transactional
 public <T> AppResult<T> doPlaceOrder(Long userId, FlashPlaceOrderCommand command) {
     log.info("doPlaceOrder|开始下单|{}, {}", userId, JSON.toJSONString(command));

     // 校验秒杀活动、秒杀时间条件
     checkPlaceOrderCondition(command.getActivityId(), command.getItemId());

     // 扣减库存
     decreaseItemStock(command.getItemId(), command.getQuantity());

     // 初始化秒杀订单信息
     FlashOrder flashOrder = initialFlashOrderInfo(userId, command);

     // 秒杀订单入库
     flashOrderDomainService.doPlaceOrder(flashOrder);
     log.info("doPlaceOrder|下单成功");

     return AppResult.success();
 }
  • 值得关注的是使用了@Transactional注解,让Spring为我们管理这个事务,其中有两个修改数据库的动作,扣减库存秒杀订单入库,两条SQL必须都要执行正确才行,哪有说扣减库存成功而下单失败的?
  1. 这个秒杀动作做了四件事,校验条件扣减库存初始化订单信息秒杀订单入库
    其中的业务逻辑很简单,大家看看源代码就能很好的理解,我最想提的一点是:在秒杀订单入库的时候,采用了雪花算法生成秒杀订单的ID
  • 我为此专门写了一个雪花算法的工具类,如下
@Slf4j
public class SnowflakeIdUtil {
    // 下面两个每个5位,加起来就是10位的工作机器id
    private static final long WORKER_ID;
    private static final long DATACENTER_ID;
    // 长度为5位
    private static final long WORKER_ID_BITS = 5L;
    private static final long DATACENTER_ID_BITS = 5L;

    // 12位的序列号
    private static long SEQUENCE;
    // 序列号id长度
    private static final long SEQUENCE_BITS = 12L;
    // 序列号最大值
    private static final long SEQUENCE_MAX = ~(-1L << SEQUENCE_BITS);

    // 工作id需要左移的位数,12位
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    // 数据id需要左移位数 12+5=17位
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    //时间戳需要左移位数 12+5+5=22位
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;

    // 初始时间戳
    private static final long ORIGINAL_TIME_STAMP = 1288834974657L;
    // 上次时间戳,初始值为负数
    private static long LAST_TIMESTAMP = -1L;

    static {
        Random random = new Random(1);
        
        // workerId can't be greater than ~(-1L << workerIdBits) or less than 0
        WORKER_ID = random.nextInt(32);
        // datacenterId can't be greater than ~(-1L << datacenterIdBits) or less than 0
        DATACENTER_ID = 1;
        SEQUENCE = 1;

        log.info("Snowflake starting. timestamp left shift {}, datacenter id bits {}, worker id bits {}, " +
                        "sequence bits {}, worker id {}",
                TIMESTAMP_LEFT_SHIFT, DATACENTER_ID_BITS, WORKER_ID_BITS, SEQUENCE_BITS, WORKER_ID);
    }

    /**
     * ID生成算法
     */
    public static synchronized long nextId() {
        long currentTimestamp = System.currentTimeMillis();

        // 获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
        if (currentTimestamp < LAST_TIMESTAMP) {
            log.error("clock is moving backwards. Rejecting requests until {}.", LAST_TIMESTAMP);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                    LAST_TIMESTAMP - currentTimestamp));
        }

        // 获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
        if (LAST_TIMESTAMP == currentTimestamp) {
            SEQUENCE = (SEQUENCE + 1) & SEQUENCE_MAX;
            if (SEQUENCE == 0) {
                currentTimestamp = tilNextMillis(LAST_TIMESTAMP);
            }
        } else {
            SEQUENCE = 0;
        }

        // 将上次时间戳值刷新
        LAST_TIMESTAMP = currentTimestamp;

        /*
         * 返回结果:
         * (timestamp - ORIGINAL_TIME_STAMP) << TIMESTAMP_LEFT_SHIFT) 表示将时间戳减去初始时间戳,再左移相应位数
         * (DATACENTER_ID << DATACENTER_ID_SHIFT) 表示将数据id左移相应位数
         * (WORKER_ID << WORKER_ID_SHIFT) 表示将工作id左移相应位数
         * | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
         * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
         */
        return ((currentTimestamp - ORIGINAL_TIME_STAMP) << TIMESTAMP_LEFT_SHIFT)
                | (DATACENTER_ID << DATACENTER_ID_SHIFT)
                | (WORKER_ID << WORKER_ID_SHIFT)
                | SEQUENCE;
    }

    // 获取时间戳,并与上次时间戳比较
    private static long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}
  1. 我在秒杀订单入库之前,使用了这个工具类生成了订单ID,如下
public void save(FlashOrder flashOrder) {
    FlashOrderDO flashOrderDO = FlashOrderConvertor.toDataObject(flashOrder);
    // 雪花算法生成ID
    flashOrderDO.setId(SnowflakeIdUtil.nextId());

    flashOrderMapper.insert(flashOrderDO);
}
  • 那为什么使用雪花算法?
  1. 它所有生成的ID按时间趋势递增,索引效率高
  2. 在分布式系统中,不会产生重复的ID(在工具类里WORKER_ID是用随机数来区分不同的服务器的)
  3. 不依赖数据库生成,高性能高可用
  4. 对于秒杀系统,它能够每秒生成数百万的自增ID,解决自增生成ID的问题

4. 基础功能的完结

下单功能中的细节,我觉得值得说的也就如上所述。

这篇博客完稿之后,基础功能部分就已经完结了,接下来的几篇将主要对秒杀系统进行优化,那才是重中之重!


收!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方圆想当图灵

嘿嘿,小赏就行,不赏俺也不争你

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值