來還債了,但願你們在疫情中都是平安的,回來的時候公司也還在!git
skr shop是一羣底層碼農,因爲被工做中的項目折磨的精神失常,加之因爲程序員的自傲:別人設計的系統都是一坨shit,個人設計纔是宇宙最牛逼,因而乎決定要作一個只設計不編碼的電商設計手冊。
項目地址:https://github.com/skr-shop/m...程序員
在上一篇文章 購物車設計之需求分析 描述了購物車的通用需求。本文重點則在如何實現上進行架構上的設計(業務+系統架構)。github
架構設計能夠分爲三個層面:golang
快速簡單的說明下三個架構的意思;當咱們拿到購物車需求時,咱們說用Golang來實現,存儲用Redis;這描述的是技術架構;咱們對購物車代碼項目進行代碼分層,設計規範,以及依賴系統的規劃這叫系統架構;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/eas...
說完了加車前,如今來看購物車這一部分。咱們在以前曾討論過,購物車可能會有多種形態的,好比:存儲多個商品一塊兒結算,某個商品當即結算等。所以購物車必定會根據渠道來進行購物車類型的選擇。
這部分的操做相對是比較穩定的。咱們挑幾個比較重要的操做來說一下思路便可。
經過把條件驗證的前置,會發如今進行加車操做時,這部分邏輯已經變得很是的輕量了。要作的主要是下面幾個部分的邏輯。
這裏有幾個取巧的地方,首先是獲取商品的邏輯,因爲在前面驗證的時候也會用到,所以這裏前面獲取後會經過參數的方式繼續日後傳遞,所以這裏不須要在讀庫或者調用服務來獲取;
其次這裏須要把當前用戶現有購物車數據獲取到,而後將添加的這個商品添加進來。這是一個相似合併操做,原來這個商品是存在,至關於數量加一;須要注意這個商品跟現存的商品有沒有父子關係,有沒有可能加入後改變了某個活動規則,好比:原來買了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最經濟划算。
至此對於購物車的實現設計算是完結了,其中關於訂單表的設計會單獨放到訂單模塊去講。
對於整個購物車服務,雖然沒有寫的詳細到某個具體的接口,可是分析到這一步,我相信你們心中都是有溝壑的,可以結合本身的業務去實現它。
文中有些頗有意思的地方,建議你們動手去作作看,有任何問題,咱們隨時交流。
接下來終於要到訂單部分的設計了,但願你們繼續關注咱們。