前言
在后台开发领域,高并发的扣减一直是比较热门的话题,在各类技术博客、大会分享以及面试问题中出现频率都非常高。可见它的重要性和技术知识点的密集性。
此次主题的技术分享将分上中下三节来介绍,将由浅入深,由简至繁的介绍扣减业务中的一些高并发构建方案,这些方案中实现复杂度、支撑的性能和并发的量级有所区别。
本文为第一节(上)的介绍。
对扣减业务的理解
相信大家对秒杀高并发扣减库存有所了解,这只是扣减类业务中的一个有代表性、具备一定记住复杂度的场景,它并不能代表扣减类业务的全部场景。
除了秒杀之外,常见的扣减类业务有:
- 购买一个或多个商品时扣减的库存
- 针对用户设置的某个或某几个商品做到购买的次数限制
- 支付订单时扣减的金额等等。
上述业务场景有几个共性点。
扣减类业务定义:
扣减类业务是指在电商、金融、物流等领域中,需要对某种资源进行扣减的业务,如库存扣减、余额扣减、物流资源扣减等。 这类业务通常需要保证数据的一致性和准确性,避免出现超卖、欠费、漏发等问题。
扣减类业务的处理过程通常包括以下几个步骤:
扣减业务关注的技术点
发生资源扣减,必然就会存在返还对应的资源。
比如用户购买了商品之后,因为一些原因想要退货,这个时候就需要将商品的库存、商品设置的购买次数等,因此在实现的时候还需要考虑返还。
基于扣减的业务的定义,我们把关于扣减的实现需要关注的点如下:
- 当前剩余的数量需要大于等于当次需要扣减的数量,既不允许超卖
- 对同一个数据的数量存在用户并发扣减
- 需要保证并发一致性,可用性和性能,性能至少是秒级
- 次扣减有多个数量时,其中一个扣减不成功,既不成功需要回滚
返回实现需要关注的技术点:
- 必须有扣减才能返还
- 返还的熟练必须要加回来,不能少
- 返还的总数量不能大于扣减时的总量
- 一次扣减可以有多次返还
- 返还需要保证幂等性
在了解了扣减的业务的场景定义,确定了在实现时需要包含的功能点以及各功能点的实现要求后,下面将开始介绍扣减资源的实现方案。
下面将以库存扣减为例介绍实现方案,其他扣减场景,比如限次购买、支付扣减等技术方案基本类似,可以举一反三。
使用数据库实现扣减
顾名思义,纯数据库的方案就是扣减业务的实现完全依赖数据库提供的各项功能,而不依赖其他额外的一些存储和中间进了。
好处:逻辑简单,开发及部署成本低。
主要是依赖各类主流数据库提供的两个特性:
- 基于数据库乐观所在方式,保证数据并发扣减的强一致性。
- 基于数据库的事务实现批量扣减,部分失败时的数据回滚。
基于上述特性实现的架构方案如下图所示:
扣减服务一般是后端内部实现的 rpc
服务,不涉及前端,通常是供后端其他服务调用,比如购买等。
数据库主要涉及两张表,扣减剩余数量表和流水表。
- 扣减剩余数量表主要字段结构
字段名 | 英文名 | 含义 |
---|---|---|
商品 ID | f_sku_id | 商品标识 |
当前剩余可购买数量 | f_leaved_amount | 剩余可用数量,随扣减实时变化 |
- 流水表主要字段结构
字段名 | 英文名 | 含义 |
---|---|---|
扣减编号 | uuid | 唯一标识一次成功扣减记录 |
商品 ID | f_sku_id | 商品标识 |
本次扣减数量 | num | 本次需要扣减的商品数量 |
扣减接口实现流程
接口实现流程图如下:
流程分析:
首先进行的是数据校验,在其中可以做一些常规的参数格式校验。
其次,它还可以进行库存库存的前置校验。比如当数据库中库存只有 8 个时,而用户要购买 10 个,此时在数据校验中即可前置拦截,减少对于数据库的写操作。
如果在校验时剩余库存有八个,此时校验会通过,但在后续的实际扣减时,因为其他用户也在并发的扣减,可能会出现了幻读。
因此,用户实际去扣减时,不足两个导致失败,这种场景就会导致多一次数据库查询,降低了整体的扣减性能。
在实践中,前置校验是必须的。
在事务之后,则是数据库更新操作,因为用户扣减的商品数量可以是一个或多个,只要其中一个扣减不成功,则判定用户不能购买。
注意,因为在事物之后对商品使用 for 循环进行处理,每一次循环都需要判断结果,如果一个扣减失败,则进行事物回滚。
单条商品的扣减 SQL如下:
此 SQL 采用类似乐观锁的方式实现了原子性。在外条件里,判断此时需要的数量小于等于剩余的数量。
在扣减服务的代码里,判断此 SQL 执行的返回值,如果值为 1,表示扣减成功,即用户此次购买的数量、当前的库存可以满足,否则返回 0 进行回滚即可。
扣减完成之后,需要记住流水数据,每一次扣减时都需要外部用户传入一个UUID
作为流水编号,此编号是全局唯一的。
用户在扣减时传入唯一的编号有两个作用:
- 第一个作用是当用户返还数量时,需要带回此编号,因为标识次返还属于历史上的具体哪次扣减
- 第二个作用是进行幂等性控制,当用户调用扣减接口出现超时时,因为用户不知道是否成功,用户可以采用此编号进行重试或反查,在重试时使用此编号进行标识防重。
扣减架构实现升级
我们知道多一次查询,就会增加数据库库的压力,同时对整体服务性能也有一定的影响,因此可以通过读写分离进行升级优化。
升级后的接口实现流程如下图所示:
可以看到图中的升级策略采用数据库的读写分离来实现,数据库已有的功能,改动较小。
扣减服务分两个数据操作,查询时读从库,扣减(写操作)时写主库。
读写分离之后,根据二八原则,80%的均为读流量,主库的压力降低了 80%。
但采用了读写分离,也会导致读取的数据不准确(主从可能会有延迟)的问题。
但是库存也是实时变化的,短暂的差异业务是可以容忍的。
扣减架构实现再次升级
上面提到的因为在扣减前为了降低数据库的压力,增加了前置校验导致的性能下降问题并没有得到太多实质性的升级解决。
那么接下来我们该从什么方向上解决这个问题了?如下图所示:
可以看到升级后,增加了缓存,用于提供读数据,提高性能;另外缓存的数据由从数据库的 binlog 异构同步过来。
基于该实现架构,提高了整体性能,从而提高用户的体验性。
综上所述,纯数据库扣减方案有以下几个优点:
- 实现简单,即使读使用了前置缓存,整体代码工程就两个,即扣减服务和数据映射同步服务
- 使用数据库的 ACD 特性进行扣减,业务上库存数量既不会出现超卖,也不会出现少卖
不足之处:
- 但不足之处是当扣减 SQ 数量增多时,性能会较差。因为对每个 SQ 都需要单独扣减,导致事物非常大,极端情况下可能出现几十秒的情况。
此扣减方案适用场景
在上述的优点和不足背景下,此方案在落地上有哪些适用场景?
- 在一些企业内部 ERP 系统里的次数限制
- 中小电商占领的库存管理
- 政府系统等场景里
其实此方案是比较适合系统的用户并发数、对于请求的耗时要求、购买商品的数量相对较小的场景。
总结
综上所述,不同的扣减库存方案各有优缺点,需要根据具体的业务场景和需求进行选择。
一个优秀的方案一定是建立在对业务需求和对本质问题的理解之上。
如果你觉得今天的内容对你所启发,欢迎分享给身边的朋友,我们一起交流。
思考题:
可以把你的想法、思路或者总结写的品论区。