订单系统篇

库存系统

这里的库存系统,是和商品系统写在一起了,按道理应该将库存和商品分两张表,比较合理。因为商品表总的来说都是读操作,但是库存表较多的是写操作,所以最好单独分一个库存表。

需要用到缓存?

库存系统,对库存进行操作,如果都在库中进行是一个很可怕的事情,如果请求率和修改率能达到1:1,其实压根不会出问题。如果一但,请求率高于能够修改的速率,1.1:1那么1s就是0.1,2s就是0.2,越往后后续等待的窗口越长,所以提高修改效率非常重要,也就是会使用到缓存。我看了许多关于缓存一致性的方案,但这些方案并不适用于库存,库存是一个修改频繁的键,如果每次修改都去库中同步,那还不如直接读取库算了。订单查询这种操作使用的就是延迟双删

需要使用缓存另一个重要原因是redis处理是单线程,如果缓存的扣除由订单生成的时候,直接操作数据库,如果只使用一个消息队列单线程,其实没啥关系,但是如果使用了多线程,对于同一个商品的修改,一个时间内,只有一个线程能修改库存成功,如果有2个商品在同一个事务修改库存,很容易陷入事务大量失效的场景,多线程此时就是噩梦般存在,此时设计上对于订单创建上,商品代码的侵入性会很高。所以将整体库存消耗交给redis,再通过有限状态自动机中的action部分,通知数据库修改库存。

库存初始化

使用lua脚本先get key,返回一个list,然后将缺少的list查库,然后setnx设置key,设置过期时间60s(这个时间是允许数据库最终一致性的时间,也就是消息队列消耗最大时间)。

如何防止超卖

超卖在库存中肯定不被允许,使用数据库肯定不需要考虑这个问题,使用缓存的时候,需要考虑到原子性问题,使用decs和incs可以保证库存的原子性,然后将值返回,如果小于0就说明不够了。

如何保证库存系统的原子性?

很多时候,商品都会有多件,需要保证这些商品同时足够。第一个版本的时候,我是直接使用redis的ops来操作,遇到不够的则依次反向操作一次,但是如果此时发生故障,库存系统肯定不在准确,后续学习了一下午lua脚本,之后的部分redis有多个操作需要保证原子性的时候,都会使用lua脚本。例如商品库存扣除脚本如下。

local inventory = KEYS
local consumer = cjson.decode(ARGV[1])
local orderNo = cjson.decode(ARGV[2])

redis.log(redis.LOG_DEBUG, consumer)
-- return consumer[1]
--
for i = 1, #inventory
do
    --     print(inventory[i])
    local invent = tonumber(redis.call('get', inventory[i]))
    redis.log(redis.LOG_DEBUG, invent)
    if (invent < consumer[i])
    then
        return -1;
    end
end

-- 扣减库存
for i = 1, #inventory
do
    redis.call('DECRBY', inventory[i], consumer[i])
end
for i = 1, #orderNo
do
    redis.call('sadd', 'orderInventory', orderNo[i])
    redis.call('sadd', 'inventorySuccess', orderNo[i])
    -- 延迟消息逻辑比较复杂,
    -- 首先发送一个mq,用来之后核对库存。
    -- 扣减库存 执行当前lua脚本
    -- 执行保存订单操作。
    -- 如果执行成功,删除 orderInventory,对应值。
    -- 如果保存成功后,但是删除过程或者其他过程出了问题,可以由延迟消息补救一次。

    -- 延迟消息主要是判断3点,第一点如果订单没有创建,那么需要返回库存。
    -- 如果创建成功。则不做任何操作。

    -- case 1.非常特殊的情况,订单插入的时候,由于主键约束,不会出现幂等状况,由于是先删除的库存,所以无法保证
    -- 2次相同的请求,都进行扣除了库存操作,所以需要将o和i的值设置为orderNo+时间戳,保证创建失败的时候,将重复消息返回。

    -- orderInventory是用来确认订单执行成库存的,如果订单直接保存成功,肯定是能扣减库存成功的。
    -- inventorySuccess用来确认,订单是否成功扣减。
