[原创]商城系统下单库存管控系列杂记(二)(并发安全和性能部分延伸)

简介: 四、阐述关于并发环境中库存管控的一些案例问题,以及涉及到的相关技术实现细节 库存扣减,简单来说,就是在对应的存储器中(数据库或者持久缓存)将对应商品的数量减少。 数据库设计时,一般包含但不限于 商品主表,商品规格表,商品库存表,商品库存流水日志表等等。
 
商城系统下单库存管控系列杂记(二)(并发安全和性能部分延伸)
 
 
前言
 
参与过几个中小型商城系统的开发,随着时间的增长,以及对系统的深入研究和测试,发现确实有很多值得推敲和商榷的地方(总有很多重要细节存在缺陷)。基于商城系统,无论规模大小,或者本身是否分布架构,个人觉得最核心的一环就是下单模块,而这里面更相关和棘手的一些设计和问题,大多时候都涉及库存系统。想想之前跟某人的交流,他精辟点评“库存管控做得好,系统设计就成功了一半”,自己颇有认同。围绕这个点,结合目前经验和朋友间的交流(包括近来参阅其他文章提到的点),闲来做些整理记录,也许不太完整,但总归希望能有更多启发,自己往后也会重新揣摩。当然,文中若有不妥,欢迎指正。
   
正文
 
谈及”下单“,就立刻想起前年参与的一个基于微信的小型商城系统,里面下单这块本身谈不上复杂,大概可以这样描述提交过程:用户提交商品订单,系统核对用户提交的订单,校验商品(商品价格、优惠折扣、积分等),检测附属信息(地址运费等),一切Pass,操作库存(记录/预扣),生成订单及相关联的明细数据。此时下单Ok,那么后续则是等待用户的及时付款了。
 
然而,看似如此简单的一个流程,放在并发环境下,就暴露了足够多的问题。深入进去,首当其冲的就是库存管控。包括但不限于库存的扣减方式,如何安全操作,以及减少性能损耗等等。
 
