文档参考:书名:《企业it架构转型之道》-钟华
前文如下:
大促秒杀活动催生缓存技术的高度使用
对内存的数据操作时间一般是纳秒级,而传统的数据库访问中,一次SSD盘数据访问在几十微秒,一次SATA盘数据访问在几十毫秒,显然处理时间有数量级的差异,所以通过缓存(大部分缓存产品均是基于内存的数据存取实现)让应用具备更好的处理性能和系统吞吐率早已经在应用开发领域广泛使用。淘宝的核心业务所用的分布式缓存产品是Tair,除了阿里巴巴自主研发的Tair缓存产品外,其他优秀的缓存平台如Redis也已经在阿里巴巴的某些业务中被采用。
本次介绍淘宝早期进行大促秒杀活动时,如何推动了架构演变和缓存的高度使用,以便让读者了解如何构建一个满足秒杀和大型促销活动场景的系统架构,进而对缓存在交易环节所发挥的作用有一个更加清晰的认识。
所谓的大促、秒杀场景,其关键词有“低廉价格”、“大幅推广”、“瞬时售空”,只有满足这三个关键词的场景才能称为大促、秒杀的场景。
“低廉价格”是形成大促秒杀的基础,只有足够有吸引力的低廉价格才能让买家产生购买的意愿,比如原价2200元的手机在秒杀活动中仅仅只要9元,就能极大刺激买家对该商品的购买意愿,从而达到吸引大量用户到时会来参加本次促销活动。
“大幅推广”指的是该促销活动被客户所知晓的辐射范围,淘宝早期在省市地方台上打广告和在中央电视台上打广告所带来的用户流量根本不在一个量级,也就是说,就算商品价格再低廉,如果没有那么多人知道这个活动,而产生大家蜂拥而至的效果,也产生不了大促秒杀的场景。
“瞬时售空”其实是前两个关键词在发挥了效应后一定会产生的一个现象,海量涌入的买家对商品进行下单操作,在极短时间内(通常1~2秒)该商品就进入售罄状态。对于秒杀或双11开始时限量的畅销商品,均是这种现象。
在一次大促秒杀活动很好地满足了以上三个关键词后,便引来了所预期的市场关注度,一个架构如何能平稳地支持这样的大促秒杀活动?不会因为大促活动出现秒杀商品的超卖、秒杀界面无法访问、甚至造成整个平台不可用的情况吧?这就需要在传统的架构基础上进一步优化和调整才能满足这些要求。
平台如何完美地支持大促秒杀场景是一个体系的工作,牵涉到应用架构设计的合理、平台的稳定性保障、极强的系统扩展能力等多个方面,下面重点阐述如何利用缓存技术实现商品数据的高性能读取,以满足秒杀活动中对于商品数据访问的同时不会出现商品超卖等严重的业务问题。
1.小库存商品秒杀典型架构
比如库存为10个,秒杀价格为1元的手机则是典型的小库存商品秒杀活动。在这种类型的秒杀活动中,因为商品会在极短的瞬间库存会降到0,所以只要处理好商品的库存的扣减,不要出现商品超卖的情况就能平稳地度过这次秒杀活动,如图为此类秒杀活动的典型架构示意图。
首先一定要让负责秒杀场景的商品中心应用实例(图中“秒杀IC”)与满足普通商品正常访问的商品中心应用实例(图中IC)隔离部署,通过服务分组方式,保持两个运行环境的隔离,避免因为秒杀产生的过大访问流量造成整个商品中心的服务实例均受影响,产生太大范围的影响。
因为秒杀在正式开始前,一定会有大量的用户停留在商品的详情页(图中Detail)等待着秒杀活动的开始,同时伴随有大量的页面刷新访问(心急或担心页面没有正常刷新的买家们),此时,如果每一次刷新都要从后端的商品数据库(图中ICDB)中获取商品相关信息,则一定会给数据库带来巨大的压力,在淘宝早期举办秒杀活动时就出现了秒杀活动还没开始,因为商品详情页访问太大,造成平台提前进入不可访问状态的情况。所以一定是通过缓存服务器(图中Tair),将商品的详细信息(包括库存信息)保存在缓存服务器上,商品详情页和购买页所有有关商品的信息均是通过缓存服务器获取,则无需访问后端数据库。
如图中“本地缓存”所示,可通过给网页资源设置Expires和Last-Modified返回头信息进行有效控制,从而尽可能减少对后端服务端的访问次数。
当用户进入到付款界面(图中Buy)进行成功付款操作后,则对商品数据库进行实际的商品库存修改,当商品的库存被修改后,会同时修改缓存中对应商品的库存信息,接下来用户在商品详情页和下单页面看到的就是更新后的库存信息。
商品定时上架。 在秒杀活动中,都是商品在某一个时间点才开始秒杀活动,比如一个晚上8点开始的秒杀活动,参加该活动的商品在晚上8点活动开始前,用户只能在商品界面上看到进入秒杀活动的倒计时或即将开始的提示,是看不到商品下单的操作按钮。
商品库存的乐观锁实现。 避免商品出现超卖(即成功下单的订单中商品的库存数量大于商品现有的库存量,则称为商品超卖)的问题,核心技术是利用数据库的事务锁机制,即不允许同一商品的库存记录在同一时间被不同的两个数据库事务修改。在前柔性事务介绍中所提到的,用户在进行商品下单操作中,会进行一系列的业务逻辑判断和操作,对于商品库存信息这一访问热点数据,如果采用数据库的悲观锁(比如select语句带for update)模式,则会给订单处理带来很大的性能阻塞,所以会采用乐观锁的方式实现商品库存的操作。实现的方式也比较简单,也就是在最后执行库存扣减操作时,将事务开始前获取的库存数量带入到SQL语句中与目前数据库记录中的库存数量进行判断,如果数量相等,则该条更新库存的语句成功执行;如果不相等,则表示该商品的库存信息在当前事务执行过程中已经被其他事务修改,则会放弃该条update的执行,可以采用重试的机制重新执行该事务,避免商品超卖的发生,具体的SQL语句示意如下:
其中#dbQuantity#为事务中在update语句执行前,通过select语句获取到的商品库存数量。 商品库存控制业务流。商品的库存在该类秒杀场景下,详细的库存控制业务流如下图所示。
图中描述了在小库存商品秒杀场景下,用户在通过商品详情页查看商品时,获取的商品基本信息以及库存均是从缓存Tair中获取,如步骤1.1。
当用户查看到当前商品还有可卖库存时,进入到Buy商品下单界面,此时商品的相关信息依然还是从缓存获取,如步骤2.1。
用户在进行下单操作后,此时就通过数据库本机事务操作的方式,通过商品中心的服务(IC)获取到当前商品真实的库存信息(步骤3.1),当此时获取的库存大于0时,则进行库存的扣减操作(步骤3.2),在通过步骤3.3更新了商品数据库中的库存信息后,同时也会更新缓存中该商品的库存信息(步骤3.4),则前方用户再访问该商品信息时,看到的就是已经更新后的库存信息。
在小库存商品的秒杀场景中,缓存平台提供了对商品相关信息的缓存服务,使得用户只有在最终的下单环节才需要对数据库进行访问操作,大大降低了数据库的访问频率,而且因为商品的库存少,秒杀活动转瞬间就结束了,所以采用这样的架构基本就能满足该类大促秒杀场景业务的要求。
2.大库存商品大促架构
如果参与大促的商品拥有较大库存数量的时候,比如5000件1元的洗衣液,因为洗衣液的库存记录只有一条记录,则在大量客户同时下单的过程中,按照上一流程中订单创建的逻辑,只有当前事务中碰巧在修改商品库存时该商品的库存信息相比事务开始时没有发生变化,才能进行库存的更新,否则可能采用重试的方式,这就会出现让用户长时间的订单创建等待的同时,还可能会出现后点击创建订单的客户反而会比前面已提交订单的客户提前成功下单,甚至造成先提交订单的客户可能因为商品售罄没有成功下单的情况。
为了解决这一类大促场景的需求,则就要进一步发挥缓存产品的威力,将之前仅仅作为商品信息浏览的缓存的作用,提升到为库存操作提供事务支持的角色。如图所示。
在秒杀活动开始前,直接将该商品的初始库存信息通过商品中心(图中的IC)加载到缓存服务器Tair上(步骤0.1)。
跟小库存商品秒杀场景相同,当用户访问商品详情页时,是从Tair缓存服务器上获取商品的库存信息(步骤1.1),在库存大于0的情况下,用户进入到下单界面(Buy),并点击“确认购买”按钮进行下单操作时(步骤3),后端处理的逻辑就跟小库存商品秒杀有了较大的区别。
在用户进行了下单操作后,程序首先会为该订单创建一个订单详单记录,只不过在库存成功扣减前,该订单的状态是用户不可见的。保存该订单的信息非常重要的作用是当缓存服务出现故障不可用时,可通过商品数据库中初始缓存的信息和数据库中订单详单信息能还原出订单处理的最新状态,而不至于出现因为缓存的故障造成业务数据的丢失。
在数据库中成功创建订单详单后,会发送一个库存修改的请求到消息服务器。因为一般的缓存平台还不支持MVCC或数据锁的功能,此时利用消息服务可让库存修改的请求线性处理,这里可利用上文提到的事务消息的方式,保证在订单详单创建成功后,发送消息到消息服务器,当消息的订阅程序从消息服务器获取到消息后,则对缓存中的库存数据进行更新处理。因为缓存数据更新时间在纳秒级,所以整体的库存处理性能相比传统数据库方式差别至少在百倍。当缓存中的库存数据成功更新后,则将之前创建的订单详情状态修改为成功下单状态,整个订单创建过程结束。
在这个方案中,实际上是将订单交易创建环节中对于原本商品数据库的库存信息操作替换为在缓存服务器中运行,充分展现了缓存服务相比于传统数据库在性能上的巨大优势。而且随着缓存技术的逐步完善和发展,业界各缓存平台的高可用性已经越来越高,从趋势来看,缓存技术将会在互联网应用场景中将扮演越来越重要的角色。