商城系统下单库存管控系列杂记(二)(并发安全和性能部分延伸)
 
 
前言
 
参与过几个中小型商城系统的开发,随着时间的增长,以及对系统的深入研究和测试,发现确实有很多值得推敲和商榷的地方(总有很多重要细节存在缺陷)。基于商城系统,无论规模大小,或者本身是否分布架构,个人觉得最核心的一环就是下单模块,而这里面更相关和棘手的一些设计和问题,大多时候都涉及库存系统。想想之前跟某人的交流,他精辟点评“库存管控做得好,系统设计就成功了一半”,自己颇有认同。围绕这个点,结合目前经验和朋友间的交流(包括近来参阅其他文章提到的点),闲来做些整理记录,也许不太完整,但总归希望能有更多启发,自己往后也会重新揣摩。当然,文中若有不妥,欢迎指正。
 
 
正文
 
谈及”下单“,就立刻想起前年参与的一个基于微信的小型商城系统,里面下单这块本身谈不上复杂,大概可以这样描述提交过程:用户提交商品订单,系统核对用户提交的订单,校验商品(商品价格、优惠折扣、积分等),检测附属信息(地址运费等),一切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.
 
 
 
 

转载于:https://www.cnblogs.com/bsfz/p/7824428.html

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

  1. prusai3打印机使用教程_打印虎原创RepRapPrusai33D打印机校准图解教程系列之二.pdf...

    [打印虎原创]RepRap_Prusa_i3_3D 打 印机校准图解教程系列之二 在打印虎的上一篇教程, [打印虎原创]Prusa_i3_3D 打印机校准图解教程-基 础篇中,我们介绍了最重要.最基础 ...

  2. 华夏旅游CMS商城系统操作手册

    华夏旅游CMS商城系统操作手册 一.系统概述 二.系统运行环境 1.PC端 2.移动端 三.系统功能模块 1.用户注册登录模块 1.1.系统注册方式 1.2.用户注册相关规则 1.3.用户登录 2.商 ...

  3. java开源b2b2c商城系统_java开源b2b2c商城系统有好用的吗?

    java语言是许多商家选择b2b2c商城系统都会考虑的语言之一,因为这种语言开发出来的商城系统会更加安全稳定.开源商城系统一般都以价格实惠受到商家欢迎,所以java开源商城系统也受到关注.那么java ...

  4. asp.net网上商城系统VS开发sqlserver数据库web结构c#编程计算机网页源码项目

    一.源码特点         asp.net 网上商城系统是一套完善的web设计管理系统,系统具有完整的源代码和数据库,系统主要采用B/S模式开发,开发环境为vs2010,数据库为sqlserver2 ...

  5. 安卓系统源码编译系列(一)——下载安卓系统源码教程

    最近需要编译安卓系统,咨询了一个编译过安卓系统的朋友,说是下载源码就得下载两天,于是做好了长期抗战的准备,开始了下载安卓源码的旅程.在刚开始下载时,可以参照的内容只有官方教程,于是跟着官方教程一步一步 ...

  6. 毕业设计之基于springboot的开源商城系统

    一.介绍 本项目是一个基于springboot的开源商城系统 ,前后端分离. 二.功能模块 平台端 管理端 用户手机端 首页 会员 订单 商品 促销 店铺 运营 统计 设计 三.技术架构 前端 vue ...

  7. jsp源码商城系统Myeclipse开发mysql数据库servlet开发java编程计算机网页项目

    一.源码特点   JSP 源码商城系统 是一套完善的web设计系统,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统采用serlvet+dao+bean  mvc 模式,系 ...

  8. Java源码美风的网上花店商城系统

    Java源码非常不错的网上花店商城系统 大家好,小辰哥又来啦,今天给大家介绍一个精美风的网上花店商城系统 文章目录 Java源码非常不错的网上花店商城系统 前言 一.项目简述 二.项目运行 1.运行环 ...

  9. Windows系统安装及优化系列(二)在PE下安装Windows10系统以及Gho镜像的安装方法

    说实在的我也是个刚进入IT世界的小白.写这个文章系列主要是为了给一些分享一些Windows系统的安装方法,因为我经常帮同学重装系统,我就在想能不能出个教程来让他们自己动手折腾,体验一下重装系统的乐趣, ...

最新文章

  1. IDEA 真牛逼,900 行又臭又长的类重构,几分钟搞定?真香!
  2. 5G风起,四大IoT迎风爆发!智能家居与电表高速增长
  3. 【Android 安装包优化】WebP 图片格式兼容与性能 ( Android 中的 WebP 图片格式兼容问题 | Android 中的 WebP 图片格式性能 )
  4. 折半查找算法及分析(手工过程)
  5. opencv python轻松入门_OpenCV轻松入门 面向Python
  6. Could not resolve host: 'localhost 报错解决办法
  7. [BZOJ]4650 优秀的拆分(Noi2016)
  8. c++代码整洁之道pdf_软件工程-实践者的研究方式的阅读(代码大全后面再说)...
  9. 树莓派之Debian游戏(部分)
  10. 2012第二届GIS制图大赛——公开课技术问题答疑(珍贵资源哦!)
  11. PWM波转DC直流之二阶RC低通滤波器设计
  12. php 车牌号限号,机动车限行尾号今天起轮换 周一至周五分别限行 4 和 9、5 和 0、1 和 6、2 和 7、3 和 8...
  13. 矩阵相乘c语言代码用指针实现,矩阵相乘C语言实现
  14. xctf攻防世界 MISC高手进阶区 2-1
  15. VMware下如何虚拟软盘启动
  16. 全球与中国相机模组胶黏剂市场运营现状及投资可行性评估报告2022-2028年
  17. Python3 中英文列表输出对齐
  18. 什么是反射 反射怎么用
  19. 【论文笔记_自蒸馏_2020】Regularizing Class-wise Predictions via Self-knowledge Distillation
  20. EJB----EJB 概念

热门文章

  1. OpenAPI使用(swagger3),Kotlin使用swagger3,Java使用swagger3,gradle、Maven使用swagger3
  2. 2022-2028年中国卫星互联网产业深度调研及投资前景预测报告(全卷)
  3. 更改Jenkins升级站点
  4. 2022-2028年中国数字乡村建设深度调研及投资前景预测报告
  5. 2022-2028年中国热塑性聚酯PBT工程塑料行业市场全景调查及发展趋势分析报告
  6. 【Spring】基于xml实现事务控制(银行转账)
  7. LeetCode简单题之移动零
  8. 硬核科普:到底啥是云原生?
  9. AI人工智能天机芯芯片
  10. NVIDIA FFmpeg 转码技术分析