後端儲存架構設計迷思

2022-09-09 18:45:14 字數 4010 閱讀 6033

**「極客時間」 《後端儲存實戰課》,感覺在實際專案設計中很有用。

以設計電商系統為例。首先分析電商系統的業務模組,畫uml圖。電商系統面對的使用者包括:使用者、老闆、運營人員。

電商系統的核心功能是購物,流程為:

使用者瀏覽商品 -> 加入購物車 -> 下單 -> 支付 -> 商家發貨 -> 使用者收貨

可以畫乙個時序圖感受一下。

另外,電商系統的主要模組還包括:

對於乙個電商系統,正確使用資料庫事務是必要的。

乙個訂單系統,提供建立訂單的http介面,使用者在瀏覽器頁面上點選「提交訂單」按鈕的時候,瀏覽器就會給訂單系統發乙個建立訂單的請求,訂單系統的後端服務,在收到請求之後,往資料庫的訂單表插入一條訂單資料,建立訂單成功。

假如說,使用者點選「建立訂單」的按鈕時手一抖,點了兩下,瀏覽器發了兩個http請求,結果是什麼?建立了兩條一模一樣的訂單。這樣肯定不行,需要做防重。

有的同學會說,前端頁面上應該防止使用者重複提交表單,你說的沒錯。但是,網路錯誤會導致重傳,很多rpc框架、閘道器都會有自動重試機制,所以對於訂單服務來說,重複請求這個事兒,你是沒辦法完全避免的。

解決辦法是,讓你的訂單服務具備冪等性。什麼是冪等呢?乙個冪等操作的特點是,其任意多次執行所產生的影響均與一次執行的影響相同。也就是說,乙個冪等的方法,使用同樣的引數,對它進行呼叫多次和呼叫一次,對系統產生的影響是一樣的。所以,對於冪等的方法,不用擔心重複執行會對系統造成任何改變。乙個冪等的建立訂單服務,無論建立訂單的請求傳送多少次,正確的結果是,資料庫只有一條新建立的訂單記錄。

如何實現冪等?

我們知道,表的主鍵自帶唯一約束,如果我們在一條insert語句中提供了主鍵,並且這個主鍵的值在表中已經存在,那這條insert會執行失敗,資料也不會被寫入表中。我們可以利用資料庫的這種「主鍵唯一約束」特性,在插入資料的時候帶上主鍵,來解決建立訂單服務的冪等性問題。

具體的做法是這樣的,我們給訂單系統增加乙個「生成訂單號」的服務,這個服務沒有引數,返回值就是乙個新的、全域性唯一的訂單號。在使用者進入建立訂單的頁面時,前端頁面先呼叫這個生成訂單號服務得到乙個訂單號,在使用者提交訂單的時候,在建立訂單的請求中帶著這個訂單號。

這個訂單號也是我們訂單表的主鍵,這樣,無論是使用者手抖,還是各種情況導致的重試,這些重複請求中帶的都是同乙個訂單號。訂單服務在訂單表中插入資料的時候,執行的這些重複insert語句中的主鍵,也都是同乙個訂單號。資料庫的唯一約束就可以保證,只有一次insert語句是執行成功的,這樣就實現了建立訂單服務冪等性。

還有一點需要注意的是,如果是因為重複訂單導致插入訂單表失敗,訂單服務不要把這個錯誤返回給前端頁面。否則,就有可能出現這樣的情況:使用者點選建立訂單按鈕後,頁面提示建立訂單失敗,而實際上訂單卻建立成功了。正確的做法是,遇到這種情況,訂單服務直接返回訂單建立成功就可以了。

關於網路延遲問題:考慮修改訂單的業務場景,如果有一條修改請求a:將訂單號改為666,和另一條修改請求b:將訂單號修改為888。對於網路斷線重連的情況,可能會重發請求,導致結果不一致。這個問題怎麼解決?

aba問題怎麼解決?這裡給你提供乙個比較通用的解決方法。給你的訂單主表增加一列,列名可以叫version,也即是「版本號」的意思。每次查詢訂單的時候,版本號需要隨著訂單資料返回給頁面。頁面在更新資料的請求中,需要把這個版本號作為更新請求的引數,再帶回給訂單更新服務。

訂單服務在更新資料的時候,需要比較訂單當前資料的版本號,是否和訊息中的版本號一致,如果不一致就拒絕更新資料。如果版本號一致,還需要再更新資料的同時,把版本號+1。「比較版本號、更新資料和版本號+1」,這個過程必須在同乙個事務裡面執行。

具體的sql可以這樣來寫:

update orders set tracking_number = 666, version = version + 1

where version = 8;

使用資料庫快取,抵擋絕大部分讀請求;

使用mongodb儲存商品引數,從而跨商品類別儲存;

將商品介紹靜態化

對於電商系統而言,使用資料庫事務是基本的處理方法。首先需要確定資料庫事務的隔離級別。

一般用rc或者rr相對更安全,另外兩種隔離級別不常用。有一種通用的方法可以讓電商系統的事務處理更安全合理,更能夠應對併發場景:

這種方法對於rc和rr都適用。

需要特別注意的一點是,更新賬戶餘額後,不能只檢查更新語句是不是執行成功了,還需要檢查返回值中變更的行數是不是等於1。因為即使流水號不相等,餘額沒有更新,這條更新語句的執行結果仍然是成功的,只是更新了0條記錄。

