商城系统下单库存管控系列杂记(一)(并发安全和性能基础认识)
前言
参与过几个中小型商城系统的开发,随着时间的增长,以及对系统的深入研究和测试,发现确实有很多值得推敲和商榷的地方(总有很多重要细节存在缺陷)。基于商城系统,无论规模大小,或者本身是否分布架构,个人觉得最核心的一环就是下单模块,而这里面更相关和棘手的一些设计和问题,大多时候都涉及库存系统。想想之前跟某人的交流,他一句“库存管控做得好,系统设计就成功了一半”,自己颇有认同。围绕这个点,结合目前经验和朋友间的交流(包括近来参阅其他文章提到的点),闲来做些整理记录,也许不太完整,但总归希望能有更多启发,自己往后也会重新揣摩。当然,文中若有不妥,欢迎指正。
正文
谈及”下单“,就立刻想起前年参与的一个基于微信的小型商城系统,里面下单这块本身谈不上复杂,大概可以这样描述提交过程:用户提交商品订单,系统核对用户提交的订单,校验商品(商品价格、优惠折扣、积分等),检测附属信息(地址运费等),一切Pass,操作库存(记录/预扣),生成订单及相关联的明细数据。此时下单Ok,那么后续则是等待用户的及时付款了。
然而,看似如此简单的一个流程,放在并发环境下,就暴露了足够多的问题。深入进去,首当其冲的就是库存管控。包括但不限于库存的扣减方式,如何安全操作,以及减少性能损耗等等。
一、简单提一提通常的库存扣除时机选择:“下单减库存”和“付款减库存”
首先表明个人观点,在大多数业务场景下,个人相对倾向前者——“下单减库存”。后续大部分解决方案的论述,也都是以这个为主要前提展开。当然,针对去年参与的某微信商城系统(之后用“AutumnBing”作为项目代号),两种是同时实现的 ——— 商户可以在管理后台,指定某商品的库存扣减方式。
两者在应用上的一些细节区别:
1.1 下单减库存:
用户下单时,后台进行预扣库存,当前用户体验不错。但当前用户若迟迟未付款,这种“吊单”就造成库存浪费,影响商户利益,同时也影响了其他用户的需求购物(除非,能做好一定风控和库存回滚,后续会有阐述,也是我更倾向的)。
1.2 付款减库存:
用户下单后过了几秒,进行在线支付,结果付款成功了,却发现库存已经不足,导致购物失败,严重影响购物体验。同时,还要考虑扣款的回退造成更多复杂性(除非,允许一定超卖,或者库存数量“不计” ,另外倘若是秒杀场景,则依然是无法应付)。
二、描述下非并发情况下,针对库存预扣的(其中)一种抽象流程
2.1
用户选择 商品P * 数量N,并提交订单,系统后台核心API接口 如SubmitOrder,进行接收处理。
2.1.1
在SubmitOrder中,假定商品P的库存足够,将对应商品规格的库存 -N。
2.1.2
在SubmitOrder中,倘若商品P的库存已经不足够,告知下单失败及原因。
2.2
订单付款设置有一定的时效,为M分钟。
2.2.1
在M分钟内,可以正常付款并流转后续服务。
2.2.2
超过M分钟,则当前订单自动处理为过期(或者直接Close),并将商品P的库存 +N,从而恢复库存。
2.3
用户取消订单,类似2.2.2,直接将当前订单状态改为取消(或者直接Close),并将商品P的库存 +N,从而恢复库存。
2.4
用户申请退款
2.4.1
未发货,可以直接申请退款,同时将商品P的库存 +N,从而恢复库存(Tips:某些极少数现存项目里,存在发货后才减库存的设计,那么无需补足,但这里不对比论述,否则本流程也会相应调整,也非重点)。
2.4.2
已发货,可以直接申请退款,但需要发回商品,等待相关处理(手动重新上架,或者补充商品P库存)。
三、额外说明,在不考虑并发情况,库存风险管控上的一些附属问题
商品P若被大量下单,这些订单中又存在相当大比例的未支付,此时商品P的库存会被瞬间清空,必定就需要针对这块的风控检测。(PS:其实这不是本文阐述的主要方向,但最近刚好在其他平台看到几篇相关的文章中有简单提到以下几点,既然有一定的关联性,本人就结合目前的想法,就尽量先抛出来,但不做过多延伸)。
3.1 商品P被同一用户刻意反复下单(非并发造成):
可采取在SubmitOrder之前,设置用户限购以及关联商品P的订单待支付核查。同时提供备用的手动黑名单机制。
3.2 商品P被同一IP的多个用户刷单:
这种情况,首先就有系统的分级检测风控,譬如第一级是设置验证码(针对用户),第二级是已购买检测(类似3.1),第三级是库存阈值报警通知(针对商家),第三级是黑名单拦截(程序拦截和手动拦截)等。其实这在“AutumnBing”项目里并未用到,而截止目前,本人身边也只有一位朋友提到了相关实际应用,并且是相对简单粗糙的实现,毕竟这块程序上能做的只是辅助。
3.3 商品P被不同IP的用户恶意下单:
比如某些竞争对手非法发起的有网络组织进行团体性恶意拍单,这种类似“DDOS”的洪流(条件类似)已经上升到了另外的高度上去了。除了结合上面的一些辅助手段,目前没有见过或者听过有效的处理方式。但值得一提的是,和DDOS场景本身不同的地方,如果这些用户账号不停下单,却未履约,那么会受到一些冻结处罚(配合时效),这会使得恶意攻击的成本更高 。
四、阐述关于并发环境中库存管控的一些案例问题,以及涉及到的相关技术实现细节
库存扣减,简单来说,就是在对应的存储器中(数据库或者持久缓存)将对应商品的数量减少。
数据库设计时,一般包含但不限于 商品主表,商品规格表,商品库存表,商品库存流水日志表等等。但这里为了方便后续阐述,将其简化为一张表——商品表(PT),该表仅包含两个字段——商品主键(id)和商品库存(qty )。
依然以商品P举例,其主键为pid,那么就是在下单时,将历史库存S修改为 S -N。具体到SQL里,原始操作大概是这样(以SQL SERVER 举例):
update PT set qty = (S - N) where id = pid ;
这是以前的最原始的操作方式,单粒度的看,也没什么大碍。然而,放在一个并发环境中,则立马暴露出诸多问题。
假定在同一时刻,有两个用户提交了订单,一样的操作,一样的商品,一样的数量。那么最终商品P的库存数量应该为 S - N - N。而执行上面的SQL,因为并发,导致两次查询到历史库存均是S(应该至少有一次qty为S - N),则更新完毕后,商品数量最终是 S - N。这种致命性的Bug,也属于超卖(虽然不会扣为负数),如果放在线上,简直是一个定时炸弹。
围绕解决这样的问题,考虑到并发安全以及并发性能,产生了各种解决方案。大体基于两种机制:悲观锁和乐观锁。在诸多场景里,基于每种锁,都有配套的辅助手段,以及各自不同的侧重取舍和相关实现。
考虑到本篇更主要的是做一些基础铺垫,也可为对于电商系统不太了解的朋友做一些相关引导,第一篇 ,暂告一段落。第二篇 —— 商城系统下单库存管控系列杂记(二) ,本人将找个专门的时间写,可能会篇幅上长很多,内容上紧接第四点,将会谈及较多的重要细节,以及相关应用实现,到时再进行具体延伸,以及其他扩展讨论。
End.