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

 
商城系统下单库存管控系列杂记(二)(并发安全和性能部分延伸)
 
 
前言
 
参与过几个中小型商城系统的开发,随着时间的增加,以及对系统的深刻研究和测试,发现确实有不少值得推敲和商榷的地方(总有不少重要细节存在缺陷)。基于商城系统,不管规模大小,或者自己是否分布架构,我的以为最核心的一环就是下单模块,而这里面更相关和棘手的一些设计和问题,大多时候都涉及库存系统。想一想以前跟某人的交流,他精辟点评“库存管控作得好,系统设计就成功了一半”,本身很有认同。围绕这个点,结合目前经验和朋友间的交流(包括近来参阅其余文章提到的点),闲来作些整理记录,也许不太完整,但总归但愿能有更多启发,本身日后也会从新揣摩。固然,文中如有不妥,欢迎指正。
 
 
正文
 
谈及”下单“,就马上想起前年参与的一个基于微信的小型商城系统,里面下单这块自己谈不上复杂,大概能够这样描述提交过程:用户提交商品订单,系统核对用户提交的订单,校验商品(商品价格、优惠折扣、积分等),检测附属信息(地址运费等),一切Pass,操做库存(记录/预扣),生成订单及相关联的明细数据。此时下单Ok,那么后续则是等待用户的及时付款了。
 
然而,看似如此简单的一个流程,放在并发环境下,就暴露了足够多的问题。深刻进去,首当其冲的就是库存管控。包括但不限于库存的扣减方式,如何安全操做,以及减小性能损耗等等。
 
【为了方便独立成文,原谅在内容排版上的一点点我的强迫症】
【本文内容由上一篇扩展论述(详见:商城系统下单库存管控系列杂记(一) http://www.cnblogs.com/bsfz/p/7801980.html)】
 
 
4、阐述关于并发环境中库存管控的一些案例问题,以及涉及到的相关技术实现细节
 
库存扣减,简单来讲,就是在对应的存储器中(数据库或者持久缓存)将对应商品的数量减小。
数据库设计时,通常包含但不限于 商品主表,商品规格表,商品库存表,商品库存流水日志表等等。但这里为了方便后续阐述,将其简化为一张表——商品表(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前端,不少时候则是须要配合一些轮询机制来检查订单状态(固然,轮询这里也有一些具体细节,好比异步体验、轮询时长和状态重置等考虑)
 
 
 
5、涉及到分布式SOA架构体系(包括现在基于SOA开始流行的微服务架构)状况下的一些额外考虑。
 
首先声明,我的认为SOA只是一种架构上的抽离设计,自己与论述的库存管控没有直接关系。但这里以库存管控为例,也有须要额外考虑的地方。
 
咱们假定在一个下单API中,包含了3个独立的API接口:A-积分扣减API,B-优惠券扣减API,C-库存扣减API。考虑一种状况:假定库存自己能够被合法扣除,而且执行C成功了,可是发生了其余问题,A或者B执行失败了,那库存该如何回滚。
 
必须纠正的是,在这样一个耦合性系统场景里(而上例仅是其中一种案例),须要解决的问题本质和库存如何扣减没有丝毫直接关系,其暴露的实质问题是如何实现一个分布式事务机制。这是一个比较大的专题,实现相对复杂,开发成本也足够高。基于单一RPC接口,到现在流行的更小粒度的微服务,都足够写一本书了。截止目前我的的了解,如早期的2PC (两阶段)、3PC(三阶段)、TCC(补偿事务),以及后来的纯消息列表式方案等等,均是一些没法达到完美的理论(性能、时效、复杂度等)。至于实践上,天然就没有绝对OK的方案,只能根据项目规模和实际业务作些取舍,最终获得一个尽可能知足的“高可用”方案。之后待到经验足够,有机会尝试一下单独开篇讨论。(对于分布式事务,写过一些demo,却应用不深,之后会考虑抽个专门的时间在续篇中尝试撰写探讨)。 
 
 
 
6、结合高并发场景(如:秒杀活动),简单聊聊如何关联各种技术手段,进行下单及库存管控的应用。
 
在电商系统里,并发简直无处不在,目前较为突出的一个场景,则是秒杀活动。所谓秒杀,最简单直观的场景以下:在某个时刻,商品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.
相关文章
相关标签/搜索