You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

153 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 03 | 复杂而又重要的购物车系统,应该如何设计?
你好,我是李玥。
今天这节课我们来说一下购物车系统的存储该如何设计。
首先,我们来看购物车系统的主要功能是什么。就是在用户选购商品时,下单之前,暂存用户想要购买的商品。购物车对数据可靠性要求不高,性能也没有特别的要求,在整个电商系统中,看起来是相对比较容易设计和实现的一个子系统。
购物车系统的功能,主要的就三个:把商品加入购物车(后文称“加购”)、购物车列表页、发起结算下单,再加上一个在所有界面都要显示的购物车小图标。
支撑购物车的这几个功能对应的存储模型应该怎么设计很简单只要一个“购物车”实体就够了。它的主要属性有什么你打开京东的购物车页面对着抄就设计出来了SKUID商品ID、数量、加购时间和勾选状态。
![](https://static001.geekbang.org/resource/image/ac/73/ac4dffc68c2aaf39a9f9d4003c50f773.png "备注:图片来源于网络,仅供本文介绍、评论及说明某问题,适当引用。")
这个“勾选状态”属性,就是在购物车界面中,每件商品前面的那个小对号,表示在结算下单时,是不是要包含这件商品。至于商品价格和总价、商品介绍等等这些信息,都可以实时从其他系统中获取,不需要购物车系统来保存。
购物车的功能虽然很简单,但是在设计购物车系统的存储时,仍然有一些特殊的问题需要考虑。
## 设计购物车存储时需要把握什么原则?
比如下面这几个问题:
1. 用户没登录,在浏览器中加购,关闭浏览器再打开,刚才加购的商品还在不在?
2. 用户没登录,在浏览器中加购,然后登录,刚才加购的商品还在不在?
3. 关闭浏览器再打开,上一步加购的商品在不在?
4. 再打开手机,用相同的用户登录,第二步加购的商品还在不在呢?
上面这几个问题是不是有点儿绕?没关系,我们先简单解释一下这四个问题:
1. 如果用户没登录,加购的商品也会被保存在用户的电脑里,这样即使关闭浏览器再打开,购物车的商品仍然存在。
2. 如果用户先加购,再登录,登录前加购的商品就会被自动合并到用户名下,所以登录后购物车中仍然有登录前加购的商品。
3. 关闭浏览器再打开,这时又变为未登录状态,但是之前未登录时加购的商品已经被合并到刚刚登录的用户名下了,所以购物车是空的。
4. 使用手机登录相同的用户看到的就是该用户的购物车这时无论你在手机App、电脑还是微信中登录只要是相同的用户看到是同一个购物车所以第二步加购的商品是存在的。
所以,上面这四个问题的答案依次是:存在、存在、不存在、存在。
如果你没有设计或者开发过购物车系统,你可能并不会想到购物车还有这么多弯弯绕。但是,作为一个开发者,如果你不仔细把这些问题考虑清楚,用户在使用购物车的时候,就会感觉你的购物车系统不好用,不是加购的商品莫名其妙地丢了,就是购物车莫名其妙地多出来一些商品。
要解决上面这些问题,其实只要在存储设计时,把握这几个原则就可以了:
1. 如果未登录,需要临时暂存购物车的商品;
2. 用户登录时,把暂存购物车的商品合并到用户购物车中,并且清除暂存购物车;
3. 用户登陆后购物车中的商品需要在浏览器、手机APP和微信等等这些终端中都保持同步。
实际上,购物车系统需要保存两类购物车,**一类是未登录情况下的“暂存购物车”,一类是登录后的“用户购物车”**。
## 如何设计“暂存购物车”的存储?
我们先来看下暂存购物车的存储该怎么实现。暂存购物车应该存在客户端还是存在服务端?
如果保存在服务端,那每个暂存购物车都需要有一个全局唯一的标识,这个标识并不太容易设计,并且,存在服务端还要浪费服务端的资源。所以,肯定是保存在客户端好,既可以节约服务器的存储资源,也没有购物车标识的问题,因为每个客户端就保存它自己唯一一个购物车就可以了,不需要标识。
客户端的存储可以选择的不太多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中。这个表的结构同样可以参照刚刚讲的实体模型来设计
![](https://static001.geekbang.org/resource/image/e8/cc/e8e7ae1638ec77c7bcc1ff949939b4cc.jpeg)
注意需要在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是一个用户IDFIELD里存放的是商品IDFIELD\_VALUE是一个JSON字符串保存加购时间、商品数量和勾选状态。
大家都知道从读写性能上来说Redis是比MySQL快非常多的那是不是用Redis就一定比用MySQL更好呢我们来比较一下使用MySQL和Redis两种存储的优劣势
1. 显然使用Redis性能要比MySQL高出至少一个量级响应时间更短可以支撑更多的并发请求“天下武功唯快不破”这一点Redis完胜。
2. MySQL的数据可靠性是要好于Redis的因为Redis是异步刷盘如果出现服务器掉电等异常情况Redis是有可能会丢数据的。但考虑到购物车里的数据对可靠性要求也没那么苛刻丢少量数据的后果也就是个别用户的购物车少了几件商品问题也不大。所以在购物车这个场景下Redis的数据可靠性不高这个缺点并不是不能接受的。
3. MySQL的另一个优势是它支持丰富的查询方式和事务机制这两个特性对我们今天讨论的这几个购物车核心功能没什么用。但是每一个电商系统都有它个性化的需求如果需要以其他方式访问购物车的数据比如说统计一下今天加购的商品总数这个时候使用MySQL存储数据就很容易实现而使用Redis存储查询起来就非常麻烦而且低效。
综合比较下来考虑到需求总是不断变化还是更推荐你使用MySQL来存储购物车数据。如果追求性能或者高并发也可以选择使用Redis。
你可以感受到,我们设计存储架构的过程就是一个不断做选择题的过程。很多情况下,可供选择的方案不止一套,选择的时候需要考虑实现复杂度、性能、系统可用性、数据可靠性、可扩展性等等非常多的条件。需要强调的是,**这些条件每一个都不是绝对不可以牺牲的,不要让一些“所谓的常识”禁锢了你的思维。**
比如一般我们都认为数据是绝对不可以丢的也就是说不能牺牲数据可靠性。但是像刚刚讲到的用户购物车的存储使用Redis替代MySQL就是牺牲了数据可靠性换取高性能。我们仔细分析后得出很低概率的情况下丢失少量数据是可以接受的。性能提升带来的收益远大于丢失少量数据而付出的代价这个选择就是划算的。
如果说不考虑需求变化这个因素牺牲一点点数据可靠性换取大幅性能提升选择Redis才是最优解。
## 小结
今天我们讲了购物车系统的存储该如何设计。
购物车系统的主要功能包括加购、购物车列表页和结算下单。核心的实体就只有一个“购物车”实体它至少要包括SKUID、数量、加购时间和勾选状态这几个属性。
在给购物车设计存储时,为了确保购物车内的数据在多端保持一致,以及用户登录前后购物车内商品能无缝衔接,除了每个用户的“用户购物车”之外还要实现一个“暂存购物车”保存用户未登录时加购的商品,并在用户登录后自动合并“暂存购物车”和“用户购物车”。
暂存购物车存储在客户端浏览器或者App中可以选择存放到Cookie或者LocalStorage中。用户购物车保存在服务端可以选择使用Redis或者是MySQL存储使用Redis存储会有更高的性能可以支撑更多的并发请求使用MySQL是更常规通用的方式便于应对变化系统的扩展性更好。
## 思考题
课后请你思考一下既然用户的购物车数据存放在MySQL或者是Redis中各有优劣势。那能不能把购物车数据存在MySQL中并且用Redis来做缓存呢这样不就可以兼顾两者的优势了么这样做是不是可行如果可行如何来保证Redis中的数据和MySQL中的数据是一样的呢
欢迎你在留言区与我讨论,如果你觉得今天学到的知识对你有帮助,也欢迎把它分享给你的朋友。