项目梳理-商城秒杀系统

商城秒杀

业务逻辑图

商城系统业务逻辑

需求点梳理

项目简介 & 重点模块梳理

该项目是一个高并发场景下的商品秒杀活动。

除了一些边缘模块外,项目核心内容有两个:分别是下单秒杀前的削峰限流,和下单后保证用事务异步更新销量和扣减库存。

削峰限流

削峰限流主要是在用户下单前通过验证、限流等策略大都缓冲、平滑流量的目的。具体实现流程和用到的技术为:

  1. 通过增加验证码来平滑访问流量;
  2. 通过限制大闸,颁发令牌进行削峰(考虑到商品库存100,而秒杀用户量为10w的场景,只需颁发1000个令牌即可);
  3. 通过引入限流器(采用令牌桶算法),限制单机TPS,防止服务器挂掉;
  4. 通过引入线程池进行缓冲,再由线程池管理线程进行生产者发送消息。

削峰限流方案

更新销量

由于更新销量不影响用户秒杀操作,所以可以放到MQ里异步更新。否则直接操作数据库要加锁,影响性能。

扣减库存

由于秒杀是在高并发场景下进行的,在较短时间间隔有大量用户对同一商品资源进行互斥访问,数据库加锁的方式吞吐量太低,因此考虑先用Redis缓存库存,再最终将库存信息通过MQ异步写入数据库中。

事务消息开发逻辑

超卖 & 少卖

超卖:超卖是说用户下单量超过商品的库存量。在真实场景中,对用户下单并未完全达成同步,导致少扣库存了,这将导致用户体验不佳,在实际中很少采用。

少卖:少卖是说最终用户的付款量少于商品的库存。在实际中,用户可能下单后又退款了,这对收益不会有很严重的影响,但是不会有超卖的用户体验不佳的问题,在实际中大多采用少卖。

少卖的解决方案:

  1. 一个想到的解决方案是利用RocketMq的延时消费机制,为队列设置一个延时时间,用户下单的时候进入队列,在规定付款时间之后进行消费,消费时,可以通过检查订单状态(已付款和未付款自动取消订单)来确定是否进行消费。

  2. 如果用redis来解决的话,将提交的订单按创建时间写入到一个队列里,然后设定时间间隔轮询队头对时间做差,如果超过付款时间出队,检查订单付款状态,如果没付款,自动回补库存,如果付款,将消息封装到MQ进行消费。

问题点梳理

Mq第二阶段check的时候为什么要存流水,查订单不行吗?

订单在本地事物生成的时候可能会有延时,当订单生成成功时,可能还没来得及写到数据库里,这时check的时候并不能说明本地事务是失败的。

用流水check的好处在于,在生产者发送消息之前先产生了一个流水存在数据库里,这样check的时候肯定能查到这条数据,同时,本地事务包含了流水的更新,通过查状态值可以判断本地事务是否已经提交。

开发逻辑梳理

  • controller
    • ItemController
      • getItemList [前端展示商品列表]
      • getItemDetail [前端展示商品详情]
    • OrderController
      • getCaptcha [用户点击获取二维码][削峰限流-二维码平滑]
        • 验证用户login_token
        • userid为K,kaptcha为V,存到redis(String)
        • response存到服务器,用于后续颁发令牌验证
      • generateToken [颁发令牌][削峰限流-令牌桶算法][promotionService.generateToken]
        • 验证验证码
        • 生成令牌,返回给用户
      • create [创建订单][削峰限流-单机限流&缓冲队列][orderService.createOrderAsync]
        • 单机限流
        • 验证活动凭证-令牌token(通过PromotionService.generateToken生成的)
        • 通过线程池(缓冲队列)调用mq
        • 异步创建订单
    • UserController
      • login
      • logout
      • getUser [根据token获取用户信息]
  • service
    • ItemService [查询商品信息,扣减库存]
      • findItemsOnPromotion [查库存和活动]
        • select item from item_table by promotion [联合索引,最左匹配]
        • select stock from stock_table by item
        • select promotion from promotion_table by item
      • findItemById [mysql查商品信息]
        • select item from item_table by id [主键查询]
        • select stock from stock_table by item [外键查询]
        • select promotion from promotion_table by item [外键查询]
      • findItemInCache [redis查商品信息]
        • 查本地缓存guava [二级缓存]
        • 查redis缓存
        • 查mysql数据库
          • 写到guava
          • 写到redis
      • increaseSales [增加订单]
        • select amount from item_table for update [X锁]
      • decreaseStock [从数据库中删减库存]
        • select stock from item_table for update [X锁]
      • decreaseStockInCache [从redis中删减库存]
        • 直接在redis中扣减库存,得到扣减后的结果result [redis(String)]
        • result < 0: 回补库存
        • result == 0: 售窑标识
      • increaseStockInCache [从redis中增加库存]
        • 在redis中增加库存 [redis(String)]
      • createItemStockLog
      • updateItemStockLogStatus [更新流水状态]
      • findItemStorkLogById [查询流水状态]
    • OrderService
      • createOrder [][][itemService.updateItemStockLogStatus][&executeLocalTransaction.createOrder]
        • 预扣库存 (并发场景下如果直接走库需要加锁,性能低) [redis]
        • 生成订单 (生成订单流水,作为主键id,方便分表) [分表]
        • 更新销量 [mq]
        • 更新库存流水状态
      • createOrderAsync [异步创建订单(没有实际创建,只是mq发送消息)][][][&OrderController.create]
        • 生成库存流水 [itemService.createItemStockLog]
        • mq第一阶段发送消息 [rocketMQTemplate.sendMessageInTransaction][mqTransaction-1.send]
        • 得到响应 [][mqTransaction-2.OK]
    • PromotionService
      • generateToken
        • 生成token
        • 放到redis里缓存
  • mq
    • producer
      • executeLocalTransaction [执行本地事务][mqTransaction-3.transaction]
        • createOrder [创建订单][mqTransaction-4.commit/rollback][orderService.createOrder,itemService.updateItemStockLogStatus(false)]
      • checkLocalTransaction [回查][mqTransaction-5.check]
        • checkStockStatus [查询流水状态][mqTransaction-6.checkStatus&7.commit/rollback][itemService.findItemStorkLogById]
    • consumer
      • DecreaseStockConsumer
        • onMessage [itemService.decreaseStock]
      • IncreaseSalesConsumer
        • onMessage [itemService.increaseSales]

技术点梳理

mq原理

RocketMQ两阶段提交的过程概述

1.事务消息发送及提交:

  • 发送消息(half消息)。
  • 服务端响应消息写入结果。
  • 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
  • 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)

2.补偿流程:

  • 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
  • Producer收到回查消息,检查回查消息对应的本地事务的状态
  • 根据本地事务状态,重新Commit或者Rollback

其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。

mq的使用场景

producer:发送本地事务;回查

consumer:增加销量、扣减库存

令牌桶 & 漏桶

令牌桶:业务组件会出现高峰,能短期处理些高发情况;
漏桶算法:业务组件处理速率恒定;

漏桶算法

令牌桶算法

todo

结合代码对业务逻辑进行梳理;
结合业务实现对涉及到的技术点进行梳理;