导读:本文将探讨如何设计一个快速、可靠的商品系统存储架构。
作者:李玥
来源:大数据DT(ID:hzdashuju)
电商的商品系统所包含的主要功能就是增、删、改、查商品信息,业务逻辑比较简单,支撑的主要页面就是商品详情页。尽管如此,在设计商品系统的存储架构时,仍然需要着重考虑如下两个方面的问题。
第一,需要考虑高并发的问题。不管是哪种电商系统,商品详情页一定是整个系统中DAU(Daily Active User,日均访问人数)最高的页面之一。
商品详情页DAU高的原因与用户使用电商App的习惯息息相关,绝大部分用户浏览完商品详情页之后不一定会购买,但购买之前一定会浏览很多同类商品的详情页,正所谓“货比三家”。
所以商品详情页的浏览次数要远高于系统的其他页面。如果在设计商品系统的存储架构时,没有考虑到高并发的问题,那么在电商系统举办大促活动的时候,海量的浏览请求会在促销开启的那一刻同时涌向我们的系统,支撑商品详情页的商品系统必然是第一个被流量冲垮的系统。
第二,需要考虑商品数据规模的问题。商品详情页的数据规模,可以总结为如下六个字:数量多,体量大。
为什么说“数量多”?在国内一线的电商平台中,SKU(Stock Keeping Unit,库存单元,在电商行业也可以直接将其理解为“商品”)的数量大约在几亿到几十亿这个量级。当然,实际上并没有这么多种。
商品数量级这么大的原因有很多,比如,同一个商品通常会有数种不同的版本型号,再比如,商家为了促销需要,可能会反复上下架同一个商品,或者为同一个商品加上不同的“马甲”,这些原因都导致了SKU数量巨大。
为什么说“体量大”?我们可以打开一个商品详情页看一下,从上一直拉到底,看看页面有多长?一般来说都在10个屏幕高度左右,并且这其中不仅包含了大量的文字,还会包含大量的图片和视频,甚至还包含了AR/VR的玩法。所以说,每个商品详情页都是一个“大胖子”。
商品系统的存储架构,需要保存这么多的“大胖子”,还要满足高并发的需求,任务非常艰巨。
01 商品系统需要保存哪些数据
本节就来讨论商品详情页需要保存哪些信息,下面将商品详情页里的所有信息都总结在了图3-1所示的思维导图中。
▲图3-1 商品详情页所含信息思维导图
在图3-1中,右边灰色部分所列举的信息,均来自电商平台的其他系统,我们暂且不讨论;左边黑色部分所列举的信息,都是商品系统需要存储的内容。
那么,应该如何存储这么多内容呢?能不能像保存订单数据那样,设计一张商品表,把这些数据全部存放进去?或者说,一张表存不下就再加几张子表,这样存储行不行?其实并不是不可以,现今的一线电商企业,在发展的早期阶段采用的就是这种存储结构。而现今它们所采用的复杂的分布式存储架构,都是在发展的过程中逐步演进而来的。
用数据库表存储的好处就是“糙、快、猛”,简单、可靠而且容易实现,但是缺点是,表能支撑的数据量有限,以及无法满足高并发的需求。如果只是低成本且快速构建一个小规模的电商,这可能会是相对比较合理的选择。
当然,规模一旦变大,就不能再采用数据库表存储这种简单粗暴的方案了。如果不能用数据库,那么我们应该选择哪种存储系统来保存这么复杂的商品数据呢?
在目前的情况下,任何一种存储方案都无法完全满足需求,最好的解决方案是分而治之,即把商品系统需要存储的数据,按照特点分成商品基本信息、商品参数、图片视频和商品介绍几个部分,分别进行存储。
02 如何存储商品的基本信息
首先,我们分析一下商品的基本信息,其中主要包括商品的主副标题、价格、颜色等一些最基本、最主要的属性。这些属性都是固定的,不太可能会因为需求改变或不同的商品而变化。而且这部分数据不会太大,所以,建议在数据库中建一张表来保存商品的基本信息。
然后,我们还需要在数据库前面加一个缓存,以帮助数据库抵挡绝大部分的读请求。可以使用Redis或Memcached实现缓存,这两种存储系统都是基于内存的KV(Key-Value)存储,都能很好地解决问题。
接下来我们简单说一下,如何使用前置缓存来缓存商品数据。
处理商品信息的读请求时,需要先到缓存中查找,如果找到对应的商品信息,就直接返回缓存中的数据。如果在缓存中没找到,就去数据库中查找,然后把从数据库中查到的商品信息返回给页面,同时把数据存放在缓存中。
更新商品信息的时候,在更新数据库的同时,缓存中的相关数据也要一并删除。否则就有可能会出现如下这种情况:数据库中的数据发生了改变,而缓存中的数据没有变,商品详情页上看到的还是旧数据。
这种缓存更新的策略,称为Cache Aside,是一种最简单实用的缓存更新策略,适用范围也最广。如果想要缓存数据,若无特殊情况,则应该首先考虑使用Cache Aside策略。除了Cache Aside以外,还有Read/Write Through、Write Behind等几种策略,分别适用于不同的情况,后面的章节会有专门的讲解。
设计商品基本信息表的时候,需要特别注意的一点是,一定要记得保留商品数据的每一个历史版本。因为商品数据是随时变化的,但是订单中关联的商品数据,必须是下单那个时刻的商品数据,这一点很重要。解决方案是,为每个历史版本的商品数据保存一个快照,可以创建一个历史表保存到MySQL中,也可以保存到一些KV存储中。
03 使用MongoDB保存商品参数
本节就来分析如何保存商品参数,商品参数就是商品的特征,比如,电脑的内存大小、手机的屏幕尺寸、酒的度数、口红的色号,等等。与商品的基本属性一样,参数也是结构化的数据。关于参数,需要解决的一个难题是,不同类型的商品,其参数是完全不一样的。
如果要设计一个商品参数表,那么这个表所要包含的字段就太多了,并且每增加一个品类的商品,这个表就要加入新的字段,所以这个方案行不通。
既然一个表不能解决问题,那就每个类别分别建一张表。比如,建一个电脑参数表,其中包含的字段有CPU型号、内存大小、显卡型号、硬盘大小,等等;再建一个酒类参数表,其中包含的字段有酒精度数、香型、产地,等等。
如果品类比较少(在100个以内),那么用几十张表分别保存不同品类商品参数的做法也是可以的。但是这并不是一个很好的解决方法,那么还有没有更好的方法呢?
大多数数据库,都要求数据表要有一个固定的结构,但有一种数据库没有这个要求,特别适合用于保存像“商品参数”这种属性不固定的数据,这个数据库就是MongoDB。
MongoDB是一个面向文档存储的NoSQL数据库,在MongoDB中,表、行、列对应的概念分别是collection、document、field,这些概念总体上可以一一对应,但会有一些细微的差别。为了便于理解,我们不必拘泥于具体的文字表达,下文还是用“表、行、列”来说明。
MongoDB最大的特点是,它的“表结构”是不需要事先定义的。其实,在MongoDB中根本就没有表结构。由于没有表结构,因此MongoDB可以把任意数据都放在同一张表里,甚至还可以在一张表里保存商品数据、订单数据、物流信息这些结构完全不同的数据。除此之外,MongoDB还支持按照数据的某个字段进行查询。
它是怎么做到的呢?MongoDB中的每一行数据,只是简单地把数据转化成BSON格式后存在存储层中,BSON就是一种更紧凑的JSON。所以,即使是在同一张表中,每一行数据的结构也可以是不一样的。当然,这种灵活性也是需要付出代价的,MongoDB不支持SQL、多表联查,且对复杂事务的处理能力比较弱,不太适合用来存储一般的数据。
不过,MongoDB可以很好地满足商品参数信息数据量大、数据结构不统一等特性,而且我们也不需要对商品参数进行事务和多表联查,因此MongoDB简直就像是为了保存商品参数量身定制的数据库一样。
04 使用对象存储保存图片和视频
图片和视频由于所占用的存储空间比较大,因此一般的存储方式是,在数据库中只保存图片和视频的ID或URL,实际的图片和视频则以文件的方式单独存储。
现今,图片和视频的存储技术已经非常成熟了,首选的方式是保存在对象存储(Object Storage)中。各大云厂商都提供了对象存储服务,比如,国内的七牛云、AWS的S3,等等,除此之外,还有开源的对象存储产品,比如,MinIO可以私有化部署。虽然每个产品的API各不相同,但功能大同小异。
对象存储可以简单理解为一个无限容量的大文件KV存储,它的存储单位是对象,其实就是文件,可以是一张图片、一个视频,也可以是其他任何文件。每个对象都有一个唯一的键(key),通过这个键,我们可以随时访问对应的对象。对象存储的基本功能包括写入、访问和删除对象,大部分对象存储不支持修改对象的内容。
云服务厂商的对象存储大多提供了客户端API,可以在Web页面或App中直接访问,而不用通过后端服务来中转。这样,App和Web页面在上传图片和视频的时候,可以直接保存到对象存储中,然后把对应的键保存在商品系统中就可以了。
访问图片和视频的时候,真正的图片和视频文件,也不需要经过商品系统的后端服务进行读取,而是在Web页面上通过对象存储提供的URL直接访问,这种方式既省时省力又节约带宽。而且几乎所有的对象存储云服务都自带CDN(Content Delivery Network,内容分发网络)加速服务,响应时间比直接请求业务的服务器更短。
国内很多云厂商提供的对象存储,对图片和视频都进行了大量有针对性的优化。其中最有用的是缩放图片和视频转码,只需要把图片和视频存放到对象存储中,就可以随时获得任意尺寸大小的图片,视频也会自动转码成各种格式和码率的版本,适配各种App和场景,使用体验非常好。
05 将商品介绍静态化
商品介绍在商品详情页中所占的比重是最大的,其中包含了大量的带格式文字、图片和视频。图片和视频自然要存放在对象存储中,而关于商品介绍的文本,则一般是随着商品详情页一起静态化,保存在HTML文件中。
什么是静态化呢?静态化是相对于动态页面来说的。一般来说,部署到Tomcat中的Web系统,返回的都是动态页面,即服务端程序在处理Web请求时动态生成的页面。
比如,用户在App或Web页面打开一个商品详情页时,一个带着相应的SKUID参数的HTTP请求将被发送到后端的Web服务中,也就是Tomcat中的商品详情页模块。然后这个Web服务将访问各种数据库、调用其他微服务获取数据,将该商品详情页中的数据动态拼在一起,返回给浏览器。
不过,现在基本上已经没有系统再采用上述这种方式了。因为对于每个SKU的商品详情页,每次动态生成的页面内容都是完全一样的,而且还会多次生成,上述方式不仅浪费服务器资源,而且速度还慢。更关键的问题是,Tomcat能支撑的并发量,与Nginx完全不是一个数量级的。
由于商品详情页的绝大部分内容都是商品介绍,这部分内容基本上是不会频繁改变的,因此一个比较好的解决方案是事先就生成好页面的内容,将其保存成一个静态的HTML文件,访问商品详情页的时候,直接返回该HTML文件即可。这就是静态化。
商品详情页静态化之后,不仅可以节省服务器资源,还可以利用CDN加速,把商品详情页放到离用户最近的CDN服务器上,让商品详情页的访问变得更快。
至于商品价格、促销信息等这些需要频繁变动的信息,由于不能将其静态化到页面中,因此可以在前端页面,使用AJAX请求商品系统动态获取。这样就既兼顾了静态化带来的优势,也能解决商品价格等信息需要实时更新的问题。
06 小结
本文主要介绍了商品系统分而治之的存储架构。商品系统的存储需要保存商品的基本信息、商品参数、图片和视频,以及商品介绍等数据。商品的基本信息和商品参数分别保存在MySQL和MongoDB中,用Redis作为前置缓存,图片和视频存放在对象存储中,商品介绍则随着商品详情页一起静态化到HTML文件中。
总结下来,商品系统的存储结构可以用图3-2来描述。
▲图3-2 商品系统的存储结构
这样一个商品系统的存储结构,其最终的效果是怎样的?图3-2中的实线表示每访问一次商品详情页,需要真正传输的数据;虚线表示当商品详情页的数据发生变化时,才需要进行传输的数据。
用户打开一个SKU的商品详情页时,首先会去CDN获取商品详情页的HTML文件,然后访问商品系统获取价格等会频繁发生变化的信息,这些信息可以从Redis缓存中获取。图片和视频信息,则是从对象存储的CDN中获取。
下面就来分析一下效果。数据量最大的图片、视频和商品介绍都是从离用户最近的CDN服务器上获取的,速度快,且节约带宽。真正需要触达商品系统的请求,只是价格等需要动态获取的商品信息,一般情况下做一次Redis查询就可以了,基本上不会有请求到达MySQL中。
综上所述,商品系统的存储架构把大部分请求都转移到了既便宜速度又快的CDN服务器上,因此这种架构方式可以用少量的服务器和带宽资源支撑起大量的并发请求。
关于作者:李玥,美团基础技术部高级技术专家,极客时间《后端存储实战课》《消息队列高手课》等专栏作者。曾在浪潮集团、当当网、京东零售等公司任职。从事互联网电商行业基础架构领域的架构设计和研发工作多年,曾多次参与双十一和618电商大促。专注于分布式存储、云原生架构下的服务治理、分布式消息和实时计算等技术领域,致力于推进基础架构技术的创新与开源。
本文摘编自《电商存储系统实战:架构设计与海量数据处理》,经出版方授权发布。(ISBN:978-7-111-69741-1)