抢购、闪购,从国外风靡后,国内各类网站都开始做相似的业务,我们耳熟能详的唯品会、淘宝、京东都有这类业务。抢购,更多出现在电商网站。那么,今天和大家一起学习下抢购业务形态的业务架构设计。
我们常见的抢购业务分两种: 限时抢购、限量抢购,我简单分了下这些case,如下图:
想必小米的抢购运营的最火爆了,每发一款新品,都限量发售,每次搞的大家心里痒痒的。记得之前还因为抢购太火爆,站点打不开,崩溃了。那么问题来 了:为什么抢购总是引发RD、OP恐慌?我理解是,爆品太火爆,瞬时请求太大,导致业务机器、存储机器都在抢购高峰时扛了太多压力。那么,我们今天以一个 抢购业务场景为例,看看如何扛住压力,做好抢购业务!假设,这时候我们接到了产品层面的需求,如下图:
PM也挺呵呵的,又要有时段的要求、又要有限量的要求,大而全呐!
首先,我们冷静的看看需求。
通过我们先行的抢购需求分析,我们画一个粗略的流程图,如下:
当然,数据源自第三方嘛,我们的数据层基于第三方资源数据构建。
首先看看库。
数据层的“商品库”,显而易见,用于存储第三方商品数据,通过第三方推、我们拉的方式来构建这个数据库信息。数据库层的“抢购计划”库,主要由旁路的“运营控制”环节产生的数据,由运营同学来维护抢购场次、商品数量。
业务层的“抢购库”,其实是商品库的子集,由运营同学勾选商品并配好该商品放出多少用于抢购,发布到业务层面的抢购库中。
业务层的Transaction Data,一会我们讲到与第三方对账时候,我们再说它。
我们此时回顾下目录,目录中我们讲,如何隔离前后端压力呢?做法是:
1. 让我们业务的压力,不会传递到资源方,避免造成资源方接口压力同比增长。所以,我们自己建了商品库,此时,第三方笑了。
2. 业务层与数据层解耦,我们让抢购库位于业务层,让商品库位于数据层。因为我们可以想象到,抢购高峰来临时,查询“商品还有没有?”的请求是最多的,若“有 没有”这种高频请求每次都去数据层,那我们其实就将业务、数据耦合在一起了,那么,就有了抢购库这个子库,在业务层抗压力。(这里可以明确的是,数据层的 商品库为关系型存储,业务层的抢购库为NoSQL的)
有了业务层的NoSQL(我们就用Redis吧)抗高频压力,数据层的商品库笑了。
这里就可以抛一个思想了:我们的架构设计中,需要分解压力,在互联网项目中,来自于用户的大流量不少见,这些流量最终都会落到一个地方,就看我们的设计如 何分解这个压力了,如何避免它层层传递。抛个case,我们的水平分布业务机器,也是考虑通过水平扩展实例的方式,来分解大流量压力。
不扯概念的东西了,我们回归我们的抢购业务。
有了简单的分层设计,解决的大家都担心的压力问题,我们就看看抢购业务的时序是怎样的。
我们的时序图分两个视角来说明:
1. 商品的角度;
商品角度的时序图,从左到右:资源方、数据层、旁路-运营控制层、业务层。如下图:录入商品 即商品从资源方发布到我们的数据层,形式可以是通过API、可以是通过文件传输、可以是我们去拉去。通过我们的代码逻辑,记录到我们数据层的“商品库DB”。
有了自建的商品库的数据,我们的运营同学就可以基于商品库设计每天的抢购场次(此事就有Web界面的事情,这里我们就不展开这块了),运营同学创建好一批抢购场次,记录在数据层的“抢购计划”这一关系型数据库中。
运营同学创建完抢购场次后,没完事,还得应产品需求,基于商品库,配置每场抢购场次中覆盖的商品,及商品的数量。这些抢购场次内的商品配置,会简单的记录 在业务层的“抢购库”中。(抢购库记录的信息较为简单,例如商品库中ID为123123的商品有100件,业务层的抢购库中只存ID 123123商品运营配了在第X场抢购中有5件)
假设此事某场场次的抢购活动已经开始,我们再看看用户角度的时序图:
用户点击某个商品的抢购按钮,业务层代码首先去看看抢购计划库此时是否开始(此步可缓存、也可cache在前端页面或Client,若有cache 的话,此步可忽略)。若抢购在进行中,此时业务代码需要查询商品在本次抢购中的库存还有否(高频请求,即图中“争取名额”阶段)。
“争抢名额”这块,一会我们细讲,先把时序图说完。
若用户抢到了名额,就允许用户跳转到第三方的支付页面产生消费。(此时第三方笑了),产生消费后,第三方自己的库存-1,并且可以实时、异步、完事对账的方式通知我们。
此时,我们回顾下目录,“如何保证商品库的库存可靠”。
我们构建商品维度的cache,上图中虽然说是“队列”,我们可以用redis的list来真正实现个队列,也可以通过 /–来实现。
假设商品A,运营配了20件,此事来了N多用户的请求,业务代码都会来查询cache_prefix_a_id这个队列的长度,若队列长度≤0,则 有权去–(减减)抢购库的商品库存。若队列长度在20件内,则通过业务代码内的等待来等待队头的位置,然后获得抢购权限。若队列长度太长,则可以直接返 回,认为商品已被抢空。
这时插入一个运营配库的时序,便于大家理解。该时序图有详细的说明和标注,就不展开了,如下图:
此时,我们可以想象,若上游用户的请求压力是N,这个N会压在业务层的抢购库,俗话说“责任止于此”:P
那么,我们回顾目录“如何和第三方多方对账”?
这里就要提到“Transaction Data”这个库了。
Transaction ID为用户维度的Session记录,用户从进入抢购业务开始,产生一个Transaction ID,该Transaction ID生命周期截止到用户跳转去第三方支付为止。期间在生活服务中产生的浏览、抢购行为均会挂靠到该Transaction ID之下,并会在跳转去第三方支付页时携带该Transaction ID凭证。最主要的是需要记录下:用户获得商品名额后,跳转去第三方时,这一行为。
考虑到Transaction ID为抢购业务中,用户操作行为的关键字段,值需要保证唯一。故此处可以采用发号器之类的能力。
我们构建的Transaction Data记录,就可以按照DailyRun的方式,与第三方对账,来fix两方数据库库存不一致等问题。
为什么会产生我们和资源方的库存不一致,可能是因为用户在第三方消费后,第三方callback我们时候失败造成,也可能是因为用户跳去第三方后并没有真正支付,但我们的商品库、抢购库的库存都已经减少造成的。原因可能有很多,对账机制是必要的。
最后,我们回顾回顾设计,压力问题在业务层解决了,库存不一致问题我们通过对账机制解决了,产品的需求我们也通过旁路可配解决了,嗯,可以喝杯茶,发起评审,评审通过后开始写代码了。
感谢大家。分享中的数据强一致那块,以及如何做取舍,都是很有意思的点,都可以展开聊很久,这里没展开,大家可以事后查查资料。
================================================
感谢 Coding 和 UPYUN 对本微信的支持。Coding.net 是一个面向开发者的云端开发平台,目前提供代码托管、运行空间、质量控制、项目管理等功能。
upyun.com是国内领先的云服务提供商,专注于提供静态文件的云存储、云处理和CDN加速服务。现在注册,即可免费体验!