37讲电商系统表设计优化案例分析
你好,我是刘超。今天我将带你⼀起了解下电商系统中的表设计优化。
如果在业务架构设计初期,表结构没有设计好,那么后期随着业务以及数据量的增多,系统就很容易出现瓶颈。如果表结构扩展性差,业务耦合度将会越来越⾼,系统的复杂度也将随之增加。这⼀讲我将以电商系统中的表结构设计为例,为你详讲解在设计表时,我们都需要考虑哪些因素,⼜是如何通过表设计来优化系统性能。
核⼼业务
要懂得⼀个电商系统的表结构设计,我们必须先得熟悉⼀个电商系统中都有哪些基本核⼼业务。这部分的内容,只要你有过⽹购经历,就很好理解。
⼀般电商系统分为平台型和⾃营型电商系统。平台型电商系统是指有第三⽅商家⼊驻的电商平台,第三⽅商家⾃⼰开设店铺来维护商品信息、库存信息、促销活动、客服售后等,典型的代表有淘宝、天猫等。⽽⾃营型电商系统则是指没有第三⽅商家⼊驻,⽽是公司⾃⼰运营的电商平台,常⻅的有京东⾃营、苹果商城等。
两种类型的电商系统⽐较明显的区别是卖家是C端还是B端,很显然,平台型电商系统的复杂度要远远⾼于⾃营型电商系统。为了更容易理解商城的业务,我们将基于⾃营型电商系统来讨论表结构设计优化,这⾥以苹果商城为例。
⼀个电商系统的核⼼业务肯定就是销售商品了,围绕销售商品,我们可以将核⼼业务分为以下⼏个主要模块:
商品模块
商品模块主要包括商品分类以及商品信息管理,商品分类则是我们常⻅的⼤分类了,有⼈喜欢将分类细化为多个层级,例如, 第⼀个⼤类是⼿机、电视、配件等,配件的第⼆个⼤类⼜分为⽿机、充电宝等。为了降低⽤户学习系统操作的成本,我们应该尽量将层级减少。
当我们通过了分类查询之后,就到了商品⻚⾯,⼀个商品Item包含了若⼲商品SKU。商品Item是指⼀种商品,例如IPhone9, 就是⼀个Item,商品SKU则是指具体属性的商品,例如⾦⾊128G内存的IPhone9。
购物⻋模块
购物⻋主要是⽤于⽤户临时存放欲购买的商品,并可以在购物⻋中统⼀下单结算。购物⻋⼀般分为离线购物⻋和在线购物⻋。离线购物⻋则是⽤户选择放⼊到购物⻋的商品只保存在本地缓存中,在线购物⻋则是会同步这些商品信息到服务端。
⽬前⼤部分商城都是⽀持两种状态的购物⻋,当⽤户没有登录商城时,主要是离线购物⻋在记录⽤户的商品信息,当⽤户登录商城之后,⽤户放⼊到购物⻋中的商品都会同步到服务端,以后在⼿机和电脑等不同平台以及不同时间都能查看到⾃⼰放⼊购物⻋的商品。
订单模块
订单是盘活整个商城的核⼼功能模块,如果没有订单的产出,平台将难以维持下去。订单模块管理着⽤户在平台的交易记录, 是⽤户和商家交流购买商品状态的渠道,⽤户可以随时更改⼀个订单的状态,商家则必须按照业务流程及时更新订单,以告知
⽤户已购买商品的具体状态。
通常⼀个订单分为以下⼏个状态:待付款、待发货、待收货、待评价、交易完成、⽤户取消、仅退款、退货退款状态。⼀个订单的流程⻅下图:
库存模块
这⾥主要记录的是商品SKU的具体库存信息,主要功能包括库存交易、库存管理。库存交易是指⽤户购买商品时实时消费库存,库存管理主要包括运营⼈员对商品的⽣产或采购⼊库、调拨。
⼀般库存信息分为商品SKU、仓区、实时库存、锁定库存、待退货库存、活动库存。
现在⼤部分电商都实现了华南华北的库存分区,所以可能存在同⼀个商品SKU在华北没有库存,⽽在华南存在库存的情况,所以我们需要有仓区这个字段,⽤来区分不同地区仓库的同⼀个商品SKU。
实时库存则是指商品的实时库存,锁定库存则表示⽤户已经提交订单到实际扣除库存或订单失效的这段时间⾥锁定的库存,待退货库存、活动库存则分别表表示订单退款时的库存数量以及每次活动时的库存数量。
除了这些库存信息,我们还可以为商品设置库存状态,例如虚拟库存状态、实物库存状态。如果⼀个商品不需要设定库存,可以任由⽤户购买,我们则不需要在每次⽤户购买商品时都去查询库存、扣除库存,只需要设定商品的库存状态为虚拟库存即 可。
促销活动模块
促销活动模块是指消费券、红包以及满减等促销功能,这⾥主要包括了活动管理和交易管理。前者主要负责管理每次发放的消费券及红包有效期、⾦额、满⾜条件、数量等信息,后者则主要负责管理⽤户领取红包、消费券等信息。
业务难点
了解了以上那些主要模块的具体业务之后,我们就可以更深⼊地去评估从业务落地到系统实现,可能存在的难点以及性能瓶颈了。
不同商品类别存在差异,如何设计商品表结构?
我们知道,⼀个⼿机商品的详细信息跟⼀件⾐服的详细信息差别很⼤,⼿机的SKU包括了颜⾊、运⾏内存、存储内存等,⽽⼀件⾐服则包含了尺码、颜⾊。
如果我们需要将这些商品都存放在⼀张表中,要么就使⽤相同字段来存储不同的信息,要么就新增字段来维护各⾃的信息。前者会导致程序设计复杂化、表宽度⼤,从⽽减少磁盘单⻚存储⾏数,影响查询性能,且维护成本⾼;后者则会导致⼀张表中字段过多,如果有新的商品类型出现,⼜需要动态添加字段。
⽐较好的⽅式是通过⼀个公共表字段来存储⼀些具有共性的字段,创建单独的商品类型表,例如⼿机商品⼀个表、服饰商品⼀个表。但这种⽅式也有缺点,那就是可能会导致表⾮常多,查询商品信息的时候不够灵活,不好实现全⽂搜索。
这时候,我们可以基于⼀个公共表来存储商品的公共信息,同时结合搜索引擎,将商品详细信息存储到键值对数据库,例如
ElasticSearch、Solr中。
双⼗⼀购物⻋商品数量⼤增,购物⻋系统出现性能瓶颈怎么办?
在⽤户没有登录系统的情况下,我们是通过cookie来保存购物⻋的商品信息,⽽在⽤户登录系统之后,购物⻋的信息会保存到数据库中。
在双⼗⼀期间,⼤部分⽤户都会提前将商品加⼊到购物⻋中,在加⼊商品到购物⻋的这段操作中,由于时间⽐较⻓,操作会⽐较分散,所以对数据库的写⼊并不会造成太⼤的压⼒。但在购买时,由于多数属于抢购商品,⽤户对购物⻋的访问则会⽐较集中了,如果都去数据库中读取,那么数据库的压⼒就可想⽽知了。
此时我们应该考虑冷热数据⽅案来存储购物⻋的商品信息,⽤户⼀般都会⾸选最近放⼊购物⻋的商品,这些商品信息则是热数据,⽽较久之前放⼊购物⻋中的商品信息则是冷数据,我们需要提前将热数据存放在Redis缓存中,以便提⾼系统在活动期间 的并发性能。例如,可以将购物⻋中近⼀个⽉的商品信息都存放到Redis中,且⾄少为⼀个分⻚的信息。
当在缓存中没有查找到购物⻋信息时,再去数据库中查询,这样就可以⼤⼤降低数据库的压⼒。
订单表海量数据,如何设计订单表结构?
通常我们的订单表是系统数据累计最快的⼀张表,⽆论订单是否真正付款,只要订单提交了就会在订单表中创建订单。如果公司的业务发展⾮常迅速,那么订单表的分表分库就只是迟早的事⼉了。
在没有分表之前,订单的主键ID都是⾃增的,并且关联了⼀些其它业务表。⼀旦要进⾏分表分库,就会存在主键ID与业务耦合的情况,⽽且分表后新⾃增ID与之前的ID也可能会发⽣冲突,后期做表升级的时候我们将会⾯临巨⼤的⼯作量。如果我们确定后期做表升级,建议提前使⽤snowflake来⽣成主键ID。
如果订单表要实现⽔平分表,那我们基于哪个字段来实现分表呢?
通常我们是通过计算⽤户ID字段的Hash值来实现订单的分表,这种⽅式可以优化⽤户购买端对订单的操作性能。如果我们需要对订单表进⾏⽔平分库,那就还是基于⽤户ID字段来实现。
在分表分库之后,对于我们的后台订单管理系统来说,查询订单就是⼀个挑战了。通常后台都是根据订单状态、创建订单时间进⾏查询的,且需要⽀持分⻚查询以及部分字段的JOIN查询,如果需要在分表分库的情况下进⾏这些操作,⽆疑是⼀个巨⼤ 的挑战了。
对于JOIN查询,我们⼀般可以通过冗余⼀些不常修改的配置表来实现。例如,商品的基础信息,我们录⼊之后很少修改,可
以在每个分库中冗余该表,如果字段信息⽐较少,我们可以直接在订单表中冗余这些字段。
⽽对于分⻚查询,通常我们建议冗余订单信息到⼤数据中。后台管理系统通过⼤数据来查询订单信息,⽤户在提交订单并且付款之后,后台将会同步这条订单到⼤数据。⽤户在C端修改或运营⼈员在后台修改订单时,会通过异步⽅式通知⼤数据更新该订单数据,这种⽅式可以解决分表分库后带来的分⻚查询问题。
抢购业务,如何解决库存表的性能瓶颈?
在平时购买商品时,我们⼀般是直接去数据库检查、锁定库存,但如果是在促销活动期间抢购商品,我们还是直接去数据库检查、更新库存的话,⾯对⾼并发,系统⽆疑会产⽣性能瓶颈。
⼀般我们会将促销活动的库存更新到缓存中,通过缓存来查询商品的实时库存,并且通过分布式锁来实现库存扣减、锁定库存。分布式锁的具体实现,我会在第41讲中详讲。
促销活动也存在抢购场景,如何设计表?
促销活动中的优惠券和红包交易,很多时候跟抢购活动有些类似。
在⼀些⼤型促销活动之前,我们⼀般都会定时发放各种商品的优惠券和红包,⽤户需要点击领取才能使⽤。所以在⼀定数量的优惠券和红包放出的同时,也会存在同⼀时间抢购这些优惠券和红包的情况,特别是⼀些热销商品。
我们可以参考库存的优化设计⽅式,使⽤缓存和分布式锁来查询、更新优惠券和红包的数量,通过缓存获取数量成功以后,再通过异步⽅式更新数据库中优惠券和红包的数量。
总结
这⼀讲,我们结合电商系统实战练习了如何进⾏表设计,可以总结为以下⼏个要点:
在字段⽐较复杂、易变动、不⽅便统⼀的情况下,建议使⽤键值对来代替关系数据库表存储;
在⾼并发情况下的查询操作,可以使⽤缓存代替数据库操作,提⾼并发性能; 数据量叠加⽐较快的表,需要考虑⽔平分表或分库,避免单表操作的性能瓶颈;
除此之外,我们应该通过⼀些优化,尽量避免⽐较复杂的JOIN查询操作,例如冗余⼀些字段,减少JOIN查询;创建⼀些中间表,减少JOIN查询。
思考题
你在设计表时,是否使⽤过外键来关联各个表呢?⽬前互联⽹公司⼀般建议逻辑上实现各个表之间的关联,⽽不建议使⽤外键来实现实际的表关联,你知道这为什么吗?
期待在留⾔区看到你的⻅解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起讨论。
精选留⾔
-W.LI-
外键关联,对表数据操作时,⼀锁锁好⼏张表。删除时还要做校验。影响性能
2019-08-17 06:32
作者回复
对的
2019-08-20 09:15
QQ怪
逻辑复杂,且性能低下
2019-08-17 21:08
⼩橙橙
⽼师,⽂中说的“通过⼤数据查询订单信息”这部分,能不能深⼊讲⼀下⼤数据实现的⽅案
2019-08-20 11:54
失⽕的夏天
外键是⼀个强制性的约束,插⼊数据会强制去外键表⾥先查是否有这个数据。
更新删除的时候还得去获取外键数据的锁,并发性能下降,⾼并发情况还可能造成死锁。 使⽤外键,讲代码层的逻辑转移到了数据库中,数据库性能开销变⼤,性能容易产⽣瓶颈。
除了以上⼏个,好像还有在⽔平分表和分库情况下,外键是⽆法⽣效的。倒是我⼀直没理解是为什么。⽼师能说⼀下为什么吗
?还有外键在如果存在在分库分表的系统中会有什么样的问题?
2019-08-17 08:02
灿烂明天
⽼师,你好,那逻辑上实现表的关联,具体怎么做才好呢?举个例⼦吧,谢谢
2019-08-23 22:06
作者回复
例如,订单表和详细订单表中,如果是物理关联的情况下,是以订单表的ID关联详细订单表外键orderID。
在逻辑上关联的意思就是,在没有任何业务操作的情况下,详细订单表依然有orderID字段,只是我们不需要再物理关联订单表
ID了。⼀旦有业务操作,我们记得在业务层将两张表的操作关联起来,例如,删除主订单,此时记得删除详细订单表。
2019-08-24 09:33
星星滴蓝天双云
2019-08-20 11:51
⼩笨蛋
有两个疑问,第⼀:如果把订单表以⽤户id维度⽔平分表,商家要查看他们店的所有订单情况怎么办?第⼆:如果商品⽐较多,商品也需要分表的话,⽤户搜索商品怎么处理?商家的要看⾃⼰店的商品怎么办?
2019-08-18 19:59
作者回复
第⼀个疑问,商家的订单表可⽤冗余数据来实现,商家查看的⼀套数据存放在键值对数据库;第⼆个问题,如果商品数据量⽐较⼤,我们可将商品存放在Elasticsearch、Solr。
2019-08-20 09:37
张学磊
主要会影响性能和并发度,外键为保证数据⼀致性和完整性以及级联操作会增加额外的操作,这样会影响操作性能,并且修改数据需要去另外⼀个表检查数据,并需要获取额外的锁,在⾼并发场景下,使⽤外键容易造成死锁
2019-08-17 22:30
作者回复
会存在死锁的可能
2019-08-20 09:23
许童童
外键对数据库性能有影响,⽐如保持数据的⼀致性,所以我们在程序层⾯保证数据的⼀致性,这样数据库的性能就会好很多。
2019-08-17 14:21
作者回复
对的,⽤逻辑关联来保证各个表数据的⼀致性。
2019-08-20 09:24
⽼杨同志
使⽤外键,在⼿⼯处理数据时特别麻烦。update数据要求先后顺序。程序在更新数据时多了外键约束检查也影响性能
2019-08-17 11:04
作者回复
是的,即麻烦⼜影响性能。
2019-08-20 09:21