end
return 1;

库存补偿

其实这个问题就涉及到了分布式事务一致性问题了。不再是缓存一致性,但好消息是库存只与订单是否生成有关系,与支付状态无关。订单生成必须扣除库存,订单未生成不能扣除库存。

如果订单没创建成功,库存是必须返回的,这个时候需要用到消息队列来维护库存的补偿系统。这里使用消息队列的死信交换机+队列完成延迟消息。每一个库存操作都需要在10s后检查订单是否生成成功。没有生成成功则返回库存到cache,删除orderInventory键。这个是一个set类型,里面是orderNo+timestamp。主要作用是确保库存确实倍扣除了,如果这个时候库存压根没扣,但是延迟消息发现没有订单,把库存返还回去,逻辑不正确。还有就是幂等操作,订单超时的情况下,订单入库是入不了的,但是库存返回,如果只有订单号可能会出现误判。所以需要结合时间戳判断,是否是同一个订单,如果是重复订单应该返回库存。

通过库存的补偿,缓存,在订单创建完成肯定是没有问题的。

而数据库修改,则是必须在订单创建的基础上,才能进行数据库修改。所以二者在同一个事务内,通过消息队列通知库存系统来修改数据库。将整个耗时的部分从订单创建中剥离。

 @Override
    public void saveCommodityOrder(List<CommodityOrderNewDTO> orderNewDTOS) {
        long s1 = System.currentTimeMillis();
        List<CommodityReDTO> list = new ArrayList<>();
        List<String> orderNo = new ArrayList<>();
        long current = System.currentTimeMillis();
		// 预维护
        for (int i = 0; i < orderNewDTOS.size(); i++) {
            list.addAll(orderNewDTOS.get(i).getCommodityReDTOList());
            String order = orderNewDTOS.get(i).getOrderId();
            String newOrder = order + current;
            orderNo.add(newOrder);
            CommodityCacheInventoryMO inventoryMO = new CommodityCacheInventoryMO();
            inventoryMO.setOrderNo(newOrder);
            inventoryMO.setReDTOList(orderNewDTOS.get(i).getCommodityReDTOList());
            mqClient.sendDelayTask(MqMsgTypeEnum.ORDER_CACHE_INVENTORY, inventoryMO, 10 * 1000);
            orderNewDTOS.get(i).setTimeStamp(String.valueOf(current));
        }
        
		//扣除库存
        commodityService.subInventory(CommodityDecorateBO.convert(list), orderNo);
        
        long s2 = System.currentTimeMillis();
        List<CommodityOrderNewDTO> dtos = orderNewDTOS;
		//保存订单自动机入口
        doSaveOrder(dtos);
        //成功创建订单后,删除redis中埋得点。
        commodityService.successInitOrder(orderNo);
        long s3 = System.currentTimeMillis();
        //支付订单自动机入口
        payOrders(dtos);
        long s4 = System.currentTimeMillis();
        //        long s3=System.currentTimeMillis();
        log.info("inventory time{},record time{},pay time{}", s2 - s1, s3 - s2, s4 - s3);
    }

订单未支付超时返回库存

超时未支付,也是用的延迟队列实现。过程与之前类似,返回缓存与数据库。此时如果未支付,直接返还库存即可,不需要考虑事务性。此时的想法是一定要返还库存。但是如果存在极端情况,例如支付系统宕机了,那就把此条消息保存到redis中,由循环事件检测支付系统,如果一旦存活,将redis的消息进行重试消费。如果redis也宕机了,只能将此消息直接抛出异常,将消息此通道消息阻塞,直到超时(都宕机了玩个屁)。所有的数据库操作都会发到同一个队列,单线程处理。

库存内存队列

这部分其实已经1周前花了2小时,就完成了一个小的demo模块。结果上来说,在1k线程和10k线程的时候,内存队列都可以比直接插入快3倍。而且内存队列,总体上使用到的线程池核心大小也才10,对于io密集型操作来说应该并不算多。

