来还债了,但愿你们在疫情中都是平安的,回来的时候公司也还在!git
skr shop是一群底层码农,因为被工做中的项目折磨的精神失常,加之因为程序员的自傲:别人设计的系统都是一坨shit,个人设计才是宇宙最牛逼,因而乎决定要作一个只设计不编码的电商设计手册。程序员
项目地址:https://github.com/skr-shop/manualsgithub
在上一篇文章 购物车设计之需求分析 描述了购物车的通用需求。本文重点则在如何实现上进行架构上的设计(业务+系统架构)。web
架构设计能够分为三个层面:redis
快速简单的说明下三个架构的意思;当咱们拿到购物车需求时,咱们说用Golang来实现,存储用Redis;这描述的是技术架构;咱们对购物车代码项目进行代码分层,设计规范,以及依赖系统的规划这叫系统架构;json
那业务架构是什么呢?业务架构本质上是对系统架构的文字语言描述;什么意思?咱们拿到一个需求首先要跟需求方进行沟通,创建统一的认知。好比:规范名词(购物车中说的商品与商品系统中商品的含义是不一样的);创建你们都能明白的模型,购物车、用户、商品、订单这些实体之间的互动,以及各自具有什么功能。设计模式
在业务架构分析上有不少方法论,好比:领域驱动设计,可是它并非惟一的业务架构分析方法,也并非说最好的。适合你的就是最好的。咱们经常使用的实体关系图、UML图也属于业务架构领域;网络
这里须要强点一点的是,无论你用什么方式来建模设计,有设计总比没设计强,其次必定要将建模的内容体现到你的代码中去。数据结构
本文在业务架构上的分析借助了 DDD
(领域驱动设计)思想;仍是那句话适合的就是最好的
。架构
经过前面的需求分析,咱们已经明确咱们的购物车要干什么了。先来看一下一个典型的用户操做购物车过程。
在这个过程当中,用户使用购物车这个载体完成了商品的购买流程;不断流动的数据是商品,购物车这个载体是稳定的。这是咱们系统中的稳定点与变化点。
商品的流动方式可能多种多样,好比从不一样地方加入购物车,不一样方式加入购物车,生命周期在购物车中也不同;可是这个流程是稳定的,必定是先让购物车中存在商品,而后才能去结算产生订单。
商品在购物车中的生命周期以下:
按照这个过程,咱们来看一下每一个阶段对应的操做。
这里注意一点,加车前这个操做其实咱们能够放到购物车的添加操做中,可是因为这部分是很是不稳定且多变的。咱们将其独立出来,方便后续进行扩展而不影响相对比较稳定的购物车阶段。
上面这三个阶段,按照DDD中的概念,应该叫作实体,他们总体构成了购物车这个域;今天咱们先不讲这些概念,就先略过,后面有机会单独发文讲解。
经过流程分析,咱们总结出了系统须要具有的操做接口,以及这些接口对应的实体,如今咱们先来看加车前主要要作些什么;
加车前其实主要就是对准备加入的购物车商品进行各个纬度的校验,检查是否知足要求。
在让用户加车前,咱们首先解决的是用户从哪里卖,而后进行验证?由于同一个商品从不一样渠道购买是存在不一样状况的,好比:小米手机,咱们是经过秒杀买,仍是经过好友众筹买,或者商城直接购买,价格存在差别,可是实际上他是同一个商品;
第二个问题是是否具有购买资格,仍是上面说的,秒杀、众筹这个加车操做,不是谁均可以添加的,得现有资格。那么资格的检查也是放到这里;
第三个问题是对这个购买的商品进行商品属性上的验证,如是否上下架,有库存,限购数量等等。
并且你们会发现,这里的验证条件多是很是多变的。如何构建一个方便扩展的代码呢?
整个加车过程,重要的就是根据来源来区分不一样的验证。咱们有两种选择方式。
方式一:经过策略模式+门面模式的方式来搞定。策略就是根据不一样的加车来源进行不一样的验证,门面就是根据不一样的来源封装一个个策略;
方式二:经过责任链模式,可是这里须要有一个变化,这个链在执行过程当中,能够选择跳过某些节点,好比:秒杀不须要库存、也不须要众筹的验证;
经过综合的分析我选择了责任链的模式。贴一下核心代码
// 每一个验证逻辑要实现的接口 type Handler interface { Skipped(in interface{}) bool // 这里判断是否跳过 HandleRequest(in interface{}) error // 这里进行各类验证 }// 责任链的节点 type RequestChain struct { Handler Next *RequestChain }
复制代码// 设置handler func (h *RequestChain) SetNextHandler(in *RequestChain) *RequestChain { h.Next = in return in } 复制代码
关于设计模式,你们能够看我小伙伴的github:https://github.com/TIGERB/easy-tips/tree/master/go/src/patterns
说完了加车前,如今来看购物车这一部分。咱们在以前曾讨论过,购物车可能会有多种形态的,好比:存储多个商品一块儿结算,某个商品当即结算等。所以购物车必定会根据渠道来进行购物车类型的选择。
这部分的操做相对是比较稳定的。咱们挑几个比较重要的操做来说一下思路便可。
经过把条件验证的前置,会发如今进行加车操做时,这部分逻辑已经变得很是的轻量了。要作的主要是下面几个部分的逻辑。
这里有几个取巧的地方,首先是获取商品的逻辑,因为在前面验证的时候也会用到,所以这里前面获取后会经过参数的方式继续日后传递,所以这里不须要在读库或者调用服务来获取;
其次这里须要把当前用户现有购物车数据获取到,而后将添加的这个商品添加进来。这是一个相似合并操做,原来这个商品是存在,至关于数量加一;须要注意这个商品跟现存的商品有没有父子关系,有没有可能加入后改变了某个活动规则,好比:原来买了2个送1个赠品,如今再添加了一个变成3个,送2个赠品;
注意:这里的添加并非在购物车直接改数量,可能就是在列表、详情页直接添加添加。
经过将合并后的购物车数据,经过营销活动检查确认ok后,直接回写到存储中。
为何会有合并购物车这个操做?由于通常电商都是准许游客身份进行操做的,所以当用户登陆后须要将两者进行合并。
这里的合并不少部分的逻辑是能够与加入购物车复用的逻辑。好比:合并后的数据都须要检查是否合法,而后覆写回存储中。所以你们能够看到这里的关联性。设计的方法在某种程度上要通用。
购物车列表这是一个很是重要的接口,原则上购物车接口会提供两种类型,一种简版,一种彻底版本;
简版的列表接口主要是用在相似PC首页右上角之类获取简单信息;彻底版本就是在购物车列表中会用到。
在实际实现中,购物车毫不仅仅是一个读取接口那么简单。由于咱们都知道无论是商品信息、活动信息都是在不断的发生变化。所以每次的读取接口必然须要检查当前购物车中数据的合法性,而后发现不一致后须要覆写原存储的数据。
也有一些作法会在每一个接口都去检查数据的合法性,我建议为了性能考虑,部分接口能够适当放宽检查,在获取列表时再进行完整的检查。好比添加接口,我只会检测我添加的商品的合法性,毫不会对整个购物车进行检查。由于该操做以后通常都会调用列表操做,那么此时还会进行校验,两者重复操做,所以只取后者。
结算包括两部分,结算页的详情信息与提交订单。结算页能够说是在购物车列表上的一个包装,由于结算页与列表页最大的不一样是须要用户选择配送地址(虚拟商品另说),此时会产生更明确的价格信息,其余基本一致。所以在设计购物车列表接口的时候,必定要考虑充分的通用性。
这里另一个须要注意的是:当即购买,咱们也会经过结算页接口来实现,可是内部其实仍是会调用添加接口,将商品添加到购物车中;有三个须要注意的地方,首先是这个添加操做是服务内部完成的,对于服务调用方是不须要感知这个加入操做的存在;其次是这个购物车在Redis中的Key是独立于普通购物车的,不然两者的商品耦合在一块儿很是难于操做处理;最后当即购买的购物车要考虑帐号多终端登陆的时候,彼此数据不能互相影响,这里能够用每一个端的uuid来做为购物车的标记避免这种状况。
购物车的最后一步是生成订单,这一步最要紧的是须要给购物车加锁,避免提交过程当中数据被篡改,多说一句,不少人写的Redis分布式锁代码都存在缺陷,你们必定要注意原子性的问题,这类文章网络上不少再也不赘述。
加锁成功以后,咱们这里有多种作法,一种是按照DB涉及组织数据开始写表,这适用于业务量要求不大,好比订单每秒下单量不超过2000K的;那若是你的系统并发要求很是高怎么办?
其实也很简单,高性能的三大法宝之一:异步;咱们提交的时候直接将数据快照写入MQ中,而后经过异步的方式进行消费处理,能够经过经过控制消费者的数量来提高处理能力。这种方法虽然性能提高,可是复杂度也会上升,你们须要根据本身的实际状况来选择。
关于业务架构的设计,到此告一段落,接下来咱们来看系统架构。
系统结构主要包含,如何将业务架构映射过来,以及输出对应输入参数、输出参数的说明。因为输入、输出针对各自业务来肯定的,并且没有什么难度,咱们这里就只说如何将业务架构映射到系统架构,以及系统架构中最核心的Redis数据结构选择以及存储的数据结构设计。
下面的代码目录是按照 Golang
来进行设计的。咱们来看看如何将上面的业务架构映射到代码层面来。
├── addproducts.go
├── cartlist.go
├── mergecart.go
├── entity
│ ├── cart
│ │ ├── add.go
│ │ ├── cart.go
│ │ └── list.go
│ ├── order
│ │ ├── checkout.go
│ │ ├── order.go
│ │ └── submit.go
│ └── precart
├── event
│ └── sendorder.go
├── facade
│ ├── activity.go
│ └── product.go
└── repo
复制代码
外层有 entity
、event
、facade
、repo
这四个目录,职责以下:
entity: 存放的是咱们前面分析的购物领域的三个实体;全部主要的操做都在这三个实体上;
event: 这是用来处理产生的事件,好比刚刚说的若是咱们提交订单采用异步的方式,那么该目录就该完成的是如何把数据发送到MQ中去;
facade: 这儿目录是干吗的呢?这主要是由于咱们的服务还须要依赖像商品、营销活动这些服务,那么咱们不该该在实体中直接调用它,由于第三方可能存在变更,或者有增长、减小,咱们在这里进行如下简单的封装(设计模式中的门面模式);
repo: 这个目录从某种程度上能够理解为 Model
层,在整个领域服务中,若是与持久化打交道,都经过它来完成。
最后外层的几个文件,就是咱们所提供的领域服务,供应用层来进行调用的。
为了保证内容的紧凑,我这里放弃了对整个微服务的目录介绍,只单独介绍了领域服务,后续会单独成文介绍下微服务的整个系统架构。
经过上面的划分,咱们完成了两件事情:
业务架构分析的结构在系统代码中都有映射,他们彼此体现。这样最大的好处是,保证设计与代码的一致性,看了文档你就知道对应的代码在哪里;
每一个目录各自的关注点都进行了分离,更内聚,更容易开发与维护。
如今来看,咱们选择Redis做为购物商品数据的存储,咱们要解决两个问题,一是咱们须要存哪些数据?二是咱们用什么结构来存?
网络上不少写购物车的都是只保存一个商品id,真实场景是很难知足需求的。你想一想,一个商品id如何记住用户选择的赠品?用户上次选择的活动?以及购买的商品渠道?
综合比较通用的场景,我给出一个参考结构:
// 购物车数据
type ShoppingData struct {
Item []*Item `json:"item"`
UpdateTime int64 `json:"update_time"`
Version int32 `json:"version"`
}
// 单个商品item元素
type Item struct {
ItemId string `json:"item_id"`
ParentItemId string `json:"parent_item_id,omitempty"` // 绑定的父item id
OrderId string `json:"order_id,omitempty"` // 绑定的订单号
Sku int64 `json:"sku"`
Spu int64 `json:"spu"`
Channel string `json:"channel"`
Num int32 `json:"num"`
Status int32 `json:"status"`
TTL int32 `json:"ttl"` // 有效时间
SalePrice float64 `json:"sale_price"` // 记录加车时候的销售价格
SpecialPrice float64 `json:"special_price,omitempty"` // 指订价格加购物车
PostFree bool `json:"post_free,omitempty"` // 是否免邮
Activities []*ItemActivity `json:"activities,omitempty"` // 参加的活动记录
AddTime int64 `json:"add_time"`
UpdateTime int64 `json:"update_time"`
}
// 活动
type ItemActivity struct {
ActID string `json:"act_id"`
ActType string `json:"act_type"`
ActTitle string `json:"act_title"`
}
复制代码
重点说一下 Item
这个结构,item_id
这个字段是标记购物车中某个商品的惟一标记,由于咱们以前说过,同一个sku因为渠道不一样,那么在购物车中会是两个不一样的item;接下来的 parent_item_id
字段是用来标记父子关系的,这里将可能存在的树结构转成了顺序结构,咱们无论是父商品仍是子商品,都采用顺序存储,而后经过这个字段来进行关联;有些同窗可能会奇怪,为何会存order id这个字段呢?你们关注下本身的平常业务,好比:再来一单、定金预售等,这种必定是与某个订单相关联的,无论是为了资格验证仍是数据统计。剩下的字段都是一些很是常规的字段,就不在一一介绍了;
字段的类型,你们根据本身的须要进行修改。
接下来该说怎么选择Redis的存储结构了,Redis经常使用的 Hash Table、集合、有序集合、链表、字符串
五种,咱们一个个来分析。
首先购车必定有一个key来标记这个购物车属于哪一个用户的,为了简化,咱们的key假设是:uid:cart_type
。
咱们先来看若是用 Hash Table
;咱们添加时,须要用到以下命令:HSET uid:cart_type sku ShoppingData
;看起来没问题,咱们能够根据sku快速定位某个商品而后进行相关的修改等,可是注意,ShoppingData是一个json串,若是用户购物车中有很是多的商品,咱们用 HGETALL uid:cart_type
获取到的时间复杂度是O(n),而后代码中还须要一一反序列化,又是O(n)的复杂度。
若是用集合
,也会遇到相似的问题,每一个购物车看作一个集合,集合中的每一个元素是 ShoppingData ,取到代码中依然须要逐一反序列化(反序列化是成本),关于有序集合与链表就不在分析,你们能够按照上面的思路去尝试下问题所在。
看起来咱们没得选,只有使用String
,那咱们来看一下String
的契合度是什么样子。首先SET uid:cart_type ShoppingDataArr
;咱们把购物车全部的数据序列化成一个字符串存储,每次取出来的时间复杂度是O(1),序列化、反序列化都只须要一次。看来是很是不错的选择。可是在使用中你们仍是有几点须要注意。
网上也看到不少Redis数据结构组合使用来保存购物车数据的,可是无疑增长了网络开销,相比起来仍是String最经济划算。
至此对于购物车的实现设计算是完结了,其中关于订单表的设计会单独放到订单模块去讲。
对于整个购物车服务,虽然没有写的详细到某个具体的接口,可是分析到这一步,我相信你们心中都是有沟壑的,可以结合本身的业务去实现它。
文中有些颇有意思的地方,建议你们动手去作作看,有任何问题,咱们随时交流。
接下来终于要到订单部分的设计了,但愿你们继续关注咱们。
我的公众号:dayuTalk
联系邮箱:dayugog@gmail.com
GitHub:github.com/helei112g