跳转至

03 复杂而又重要的购物车系统,应该如何设计?

你好,我是李玥。

今天这节课我们来说一下购物车系统的存储该如何设计。

首先,我们来看购物车系统的主要功能是什么。就是在用户选购商品时,下单之前,暂存用户想要购买的商品。购物车对数据可靠性要求不高,性能也没有特别的要求,在整个电商系统中,看起来是相对比较容易设计和实现的一个子系统。

购物车系统的功能,主要的就三个:把商品加入购物车(后文称“加购”)、购物车列表页、发起结算下单,再加上一个在所有界面都要显示的购物车小图标。

支撑购物车的这几个功能,对应的存储模型应该怎么设计?很简单,只要一个“购物车”实体就够了。它的主要属性有什么?你打开京东的购物车页面,对着抄就设计出来了:SKUID(商品ID)、数量、加购时间和勾选状态。

这个“勾选状态”属性,就是在购物车界面中,每件商品前面的那个小对号,表示在结算下单时,是不是要包含这件商品。至于商品价格和总价、商品介绍等等这些信息,都可以实时从其他系统中获取,不需要购物车系统来保存。

购物车的功能虽然很简单,但是在设计购物车系统的存储时,仍然有一些特殊的问题需要考虑。

设计购物车存储时需要把握什么原则?

比如下面这几个问题:

  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中。这个表的结构同样可以参照刚刚讲的实体模型来设计:

注意,需要在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两种存储的优劣势:

  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中的数据是一样的呢?

欢迎你在留言区与我讨论,如果你觉得今天学到的知识对你有帮助,也欢迎把它分享给你的朋友。