對於多個系統之間協調使用的事務場景,就需要使用分布式事務。這裡的「分布式」當然不是多台機器的意思,只是系統的多個模組,或者多個系統。單一系統內部是不需要的。

事務需要acid,但是連資料庫事務都未必完全滿足acid,分布式事務當然也不會完全滿足。因此,在沒有完整的acid的情況下,就需要進行其他演算法的協調。重點介紹2pc,還有3pc,tcc等

2pc:兩階段提交

考慮業務場景為:使用者在訂單中新增優惠券。這個過程涉及到訂單系統、**系統兩部分的連線。乙個事務跨越多個系統,就需要分布式事務參與。

訂單系統需要:

訂單系統內兩個操作的一致性問題可以直接使用資料庫事務來解決。**系統需要操作就比較簡單,把剛剛使用的那張優惠券的狀態更新成「已使用」就可以了。我們需要這兩個系統的資料更新操作保持一致,要麼都更新成功,要麼都更新失敗。

2pc引入乙個事務協調者的角色,來協調訂單系統和**系統,協調者對客戶端提供乙個完整的「使用優惠券下單」的服務,在這個服務的內部,協調者再分別呼叫訂單和**的相應服務。兩階段提交指的是準備階段和提交階段。在準備階段,事務協調者協調多個系統,都進入準備階段後,整體再進入提交階段。在準備階段,如果任何一步出現錯誤或者是超時,協調者就會給兩個系統傳送「回滾事務」請求。每個系統在收到請求之後,回滾自己的資料庫事務,分布式事務執行失敗,兩個系統的資料庫事務都回滾了,相關的所有資料回滾到分布式事務執行之前的狀態,就像這個分布式事務沒有執行一樣。

如果準備階段成功,進入提交階段,這個時候就「只有華山一條路」,整個分布式事務只能成功,不能失敗。

如果發生網路傳輸失敗的情況,需要反覆重試,直到提交成功為止。如果這個階段發生宕機,包括兩個資料庫宕機或者訂單服務、**服務所在的節點宕機,還是有可能出現訂單庫完成了提交,但**庫因為宕機自動回滾,導致資料不一致的情況。但是,因為提交的過程非常簡單,執行很快,出現這種情況的概率非常小,所以,從實用的角度來說,2pc這種分布式事務的方法,實際的資料一致性還是非常好的。

在實現2pc的時候,沒必要單獨啟動乙個事務協調服務,這個協調服務的工作最好和訂單服務或者優惠券服務放在同乙個程序裡面,這樣做有兩個好處:

參與分布式事務的程序更少,故障點也就更少,穩定性更好;

減少了一些遠端呼叫,效能也更好一些。

2pc是一種強一致的設計,它可以保證原子性和隔離性。只要2pc事務完成,訂單庫和**庫中的資料一定是一致的狀態,也就是我們總說的,要麼都成功,要麼都失敗。

所以2pc比較適合那些對資料一致性要求比較高的場景,比如我們這個訂單優惠券的場景,如果一致性保證不好,有可能會被黑產利用,一張優惠券反覆使用,那樣我們的損失就大了。

2pc也有很明顯的缺陷,整個事務的執行過程需要阻塞服務端的執行緒和資料庫的會話,所以,2pc在併發場景下的效能不會很高。並且,協調者是乙個單點,一旦過程中協調者宕機,就會導致訂單庫或者**庫的事務會話一直卡在等待提交階段,直到事務超時自動回滾。

卡住的這段時間內,資料庫有可能會鎖住一些資料,服務中會卡住乙個資料庫連線和執行緒,這些都會造成系統效能嚴重下降,甚至整個服務被卡住。

所以,只有在需要強一致、並且併發量不大的場景下,才考慮使用2pc。

elasticsearch是一種全文搜尋資料庫。其索引採用了倒排索引的機制,首先將內容進行分詞,然後進行倒排索引。因此對內容的部分分詞進行搜尋,效果比較好。在搜尋時,es也會自動把搜尋條件也分詞以後進行匹配。

但是,倒排索引相比於一般資料庫採用的b樹索引,它的寫入和更新效能都比較差,因此倒排索引也只是適合全文搜尋,不適合更新頻繁的交易類資料。

訂單資料具有熱尾效應,往往大量訪問的資料都是最近新增的資料,歷史資料幾乎很少被訪問到。因而可以對歷史資料進行歸檔。

具體的實現:

後端技術雜談8 OpenStack架構設計

openstack networking code name neutron 網路服務 openstack object storage code name swift 物件儲存服務 openstack block storage code name cinder 塊裝置儲存服務 openstack...

salesforce 架構設計 從架構設計到架構師

因為碎片化的時間多了,所以開始刷起某乎了,關注了架構相關的板塊,也順手回答了一些問題。發現有很多同道中人正在經歷著我前兩年經歷的階段,對於做架構沒有相對具象的一些理解,更沒有系統化的認識。所以把最近回答的一些內容整理一下,權當記錄,留給3年後的自己 按慣例,容許我裝x開頭 一 架構的定義 在軟體開發...

mysql架構設計 初識mysql架構設計

一 應用系統如何與mysql進行一次互動?最開始接觸jdbc的時候,我們系統如何完成一次sql操作呢?第一步,建立資料庫連線 第二步,操作sql 第三步,釋放連線。但是每次建立與資料庫的連線非常耗時和資源,所以我們加入了連線池的概念。第一步的獲取連線是從連線池中獲取乙個可用的連線,第三步的釋放連線不...