你好,我是李玥。
今天这节课我们来说一下购物车系统的存储该如何设计。
首先,我们来看购物车系统的主要功能是什么。就是在用户选购商品时,下单之前,暂存用户想要购买的商品。购物车对数据可靠性要求不高,性能也没有特别的要求,在整个电商系统中,看起来是相对比较容易设计和实现的一个子系统。
购物车系统的功能,主要的就三个:把商品加入购物车(后文称“加购”)、购物车列表页、发起结算下单,再加上一个在所有界面都要显示的购物车小图标。
支撑购物车的这几个功能,对应的存储模型应该怎么设计?很简单,只要一个“购物车”实体就够了。它的主要属性有什么?你打开京东的购物车页面,对着抄就设计出来了:SKUID(商品ID)、数量、加购时间和勾选状态。
这个“勾选状态”属性,就是在购物车界面中,每件商品前面的那个小对号,表示在结算下单时,是不是要包含这件商品。至于商品价格和总价、商品介绍等等这些信息,都可以实时从其他系统中获取,不需要购物车系统来保存。
购物车的功能虽然很简单,但是在设计购物车系统的存储时,仍然有一些特殊的问题需要考虑。
比如下面这几个问题:
上面这几个问题是不是有点儿绕?没关系,我们先简单解释一下这四个问题:
所以,上面这四个问题的答案依次是:存在、存在、不存在、存在。
如果你没有设计或者开发过购物车系统,你可能并不会想到购物车还有这么多弯弯绕。但是,作为一个开发者,如果你不仔细把这些问题考虑清楚,用户在使用购物车的时候,就会感觉你的购物车系统不好用,不是加购的商品莫名其妙地丢了,就是购物车莫名其妙地多出来一些商品。
要解决上面这些问题,其实只要在存储设计时,把握这几个原则就可以了:
实际上,购物车系统需要保存两类购物车,一类是未登录情况下的“暂存购物车”,一类是登录后的“用户购物车”。
我们先来看下暂存购物车的存储该怎么实现。暂存购物车应该存在客户端还是存在服务端?
如果保存在服务端,那每个暂存购物车都需要有一个全局唯一的标识,这个标识并不太容易设计,并且,存在服务端还要浪费服务端的资源。所以,肯定是保存在客户端好,既可以节约服务器的存储资源,也没有购物车标识的问题,因为每个客户端就保存它自己唯一一个购物车就可以了,不需要标识。
客户端的存储可以选择的不太多:Session、Cookie和LocalStorage,其中浏览器的LocalStorage和App的本地存储是类似的,我们都以LocalStorage来代表。
存在哪儿最合适?SESSION是不太合适的,原因是,SESSION的保留时间短,而且SESSION的数据实际上还是保存在服务端的。剩余的两种存储,Cookie和LocalStorage都可以用来保存购物车数据,选择哪种方式更好呢?各有优劣。
在我们这个场景中,使用Cookie和LocalStorage最关键的区别是,客户端和服务端的每次交互,都会自动带着Cookie数据往返,这样服务端可以读写客户端Cookie中的数据,而LocalStorage里的数据,只能由客户端来访问。
使用Cookie存储,实现起来比较简单,加减购物车、合并购物车的过程中,由于服务端可以读写Cookie,这样全部逻辑都可以在服务端实现,并且客户端和服务端请求的次数也相对少一些。
使用LocalStorage存储,实现相对就复杂一点儿,客户端和服务端都要实现一些业务逻辑,但LocalStorage的好处是,它的存储容量比Cookie的4KB上限要大得多,而且不用像Cookie那样,无论用不用,每次请求都要带着,可以节省带宽。
所以,选择Cookie或者是LocalStorage来存储暂存购物车都是没问题的,你可以根据它俩各自的优劣势来选择。比如你设计的是个小型电商,那用Cookie存储实现起来更简单。再比如,你的电商是面那种批发的行业用户,用户需要加购大量的商品,那Cookie可能容量不够用,选择LocalStorage就更合适。
不管选择哪种存储,暂存购物车保存的数据格式都是一样的,参照我们实体模型来设计就可以,我们可以直接用JSON表示:
{
"cart": [
{
"SKUID": 8888,
"timestamp": 1578721136,
"count": 1,
"selected": true
},
{
"SKUID": 6666,
"timestamp": 1578721138,
"count": 2,
"selected": false
}
]
}
接下来,我们再来看下用户购物车的存储该怎么实现。因为用户购物车必须要保证多端的数据同步,所以数据必须保存在服务端。常规的思路是,设计一张购物车表,把数据存在MySQL中。这个表的结构同样可以参照刚刚讲的实体模型来设计:
注意,需要在user_id上建一个索引,因为查询购物车表时,都是以user_id作为查询条件来查询的。
你也可以选择更快的Redis来保存购物车数据,以用户ID作为Key,用一个Redis的HASH作为Value来保存购物车中的商品。比如:
{
"KEY": 6666,
"VALUE": [
{
"FIELD": 8888,
"FIELD_VALUE": {
"timestamp": 1578721136,
"count": 1,
"selected": true
}
},
{
"FIELD": 6666,
"FIELD_VALUE": {
"timestamp": 1578721138,
"count": 2,
"selected": false
}
}
]
}
这里为了便于你理解,我们用JSON来表示Redis中HASH的数据结构,其中KEY中的值6666是一个用户ID,FIELD里存放的是商品ID,FIELD_VALUE是一个JSON字符串,保存加购时间、商品数量和勾选状态。
大家都知道,从读写性能上来说,Redis是比MySQL快非常多的,那是不是用Redis就一定比用MySQL更好呢?我们来比较一下使用MySQL和Redis两种存储的优劣势:
综合比较下来,考虑到需求总是不断变化,还是更推荐你使用MySQL来存储购物车数据。如果追求性能或者高并发,也可以选择使用Redis。
你可以感受到,我们设计存储架构的过程就是一个不断做选择题的过程。很多情况下,可供选择的方案不止一套,选择的时候需要考虑实现复杂度、性能、系统可用性、数据可靠性、可扩展性等等非常多的条件。需要强调的是,这些条件每一个都不是绝对不可以牺牲的,不要让一些“所谓的常识”禁锢了你的思维。
比如,一般我们都认为数据是绝对不可以丢的,也就是说不能牺牲数据可靠性。但是,像刚刚讲到的用户购物车的存储,使用Redis替代MySQL,就是牺牲了数据可靠性换取高性能。我们仔细分析后得出,很低概率的情况下丢失少量数据,是可以接受的。性能提升带来的收益远大于丢失少量数据而付出的代价,这个选择就是划算的。
如果说不考虑需求变化这个因素,牺牲一点点数据可靠性,换取大幅性能提升,选择Redis才是最优解。
今天我们讲了购物车系统的存储该如何设计。
购物车系统的主要功能包括:加购、购物车列表页和结算下单。核心的实体就只有一个“购物车”实体,它至少要包括:SKUID、数量、加购时间和勾选状态这几个属性。
在给购物车设计存储时,为了确保购物车内的数据在多端保持一致,以及用户登录前后购物车内商品能无缝衔接,除了每个用户的“用户购物车”之外还要实现一个“暂存购物车”保存用户未登录时加购的商品,并在用户登录后自动合并“暂存购物车”和“用户购物车”。
暂存购物车存储在客户端浏览器或者App中,可以选择存放到Cookie或者LocalStorage中。用户购物车保存在服务端,可以选择使用Redis或者是MySQL存储,使用Redis存储会有更高的性能,可以支撑更多的并发请求,使用MySQL是更常规通用的方式,便于应对变化,系统的扩展性更好。
课后请你思考一下,既然用户的购物车数据存放在MySQL或者是Redis中各有优劣势。那能不能把购物车数据存在MySQL中,并且用Redis来做缓存呢?这样不就可以兼顾两者的优势了么?这样做是不是可行?如果可行,如何来保证Redis中的数据和MySQL中的数据是一样的呢?
欢迎你在留言区与我讨论,如果你觉得今天学到的知识对你有帮助,也欢迎把它分享给你的朋友。
评论