精选留言(15)
  • 李玥 👍(195) 💬(24)

    hi,我是李玥。 上节课我给你留了一道思考题,是这样的。如果说,用户下单这个时刻,正好赶上商品调价,就有可能出现这样的情况:我明明在商详页看到的价格是10块钱,下单后,怎么变成15块了?你的系统是不是偷偷在坑我?给用户的体验非常不好。你不要以为这是一个小概率事件,当你的系统用户足够多的时候,每时每刻都有人在下单,这几乎是个必然出现的事件。该怎么来解决这个问题? 关于这个问题,我是这样看的。 首先,商品系统需要保存包含价格的商品基本信息的历史数据,对每一次变更记录一个自增的版本号。在下单的请求中,不仅要带上SKUID,还要带上版本号。订单服务以请求中的商品版本对应的价格来创建订单,就可以避免“下单时突然变价”的问题了。 但是,这样改正之后会产生一个很严重的系统漏洞:黑客有可能会利用这个机制,以最便宜的历史价格来下单。所以,我们在下单之前需要增加一个检测逻辑:请求中的版本号只能是当前版本或者上一个版本,并且使用上一个版本要有一个时间限制,比如说调价5秒之后,就不再接受上一个版本的请求。这样就可以避免这个调价漏洞了。

    2020-03-03

  • aoe 👍(9) 💬(2)

    思考题是可行的,但是复杂,例如需要考虑: 1. Redis的容量可能远小于数据库容量,需要缓存策略缓存数据 2. 要处理老师提到的一致性问题 3. 性价比 另外请教老师一个问题: 在电商系统中,订单的“商品快照”(商品名称、数量、详情页所有信息)一般是怎么存储的? 例如:有订单、商品2个子系统,订单的商品快照一般是由哪个系统生成和保存?

    2020-03-03

  • 大秦皇朝 👍(4) 💬(5)

    1、京东的浏览器端,为什么每次加完购物车都要跳转到一个中转界面上呢?这点我疑惑了很多年,从软件设计的逆向思维来考虑,我也想不出所以然来,还请李玥老师是否能给解答下呢?因为按照我们的日常使用习惯(如:阿里,京豆手机端等),都是点击加购物车,直接购物车数量加1提示就好了呀?为什么要多此一举影响用户体验呢? 2、李玥老师文稿中提到,用户的购物车偶发情况下丢失一些数据可以接受,但是站在消费者的角度来说,我感觉也没必要纠结是不是少了几样东西。但是会直接影响我对这个平台的感知度,我会认为这个平台能力不行,不注重用户体验等,会给产品(京东)带来很多负面影响呀,所以牺牲的可靠性的这个比例的平衡点是一定要重视的。 思考题:综上第2点所述,我觉得业务让必然需要这个方案可行把?写数据的时候MySQL写和Redis同时删,读的时候从Redis中读;如果没有读到再从MySQL中读取,同时写到Redis中。(现学现卖不知道对不对) 但是也有个问题,多端同时操作,或者网络不好的时候,么保证数据的准确性呢?完了完了,我又跳回第一讲了。。。

    2020-03-04

  • 肥low 👍(3) 💬(2)

    我觉得完全可行 而且有时候比如MySQL主从架构下是有数据延迟更新问题的 用Redis我可以尽量避免这一点 不过有对用户加购的维护成本

    2020-03-03

  • 👍(2) 💬(5)

    “用户没登录,在浏览器中加购,然后登录,刚才加购的商品还在不在?” 关于这一点,怎么判断没登录加车的用户 和 登录的用户是同一个呢?比如我没登录在购物车加了一堆东西,然后我朋友用我的电脑登录他的账户,这该如何解决

    2020-04-05

  • 王佳山 👍(0) 💬(1)

    老师,这个暂存购物车怎么做还是没太明白! 比如用cookie,当用户没登录,第一次添加购物车的时候,服务端给cookie中添加标识吗,之后这个标识就代表一个未登录用户吗?还是前端给cookie添加标识,或者怎么样? 如果未登录用户加购之后,再也不访问网站,这个暂存购物车就会保存脏数据,是不是还要对数据定时检查维护?删除时间过长的数据,或者暂存就用redis做,设置有效期

    2020-06-08

  • 张学磊 👍(0) 💬(3)

    如果使用redis做购物车存储该如何设置商品过期时间呢?

    2020-05-17

  • kamida 👍(0) 💬(4)

    老师 文中的购物车表一行只能存一种商品吧 但是一个购物车id应该可以有多种商品 请问这个该怎么解决呢

    2020-03-23

  • 京京beaver 👍(72) 💬(7)

    购物车一般建议放到MySQL中。一般电商购物车是不占库存的,但是某些特卖电商购物车是占库存的。在这种情况下,数据是不允许丢失的,不然客户体验会非常差。Redis做缓存没啥用,因为每个用户只访问自己的购物车,每次访问网站也不会打开很多次购物车,缓存数据的命中率太低,没有意义。

    2020-03-03

  • 黄海峰 👍(36) 💬(6)

    感觉购物车是写多于读,也就是经常变,用cache aside的方式保持一致性的话就经常删缓存,db压力减轻不了多少,还要多写一次缓存,没什么必要

    2020-03-03

  • 公号-技术夜未眠 👍(27) 💬(0)

    读多写少用缓存,写多读少用MQ。对于前者,前提是读场景频繁且能具备较高的命中率。用户购物车数据不符合该场景。

    2020-03-03

  • Jxin 👍(21) 💬(2)

    1.能当然能。 2.能兼备mysql的可靠性和redis的读取性能。 3.这样做不可行,因为购物车写多读少,这样玩会频繁失效缓存,进而导致大部分读都要击穿到db并多做一步缓存的操作。实则弊大于利。 4.一旦修改购物车,redis的缓存直接失效。 个人见解: 1.感觉 未登录添加购物车。这功能可以弃了。填购必须注册登陆。 2.当下用户已经很习惯网购,对于注册登陆也是顺手之事,失去这个功能对用户体验的降低实属有限。 3.以往一个电商平台核心可能是推广加销售商品。但现在多了一个用户管理,而拉新成本高的当下,失去这一功能,变成强制注册不见得就是坏事,值得一试。 总结:加上未登陆添加购物车的功能。在当下,可能是牺牲部份可能发展成活跃用户的流动用户,来换取微末的用户体验。得不偿失。

    2020-03-04

  • 传志 👍(17) 💬(1)

    购物车,同时使用redis+mysql觉得可行。以redis为主,增,查询,删除都走redis.添加加时使用mq保证最终一致性。统计等需求可以在mysql中做

    2020-03-03

  • 业余爱好者 👍(9) 💬(1)

    今日得到:以前对浏览器存储的认识只停留在cookie上。以前认为只有服务端才有session数据。以前虽然听过localstorage这个词,但是思想上没有重视。刚查了下资料,浏览器存储还有indexeddb。学习还是要系统。 存储的本质是把数据暂时或永久保存下来,单从持久化这个目标来看,什么方式存储都是为所谓的。不过,存储的作用实际是为了将来的计算。既然从持久化角度没有差别,那当然要选择一种方便我们后续处理的存储设计了。所以对存储产品的选择与数据结构的设计需要结合具体的业务需求。存储的设计要考虑存储时间(临时或永久),可靠性要求,关键业务操作。需要梳理业务需求。可见需求分析是多么重要。 思考题:购物车从功能上说,就是一个临时存储的信息。为了准求性能,所以一般没必要存db,除了一些有统计业务功能的系统。购物车购物车信息的特性是一个树性的结构,使用关系模型不好设计,需要多表join。不过可以设计成json字符串类型,或者使用新的json类型。购物车写多读少,需要频繁刷db保证数据同步,为了追求一点点没多大用处的可靠性,得不偿失。

    2020-03-03

  • 睡浴缸的人 👍(7) 💬(0)

    感受到了这种技术选择的纠结与权衡的魅力,很喜欢老师的这种讲述方式

    2020-04-08