網傳文章:阿里雙十一的高頻交易分享

若不加思索套用此設計,未必有更好的效能,反而效能會更差。因為這是針對阿里的特例,而非通例。

➊ 地區數據分庫

文中提到,不同地區商品數量擁有不同設定。例如廠商準備三萬件,但北京可以看到一萬件,上海二萬件,所以可能北京商城顯示搶購一空,但上海的商城卻還有貨。

我認為,這不是技術問題,而是業務問題,即業務是否允許這種可能產生不公平待遇的情況出現。想像我們上網訂票,網站顯示售罄,但上海的朋友卻看到還有票的情形。當然,若業務能接受,資料庫團隊壓力自然減輕不少,設計也不複雜,只是要管理兩 (或更多) 個資料庫集群。

➋ 商品品類分庫

文中提到,可以把熱門及冷門商品平均分布,即不要把熱門商品集中在少數資料庫處理。

我認為,這是很好的建議,有經驗的團隊也是如此設計,只要有過去歷史記錄可尋或專家意見即可,因為這等於要能事前區分出冷熱門商品。

➌ 訂單編號編碼邏輯

文中提到,主鍵的設計技巧。以訂單編號為例,主鍵不是常見的循序鍵 (1, 2, 3, …) 也不是 UUID,而是自建業務訂單編號算法。例如「地區碼+商品代碼+時間戳記」。如此編號產生就可以被分散到大量應用程式集群平行處理,不用消耗資料庫效能。

我認為,編號產生從原本資料庫改為應用程式,效能提升並不顯著。這麼設計的最大原因是因為前述所提的「地區數據分庫」及「商品品類分庫」,而不是效能。

如果編號是按照循序鍵 (1, 2, 3, …) 或是 UUID,這種分庫分表的方式,會無法從鍵值中直接得知地理位置或冷熱門資訊,需要再與資料庫、中間庫或其它系統查詢才得知,這會增加整體系統的壓力。所以若能按照地區碼+商品代碼+時間戳記,再依事前定義的地區碼及冷熱門代碼對應,就直接可以判斷資料庫落點,減少查詢步驟,增進效能。

➍ 預先塞入預估總量的數據單量

文中提到,預先產生空的訂單,即用 UPDATE 而非 INSERT 新單來提速。因為資料庫檢查 INSERT 主鍵時,必須確保唯一值,會對整個資料表 (table lock) 加上寫入鎖而造成效能低落。

我認為,這是針對阿里的特例而非通例,貿然採用效能可能會更差。不過在討論之前,先談一些概念。

首先,阿里在這裡用的資料庫是 MySQL/InnoDB,所以不一定適用其他資料庫。

再者,MySQL/InnoDB INSERT 時,這裡說會對整個資料表 (table lock) 加上寫入鎖,但有人回覆不會。事實上,要看情況與設定。接著會討論三種可能影響效能的因素:主鍵值產生時的鎖、主鍵唯一值檢查 (Unique Check) 的鎖、Row Lock。

  1. 主鍵值產生時的鎖。這是 INSERT 要面對的問題,而不是 UPDATE。假設為循序鍵 (1, 2, 3, …),INSERT 時可能會遇到 AUTO-INC Locks [1] 而對整個資料表 (table lock) 加上寫入鎖,不過前述提到阿里不是這種方式,而是「地區碼+商品代碼+時間戳記」,所以沒有這種問題,而且即使採用 AUTO-INC 也有設定可以有效避免 [2]。況且就算遇到 AUTO-INC Locks,事實上也只是 Latch,鎖時間會比我們一般想像中的短。再加上阿里還有分庫分表的設計,通常理論上可以忽略不計。
  2. 主鍵唯一值檢查的鎖。不管當下由 INSERT 決定,或是先 INSERT 後 UPDATE (這裡阿里採用的),在 MySQL/InnoDB 的唯一值檢查行為都是類似的。不管是 INSERT 或 UPDATE,都必須掃描主鍵後才會執行,INSERT 要先找到插入點才能執行,UPDATE 同樣也要找到該主鍵才能執行。所以唯一值檢查的耗時對於 INSERT 或 UPDATE 無異,都要找到「那個點」。
  3. Row Lock。這是 UPDATE 要面對的問題,不是 INSERT。不過訂單不能「變更」,確認的訂單只能退訂,所以不可能發生同時間同一訂單發生兩次以上的 UPDATE,所以這裡沒有 Row Lock 的問題。

這樣看來上述提到的三點都不會是問題,可是為什麼阿里還是要「預先塞入預估總量的數據單量」?

因為效能增進主要是利用到「update-in-place」技巧。MySQL/InnodB, SQL Server, Oracle 支援這個技巧,但 PostgreSQL 或 MongoDB 等都不支援。這是設計理念,不支援不代表不好,只是阿里利用這特性加速。

「update-in-place」顧名思義,就是 UPDATE 時,實際底層的資料更新會落在原邏輯物理位置。換句話說,若非「update-in-place」,如 PostgreSQL 或 MongoDB,執行 UPDATE 時,實際上對底層資料的操作會由 INSERT + DELETE 兩個操作完成。

講到這裡,應該就有一些人瞭解了。因為 INSERT 資料需要在記憶體及硬碟中,先安排 / 計算足夠的空間才能寫入。但 UPDATE,因為空間已在 INSERT 時決定,所以只要更新即可。再加上,INSERT 可能會遇到 page split 而造成多個鎖出現,「update-in-place」沒有 page split 問題,所以此時的 UPDATE 會比 INSERT 快得多。

但 MySQL/InnoDB 的 UPDATE 要符合「update-in-place」不是沒有條件,非先 INSERT 後 UPDATE 就會「udpate-in-place」。這點我在 2017 年的「RDBMS 資料庫案例設計討論營系列活動 - Schema 設計技巧」的演講中 [3] 有提到,也可見網友的筆記 [4]。

「update-in-place」有應當的 Schema 設計技巧,否則不對的 Schema 在 UPDATE 時,MySQL/InnoDB 在底層也會是 INSERT + DELETE。也就是說,原本可以 INSERT 處理完的,在「預先塞入預估總量的數據單量」下反而變成是 INSERT + DELETE 兩步驟才完成,還因為 DELETE 留下的碎片,反而造成效能低落。

小結,阿里這種設計是因為他們使用 MySQL/InnoDB,而且也要確保 Schema 能符合 UPDATE 要求的「update-in-place」才能體現效能。

資料佐證來源

[1] https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html

[2] https://dev.mysql.com/doc/refman/5.7/en/innodb-auto-increment-handling.html

[3] https://phptheday.kktix.cc/events/ant-rdbms-01

[4] https://kylinyu.win/rdbms_design/