项目梳理-商城秒杀系统
商城秒杀
业务逻辑图
需求点梳理
项目简介 & 重点模块梳理
该项目是一个高并发场景下的商品秒杀活动。
除了一些边缘模块外,项目核心内容有两个:分别是下单秒杀前的削峰限流,和下单后保证用事务异步更新销量和扣减库存。
削峰限流
削峰限流主要是在用户下单前通过验证、限流等策略大都缓冲、平滑流量的目的。具体实现流程和用到的技术为:
- 通过增加验证码来平滑访问流量;
- 通过限制大闸,颁发令牌进行削峰(考虑到商品库存100,而秒杀用户量为10w的场景,只需颁发1000个令牌即可);
- 通过引入限流器(采用令牌桶算法),限制单机TPS,防止服务器挂掉;
- 通过引入线程池进行缓冲,再由线程池管理线程进行生产者发送消息。
更新销量
由于更新销量不影响用户秒杀操作,所以可以放到MQ里异步更新。否则直接操作数据库要加锁,影响性能。
扣减库存
由于秒杀是在高并发场景下进行的,在较短时间间隔有大量用户对同一商品资源进行互斥访问,数据库加锁的方式吞吐量太低,因此考虑先用Redis缓存库存,再最终将库存信息通过MQ异步写入数据库中。
超卖 & 少卖
超卖:超卖是说用户下单量超过商品的库存量。在真实场景中,对用户下单并未完全达成同步,导致少扣库存了,这将导致用户体验不佳,在实际中很少采用。
少卖:少卖是说最终用户的付款量少于商品的库存。在实际中,用户可能下单后又退款了,这对收益不会有很严重的影响,但是不会有超卖的用户体验不佳的问题,在实际中大多采用少卖。
少卖的解决方案:
一个想到的解决方案是利用RocketMq的延时消费机制,为队列设置一个延时时间,用户下单的时候进入队列,在规定付款时间之后进行消费,消费时,可以通过检查订单状态(已付款和未付款自动取消订单)来确定是否进行消费。
如果用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
- 异步创建订单
- getCaptcha [用户点击获取二维码][削峰限流-二维码平滑]
- UserController
- login
- logout
- getUser [根据token获取用户信息]
- ItemController
- 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 [查询流水状态]
- findItemsOnPromotion [查库存和活动]
- OrderService
- createOrder [][][itemService.updateItemStockLogStatus][&executeLocalTransaction.createOrder]
- 预扣库存 (并发场景下如果直接走库需要加锁,性能低) [redis]
- 生成订单 (生成订单流水,作为主键id,方便分表) [分表]
- 更新销量 [mq]
- 更新库存流水状态
- createOrderAsync [异步创建订单(没有实际创建,只是mq发送消息)][][][&OrderController.create]
- 生成库存流水 [itemService.createItemStockLog]
- mq第一阶段发送消息 [rocketMQTemplate.sendMessageInTransaction][mqTransaction-1.send]
- 得到响应 [][mqTransaction-2.OK]
- createOrder [][][itemService.updateItemStockLogStatus][&executeLocalTransaction.createOrder]
- PromotionService
- generateToken
- 生成token
- 放到redis里缓存
- generateToken
- ItemService [查询商品信息,扣减库存]
- 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]
- executeLocalTransaction [执行本地事务][mqTransaction-3.transaction]
- consumer
- DecreaseStockConsumer
- onMessage [itemService.decreaseStock]
- IncreaseSalesConsumer
- onMessage [itemService.increaseSales]
- DecreaseStockConsumer
- producer
技术点梳理
mq原理
RocketMQ两阶段提交的过程概述
1.事务消息发送及提交:
- 发送消息(half消息)。
- 服务端响应消息写入结果。
- 根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行)。
- 根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)
2.补偿流程:
- 对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”
- Producer收到回查消息,检查回查消息对应的本地事务的状态
- 根据本地事务状态,重新Commit或者Rollback
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
mq的使用场景
producer:发送本地事务;回查
consumer:增加销量、扣减库存
令牌桶 & 漏桶
令牌桶:业务组件会出现高峰,能短期处理些高发情况;
漏桶算法:业务组件处理速率恒定;
todo
结合代码对业务逻辑进行梳理;
结合业务实现对涉及到的技术点进行梳理;