【为了方便独立成文,原谅在内容排版上的一点点个人强迫症】
【本文内容由上一篇扩展论述(详见:商城系统下单库存管控系列杂记(一) http://www.cnblogs.com/bsfz/p/7801980.html)】
   
四、阐述关于并发环境中库存管控的一些案例问题,以及涉及到的相关技术实现细节
 
库存扣减,简单来说,就是在对应的存储器中(数据库或者持久缓存)将对应商品的数量减少。
数据库设计时,一般包含但不限于 商品主表,商品规格表,商品库存表,商品库存流水日志表等等。但这里为了方便后续阐述,将其简化为一张表——商品表(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,也属于超卖(虽然不会扣为负数),如果放在线上,简直是一个定时炸弹,不,还不仅仅只是这一个定时炸弹。
 
围绕解决这样的问题,考虑到并发安全以及并发性能,产生了各种解决方案。大体基于两种机制:悲观锁和乐观锁。在诸多场景里,基于每种锁,都有配套的辅助手段,以及各自不同的侧重取舍和相关实现。
   
4.1 使用悲观锁的理念,实际就是在并发的关键地方,强制将“类似并行”改为串行,相关的一些处理方式:
 
4.1.1  数据库锁,利用数据库的自身的事务隔离机制(Isolation),进行排他操作。
 
       4.1.1.1
  极端的在查询时,直接开启事务设置行锁(rowlock)。串行目的是达到了,但即时在单机系统中,也无法承受巨大的性能损耗。并且最终的超卖问题也没有解决,非常不推荐。
 
4.1.1.2
  仅利用数据库在update时造成的排他锁,使真实更新时串行,并增加库存判断,若库存发生变动,则更新无效,超卖问题也不会发生。譬如(以SQL SERVER 举例):
  update PT set qty = qty - N  where id = pid and qty >= N;
 
  严格来讲,这依然是一个较粗的粒度,但不得不说,在单机环境下有一定的可行性。同时,需要考虑高并发情况下(例如商户举办活动,同时参与用户过多)存在一定性能瓶颈,数据库IO负载过大。此时需要结合其他方案,包括增加上层缓存层等。甚至部分场景需要单独设计一套流程(例如秒杀抢购场景,首先就是应用到队列,否则网站可能没崩溃在并发请求数上,而是直接挂在了DB上,后面会有相关阐述)
 
4.1.2  使用程序锁(单机线程锁和分布式调度锁),使部分关键代码串行。
 
4.1.2.1
  极端的直接使用程序自带的全局线程锁,以.NET Framewok 举例,里面有各级粒度的锁,常用的轻量锁有lock(Mointor语法糖)、SpinLock(自旋锁)。使用它们,最早大概是应用在“单例模式”的构建,原理本身不复杂,使用也方便,并且也达到了串行的目的。
  然而,放在下单库存管控这里,串行的却是所有用户进行任意商品下单操作,打击面太大(甚至直接上升到全面打击),对性能造成极大影响,不可行,不过多延伸,也不推荐。(曾经优化一个旧项目里的模块,初步Review代码时就发现了几处不经意的地方竟直接使用了这种写法,而开发人员还是两名老员工)。
 
4.1.2.2
  构建一个本地的线程锁管理器(这里称为LockerManage),统一分配锁对象(等待对象)。其本质是针对上面4.1.2.1方式的包装处理,实现类似“工厂模式”的机制。主要是通过它来生产具有唯一特征的Object对象,这个对象将会作为锁对象资源返回给Monitor等调用,并具有一定的使用时效,每次生成后保存在内部的线程安全的集合里,同时具有自动销毁机制(运行一个独立线程,定时检查清理)。其中有个小细节,为了优化管理器内部的并发问题,开始使用的是.NET Framewok 里自带的线程安全的字典集合(ConcurrentDictionary),后来经测试,发现并发处理并不理想,后面便换了其他方案(读写分离)。回归到下单这里,这里依然以商品P为例,首先调用LockerManage,获取一个以当前商品主键为标识的Object对象,然后在库存的预扣核对时,使用Mointor加锁处理。(当然,这里是本机锁,后续有说明)。这种方式对比数据库锁,则是降低数据库的操作,而将压力大部分转移到了程序上,但相对可以更灵活的去操控。
 
4.1.2.3
  使用分布式锁。上面的普通程序锁作为单机的存在,决定了其在分布式架构上的不可控性,而这时就有了分布式调度锁。它主要是为了方便解决分布式情况下,在多个Web程序内实现并发线程的一个管控。值得一提的是,这个“轮子”并不需要手动重新创造,目前市面上已经有相对成熟的解决方案,如利用Zookeeper和Redis。在AutumnBing项目中,当时选择的是Redis,使用的驱动库是StackExchange.Redis。(后续听到朋友提到Zookeeper更适合充当这样的角色,但由于目前自己还没有太多涉猎研究,暂时持保留态度)。当然,纯粹采用分布式锁,自然调用性能会有更多损耗。而相对更合理的做法,是结合单机锁搭配应用(试讨论,分布式锁放置外层,单机锁放置内部,每个站点各自维护)。
 
 
4.2  遵循乐观锁的理念,则是默许不会有太大的并发问题(聚焦在小粒度的商品P上,则是认为大多数情况下P不会被同时消费),“放任”线程的执行,不做管控。但是会在关键地方进行版本核对,假如失败,则内部重试或抛出失败信号。
 
 
4.2.1  数据库层面上,增加显式的版本号字段(ver)。
 
  购买商品P,下单这里需要获取到当前时刻对应的库存qty01,当前记录是版本ver01,然后在真实更新时,再次查询商品P的库存,以及对应的当前的版本ver02,如果 ver01 == ver02,那么可以更新。否则,当前数据已因并发被修改,无法更新。这更像是数据库的“不可重复读”,而出现这种情况后(高并发情况下,出现概率直线上升),必须附有关联的内部尝试机制(注意保证幂等性)。 这是一种实现并发管控的方案,但只适合存在并发,但并发量不太大的情况,否则,一是违背乐观锁的理念初衷,二是整体性能以及体验会大打折扣。
 
 
4.2.2  程序控制上,采取队列(queue)方式,进行相对集中化预受理,然后分发逐个处理。
 
  需要声明,这里本身执行原理,其实质依然离不开类似悲观锁的管控性质,一是入队时需要有个小粒度的锁机制保证串行(当然也可以是其他方式,这是队列内部的管控机制之一),二是出队,例如分发到不同服务上去处理,最终也是一个一个在操作更新(依然是某种程度上的串行)。但是,作为用户下单的提交,本身是保证了乐观的态度,一股脑“同时”或者“快速”接收,然后再考虑如何告知处理。
 
   由于单机队列的应用,会出现更多类似上面单机锁的一些额外问题,这里不推荐(当然你可以结合),也不做扩展说明。下面仅就分布式队列在大方向上举例阐述。
 
  如何采用分布式队列来实现下单以及库存管控呢?依然以商品P为例,用户同时购买商品P,本身是一个并发操作,但是我们可以将一系列的请求商品扣减数据Push到一个队列中(生产者开始生产),然后由专门的线程进行订阅消费(消费者开始消费)。暂且假定为一个线程在消费,那么该线程具体消费时,逐个将商品数据出队,进行库存扣减,这里必然不会出现并发。消费完毕,无论扣除库存逻辑上是成功还是失败,均给出一个应答(ACK)。注意这里并没有过多的拆分逻辑,而是将下单的一些操作扔进一个队列中,使用专门的程序去逐个或者逐几个(分批)处理。实际使用往往是根据业务,做更小粒度的拆分和调整。另外,关于技术框架选型,目前各类开源成熟的MQ项目比比皆是,个人圈子里了解到最多的还是 RabbitMQ,对于多个生产者以及与之配合的多个消费者,还有应答处理机制,包括本身的性能和高可用性,均极其出色。额外的,关于web前端,很多时候则是需要配合一些轮询机制来检查订单状态(当然,轮询这里也有一些具体细节,比如异步体验、轮询时长和状态重置等考虑)
 
 
 
五、涉及到分布式SOA架构体系(包括如今基于SOA开始流行的微服务架构)情况下的一些额外考虑。
 
首先声明,个人认为SOA只是一种架构上的抽离设计,本身与论述的库存管控没有直接关系。但这里以库存管控为例,也有需要额外考虑的地方。
 
我们假定在一个下单API中,包含了3个独立的API接口:A-积分扣减API,B-优惠券扣减API,C-库存扣减API。考虑一种情况:假定库存本身可以被合法扣除,并且执行C成功了,但是发生了其他问题,A或者B执行失败了,那库存该如何回滚。
 
必须纠正的是,在这样一个耦合性系统场景里(而上例仅是其中一种案例),需要解决的问题本质和库存如何扣减没有丝毫直接关系,其暴露的实质问题是如何实现一个分布式事务机制。这是一个比较大的专题,实现相对复杂,开发成本也足够高。基于单一RPC接口,到如今流行的更小粒度的微服务,都足够写一本书了。截止目前个人的了解,如早期的2PC (两阶段)、3PC(三阶段)、TCC(补偿事务),以及后来的纯消息列表式方案等等,均是一些无法达到完美的理论(性能、时效、复杂度等)。至于实践上,自然就没有绝对OK的方案,只能根据项目规模和实际业务做些取舍,最终得到一个尽量满足的“高可用”方案。以后待到经验足够,有机会尝试一下单独开篇讨论。(对于分布式事务,写过一些demo,却应用不深,以后会考虑抽个专门的时间在续篇中尝试撰写探讨)。 
 
 
 
六、结合高并发场景(如:秒杀活动),简单聊聊如何关联各类技术手段,进行下单及库存管控的应用。
 
在电商系统里,并发简直无处不在,目前较为突出的一个场景,则是秒杀活动。所谓秒杀,最简单直观的场景如下:在某个时刻,商品P开放购买(P的实际库存仅为1个或者几个),大批量的用户同时进行下单抢购。
 
秒杀时并发量之大远远超过一般情况下的并发(你要考虑到不止一个商品),甚至还会影响到商城里现有其他业务(这里讨论非独立部署)。需要考虑诸多细节,以及大量技术手段来进行有效管控。以下简单聊聊后台下单相关问题,不讨论其他前端处理技术,包括定时查询,页面静态化,网络带宽优化等。
 
6.1  明确业务本质需求,脱离业务,当然谈不了任何技术架构和实现方案。
 
  秒杀的业务场景,宏观上来说,就是一个典型的排序模型。谁先来,谁先得到。这里我们尽量简化举例:假定商品P库存为10,同时参与下单的用户数为100000。那么,最终只有开始的(理论上的)10个用户购买成功,其余99990个用户购买失败。商品库存被成功消费为0。
 
6.2  防作弊等安全监测,从RPC的第一个接口开始,就进行过滤。
 
  例如,在杂记上一篇中提到的(见第一篇主题三),做好基础的安全监测机制。如相同IP的僵尸账号,做限制IP的访问,并增加验证码等。同时,包括但不限于一些额外的业务辅助手段,如限制仅满足一定注册时间的用户可下单等。
 
6.3  限流机制,在外层计数,达到一个下单阈值,直接抛弃。
 
  从6.1中就可以发现,秒杀业务本身就注定了大部分人是抢不到的,那么针对大部分人的下单请求,完全就可以不做处理(直接抛弃)。在进行真正的下单操作之前,可在具体操作接口上,增加一个拦截计数器来统计,比如当计数超过3000时,后续下单直接返回抢购失败的信息。这样就将数据处理由大化小了,实现了限流(仅针对下单)。当然,具体实现时,这个3000名额推荐是筛选后的。比如,先过滤8000,从中随机抽取3000(这里不扩展)。
 
6.4  从数据库角度,首先就是要增加单独的临时缓存层。 
 
  即使是3000的量,在这个环节也肯定是不能直接操作数据库的(你要明白,实际秒杀的商品,不只一个),直接读库写库对数据库压力太大,甚至直接负载过大导致数据库挂掉。那么,针对这种情况,推荐的一种方案就是结合缓存来操作。譬如:把商品P * 10 这条数据提前Push到专门的缓存中,然后每次读取和更新,均是走的该缓存。这里额外提到一点,如果用户下单成功,预扣库存 -1,但又未进行安全时间内的支付,那么系统将自动回滚商品P的库存,进行 +1(当然,回滚同样需要协调处理并发)。
 
6.5  从程序角度,修改库存依然需要保证一定串行。
 
  首先,如果保证DAL的串行,可以是数据库上锁,也可以是程序上锁(或者队列)。但如果直接数据库上锁,诸多并发请求(依然考虑到,单时间内的多个商品被多用户抢购),即使前面削减了部分下单处理,数据库的I/O负载依然会很严重。那么,首先就是推荐乐观进队列,然后悲观进分布式程序锁,混合处理(即是对主题四的结合应用)。
 
 
 
结语
 
电商项目里,几乎处处是并发,无论是单机还是分布式架构。结合下单库存管控相关,我们可以深刻理解解决这些并发性能问题和并发安全顾虑,即使是同一类型的业务,也有诸多方案,每种方案都有一些细粒度的问题需要尝试克服,更需结合实际项目(具体业务性质和规模),做一些实现上的各种优化与权衡等。
 
 
[不知不觉又是凌晨两点多了,本文作为系列第二篇杂记(部分延伸篇),暂告一段落吧。第三篇,待续。该睡了,晚安。]
 
 
 
End.
 
 
 
 

目录
相关文章
|
18天前
|
数据挖掘 黑灰产治理
排队免单商城系统开发详细案例/方案项目/源码指南
排队免单商城系统开发设计是指开发一种商城系统,其中用户可以通过排队活动获得商品免单的机会。
|
7月前
|
安全
dapp预约抢单排单互助系统开发逻辑详细/功能说明/案例分析/方案规则/源码出售
Allow users to register accounts and verify their identities to ensure that the identities of participants are valid and authentic.
|
1月前
|
新零售 小程序 搜索推荐
排队免单模式小程序商城系统开发方案
新零售不再将线上和线下视为两个独立的销售渠道,而是将其整合为一个完整的销售生态系统
|
1月前
|
新零售 人工智能 供应链
排队免单返利商城系统开发|成熟源码部署|案例详情
新零售业是零售业发展的重要趋势,它通过技术的创新和变革,重新定义了传统零售业的模式和方式
|
1月前
|
新零售 供应链 数据挖掘
排队返利新零售身材系统开发|模式案例|详情
商业模式则是随着这些概念成熟运用与整合产生,并且最终形成一个将市场需求与资源整合起来的商业式系统。
|
2月前
|
小程序 开发者
【社区每周】小程序商品能力两项接口变动(11月第三期)
【社区每周】小程序商品能力两项接口变动(11月第三期)
39 0
|
2月前
|
设计模式 小程序 物联网
社区每周丨商家券开发接入流程描述优化及上周建议反馈(2.20-2.24)
社区每周丨商家券开发接入流程描述优化及上周建议反馈(2.20-2.24)
27 0
|
5月前
|
NoSQL Redis
淘东电商项目(75) -秒杀系统(用户操作频率限制)
淘东电商项目(75) -秒杀系统(用户操作频率限制)
29 0
|
6月前
|
存储 安全 前端开发
DApp公排互助预约抢单排单模式系统开发参考版/详细流程/方案逻辑/规则玩法/案例设计/源码程序
需求分析:与团队明确系统的需求、目标和范围,包括公排互助预约抢单排单模式系统的功能、规则、奖励机制等方面
|
6月前
|
存储 前端开发 安全
dapp矩阵公排互助预约排单抢单项目系统开发指南流程丨案例设计丨功能逻辑丨规则玩法丨项目方案丨源码程序
需求分析:与团队明确系统的需求和目标,包括公排互助预约排单抢单项目系统的功能、规则、奖励机制等方面。