我的demo的文档

但是内存还是存在可用性问题,可能存在服务不可用问题,此时需要将服务转移,需要现在这个方向上寻求解决方案。

面试的时候,面试官提到了inventory hint,简单的浏览了阿里云官方的技术博客后,发现整体思路上我这个想法是没问题的。实现上,他将这个过程是写入了数据库层面。

inventory hint官方博客

inventory hint

整体流程:

  1. 标记热点sql
  2. 对热点sql进行积压,sql积压100us。
  3. 合并sql,过程p1:insert 流水+p2:update inventory。
  4. p1过程先要完成select,查询以及插入的,直接返回已经插入过的。
  5. 再次合并流水表,开始事务,过程如下。
  6. 返回结果。
begin;
insert(xxx);
update(xxx);
end;

先insert在update持有锁时间短一点。

优化部分

桶设计,也就是拆分请求粒度,但是他这个应该是在数据库层面完成的,所以并不是真实存在的过程。把他的话先记录过程来,这部分我目前还是得在研究下,已经在多个地方看到这种设计思想了,有拆分是物理结构拆分,表现是真的在数据库上拆分了,有的只是内存拆分。这里在记录一个字节红包功能的一个链接。

红包系统

为了更通俗和直观的描述,缓存集群的一个key就对应于于一个"分桶"。要实现一个基于缓存分桶方案的高扩展性的库存系统,分桶的设计至关重要,比如一个热点商品应该对应多少个分桶,分桶的数量能否根据当前的业务变化做到弹性的伸缩

  • 分桶预分配库存:当分桶初始化后,每个分桶应该保存多少库存量。不一定在预分配库存阶段将该商品的库存数量从DB全部分配到缓存中,可能是一种渐进式的分配策略,DB作为库存总池子
  • 分桶扩容/缩容:分桶数量的变化,扩缩容操作本质上是调整桶映射管理内的信息,加入或者减少桶,桶信息一旦增加或者减少了,扣减链路会秒级感知到,然后将用户流量引导或者移除出去。从上面的DB架构图可以看出,比较简单的实现方式就是根据当前热点商品的桶数量取模
  • 桶内库存数量扩容/缩容:即每个分桶内该商品的库存数量变化,扩容场景主要用于当该分桶内库存接近扣减完成时,系统自动去MySQL库存集群总池子里捞一部分过来放进桶内。缩容场景主要场景在于桶下线后将桶内剩余的库存回收到库存总池子中
  • 合并展示:在基于缓存的分桶设计中,由于同一种热点商品拆分成了多个key,所以在前端界面展示上同样会带来挑战,需要做库存的合并

不足

订单的数据库操作都是单线程处理,肯定会造成瓶颈。或许可以将多个商品返回库存操作拆分成,多个单独的请求,这样可以造成失败请求减少,但对属于统一订单的商品原子性可能造成破坏。这样好处是,对相同的商品分到同一个队列,加速库存入库。消息队列不能长期宕机,一旦消息队列g了,那么会造成消息积压,库存数据库一致性受损。整个过程中数据库可以g,redis可以g,代码也可以g,但是消息队列肯定不能g。

思考

  1. 真的需要同步数据库嘛???

网上一直说,数据库作为持久化是必要的,但现在redis这么稳定,直接放redis里也可以吧,宕机了也有从服务器。

  1. 持久化可以尝试用写多读少的组件。

之前看过lsm数据结构,感觉这个应该很适合库存场景,由于数据库写太费时间采用缓存,而lsm可以做到o(1)的增删改过程,非常适合库存系统,可以同步双写缓存和lsm,有读取场景,也只会有一个地方能够读取,慢点就慢点吧。或者说频繁的改动,势必会将库存打入到lsm的memory层,即使宕机的时候也能够很快的访问到。

  1. 组件的意义

引入组件必然会带来一系列问题以及时间消耗,但是同时它可以给你解决很多问题,它可以给你保证可用性,事务性,原子性等等性质。让编码过程不需要再去担心。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值