訂單表優化方案


1 背景

隨着用戶不斷下單,DB訂單表和訂單附屬表的單表記錄數過大,影響到前端和管理系統拉取訂單列表的性能。單表最大多少行合適與具體業務有關,難以下定論,但一般推薦不要超過1千萬行,之后單表的性能下降會比較明顯。
本文檔整理了數據庫大表優化的一些常用思路的原理,最后針對訂單表提出優化方案。

2 常用思路

  • 單表分區
  • 大表分表
  • 業務分庫
  • 讀寫分離和集群
  • 熱點緩存
  • 用ES代替DB

2.1 單表分區

什么是分區?
就是將一張表的單個大文件,按一定邏輯拆分成多個物理的區塊文件。對於應用程序來說,還是一張整表;但底層實際上是由多個物理區塊組成。目前主流的DB如Oracle、MySql等都有成熟的方案支持分區

MySql支持哪些分區類型?

  • range分區:根據key的范圍來分區,比如日志表,可以按天或按月分區
  • list分區:根據key的枚舉值分區,比如以訂單狀態為key,待付款、待發貨、待收貨等分別建立一個分區
  • hash分區:給定分區數,DB根據key的hash值將記錄分散到各個分區,比如以用戶ID為key,將訂單表打散到各個分區
  • key分區:類似hash分區
  • 復合分區:Oracle支持豐富的復合分區方案,而MySql相對就簡單些,只有range和list分區支持子分區,而且子分區必須是hash或Key分區。

MySql的分區限制?

  • MySql(其他DB也是類似的)為保證唯一索引的效率,要求分區字段必須包含在每一個唯一索引中。比如某個訂單表以自增ID為主鍵,訂單ID為唯一索引,用戶ID為普通索引,如果要以用戶ID為分區字段建立分區,則必須將主鍵和唯一索引都修改為組合索引,加上用戶ID。這里就得注意一個問題了,對於組合索引,where條件如果只包含組合索引的個別字段時,必須是從前往后,否則查詢語句不會走索引。比如有一個組合索引是訂單ID+用戶ID,如果以用戶ID為查詢條件,無法使用這個索引;而用訂單ID查詢,可以使用該索引,這是索引的存儲數據結構決定的。
  • 5.6.7以前版本單表最大分區數1024,5.6.7及之后版本最大8192。注意分區數太小優化不明顯,分區數太大則會增加IO系統額外消耗反而降低性能。1024以上的分區數,恐怕只適合日志類的冷熱分離明顯的表,這類表往往只查詢最近的幾個分區;上千張表並發讀寫的話,io會讓人懷疑人生。

MySql如何對現有表分區或修改分區參數?

  • MySql支持ALTER TABLE動態創建或修改表分區,這種方式對線上業務有壓力。
  • 替代方案是創建臨時表,將主表數據導入臨時表,再將臨時表切換為主表。由於線上數據是實時變化的,這種方式需要處理數據最終一致性的問題。

分區的優勢?

  • 對應用程序完全透明、且不明顯增加DB負擔。
  • 以分區字段作為查詢條件,DB會先確定目標可能的分區,再在分區內完成查詢,可以極大的減小查詢的數據量。
  • 按天或按月分區的日志表,需要刪除舊日志時只需要刪除對應的分區即可,簡單高效。
  • 分區文件可以存儲在不同的磁盤,如count(1)類操作支持並發

接下來我們來具體思考針對訂單表,如何優化:

示例一:以用戶ID做HASH分區
之前runner指出用戶對訂單表的查詢,都基於用戶ID的,每個用戶只能拉取自己的訂單信息。所以對於訂單表,可以以用戶ID作為分區字段將訂單表分區打散;這就要求原有的主鍵(自增ID)和唯一索引(訂單ID)都修改為組合索引,分別是自增ID+用戶ID、訂單ID+用戶ID。保留原有的用戶ID的普通索引。比如我們創建4個分區。

  • 於是拉取某個用戶的所有訂單的查詢語句,只會定位到單個分區,語句需要處理的數據量是原來的1/4。
  • 使用訂單ID拉取訂單記錄的查詢,需要加上用戶ID作為條件,這樣查詢語句可以先定位到單個分區,再走訂單ID+用戶ID的組合唯一索引,數據量也優化到原來的1/4。
  • 覆蓋多個分區的查詢或者是掃表操作,由於原來的大文件被拆分成多個小文件,硬盤IO上會更友好,理論上有優化。

示例二:組合分區,range自增ID,hash用戶ID
這個方案可以進一步打散數據,比如range出4個分區,每個range分區再做4個hash子分區。

  • 通過用戶ID拉取訂單列表的情況,會在4個子分區處理,每個子分區的數據量我們粗略認為都是1/16,則一次查詢涉及的數據量還是 1/4(4 * 1/16)
  • 通過訂單ID拉取訂單記錄的查詢,則可以優化成先找出range分區,再找hash分區,最終只需要在其中一個子分區內查找數據,數據量變成原來的 1/16

采用此方案,主鍵和唯一索引需要擴展到3個字段。

示例三:組合分區,list訂單ID,hash用戶ID
電商的訂單展示,有個特點,分待付款、待發貨、待收貨及全部訂單等,很多時候用戶是單獨拉取待發貨、待收貨的訂單頁面的,這時我們可以將訂單表按狀態做一次list分區,再以用戶ID hash出子分區。

  • 通過用戶ID拉取訂單列表的情況,與前兩個方案一致。
  • 通過訂單ID拉取訂單記錄的情況,只會檢索符合狀態的訂單數據集,考慮到大部分訂單是已完成狀態,而拉取待發貨、待收貨訂單的操作可能涉及的數據集將更小

同方案二,采用此方案也需要將主鍵和唯一索引擴展到3個字段。

2.2 大表分表

分表的一個應用場景是替代分區,預先創建多個表名不同但表結構一致的表,並給每個表編號,應用程序在寫或讀之前先用ID取模等方式得到表編號,從而實現單表分區。

本章我們側重於用分表來擴展分區的功能,我們來看某金融交易平台的實際案例,將訂單表划分成多個小表來分散不同的業務請求:

  • 臨時訂單表:存儲尚未出票的訂單,主要是下單和出票系統對這個表進行並發讀寫
  • 訂單概要表:用於訂單列表展示的概要
  • 訂單詳情表:展示訂單詳情時從這個表拉
  • 訂單概要歷史表、訂單詳情歷史表:根據業務特點,超過一定時期的訂單很少會請求到,所以可以挪到歷史表中,從而保證主表的記錄數不會太大

針對電商的訂單表,我們也可以有類似的思路,比如用戶拉取訂單數據時往往分待付款、待發貨、待收貨、全部訂單等,除了全部訂單列表外,前端是按不同階段來拉取和展示訂單的,所以我們可以將不同階段的訂單移到不同的表中,降低單表的記錄數來提高查詢效率。
再比如歷史表,前端拉取1個月前甚至1年前的已完成訂單的機會是不多的,可以將一定時間以前的記錄移到歷史表去。

2.3 業務分庫

隨着業務量進一步增長,單個DB實例已經無法支撐大量的用戶請求時,可以考慮根據業務分庫,甚至是對單個業務進行細粒度的分庫,將不同的請求分散到不同的庫去處理,硬件成本換性能。目前我們平台后台與前端業務部門的后台系統DB是分離的,這就是個分庫的案例。

有了前面的分區、分表,分庫的思路應該很好理解,就目前而言,我暫時沒有看到我們有進一步分庫的需求,線上業務都在公有雲,一般的性能增長需求可以先通過快速的單庫擴容實現。如果后續需要,可以先考慮將商品中心、用戶中心等強業務相關的子系統分離;業務量繼續增長時,還可以考慮更細粒度的,比如就訂單表而言,也可以將各個子表分離到不同的DB去。當然,這些操作對我們的應用開發提出更高的適配要求,業界也有成熟的如mycat等中間件方案。

2.4 讀寫分離和集群

讀寫分離和DB集群是互聯網架構常用的方案,我們也已經實現了一部分,這種方案主要是為了解決讀遠大於寫的情況,可以是一主一從、一主多從、多主多從等,本質上是將數據拷貝多份,由多個DB實例同時負載前端應用,以硬件成本換性能。該方案在提升讀的性能、HA等方面效果明顯,但並未真正解決訂單表單表過大的問題,這里就不展開說了。

2.5 熱點緩存

一個高性價比的緩存設計,適合更新少、讀取多的數據,比如商品,大量的用戶請求會拉取商品信息,真正下單的會少很多,緩存商品信息可以攔截下大量的重復的請求。至於訂單信息,每個用戶只能拉取自己的訂單,每個訂單被訪問到的次數是很少的,所以為訂單創建緩存的性價比就顯得很低。這里也不展開討論了。

2.6 用ES代替DB

參考Fylos推薦的這篇博文:http://www.sohu.com/a/327627159_315839
京東到家的訂單系統主要依賴ES集群來承擔訂單查詢的壓力,目前支撐10億文檔數和5億的日均查詢量。

3 訂單表優化方案

3.1 業務分析

這是訂單業務涉及的主要表的關系圖,圖中我整理了select/update涉及的where的主要條件字段(索引),其中Ux表示唯一索引,Ix表示普通索引:

  • 除order_info和order_sku表外,其他表都只需要一個字段索引,如order_product_attr表的所有查詢均通過order_id字段篩選。
  • order_sku表,只有在管理系統以product_id/sku_id為篩選條件時,才會用到I2/I3兩個索引,其他所有查詢均通過order_id篩選。
  • order_info表查詢涉及的篩選字段很多,上圖我沒列全。其中從前端傳入的用戶請求,都是以用戶為維度的,帶user_id。

目前我們的訂單表記錄數不到500萬,遠沒到需要分庫、建集群才能支撐性能的地步;而訂單的查詢頻率並不高,性能主要受單表數據量太大限制,所以建緩存的意義也不大;至於用ES來代替DB查詢,引入了新的數據節點和數據同步的需要,在分區和分表能解決問題的情況下,使用ES屬於過度優化,沒這必要。
所以接下來我們直接討論怎么分區、分表。此外,分區對業務代碼是無感知的,分表需要修改業務邏輯,所以推薦優先選擇分區。

序號 表名 記錄數
1 order_info 4507745
2 order_sku 4885235
3 order_product_attr 25450856
4 order_sku_epay 5772024
5 order_product_ext_info 2927387
6 order_product_set_info 14677
7 order_package_info 1139441
8 package_sku_info 213238

從上表來看,前4張表的記錄數比較大,也是大部分請求集中的表,需要優化。

3.2 分區方案(order_sku/order_product_attr/order_sku_epay)

分區思路:以order_id為分區鍵;以當前數據量為基礎,每個分區記錄數50萬以內。

表名 分區數 每個分區的平均記錄數
order_sku 16 30.5萬
order_product_attr 64 39.8萬
order_sku_epay 16 36.1萬

這個分區設計下,即使在我們的業務增長10倍之后,每個分區的記錄數在400萬左右,也能很好的支撐。當然,后面我們可能得考慮一些分表、分庫的設計了。

存在的問題:管理系統單獨以product_id/sku_id篩選時,無法鎖定分區。考慮到這個請求不是很多,可以走普通索引。

3.2 歷史表+分區方案(order_info)

order_info表的業務特點:

  • 用戶請求:請求參數都是帶user_id的,可以按user_id分區,支持查歷史數據
  • 定時任務,有支付超時回滾、自動確認收貨、支付中定時器(確認支付狀態)、發貨輪詢、每日結算、發貨失敗自動退費、訂單發貨超時自動標志、熱銷榜等。這些定時任務基本是針對未完成訂單或是近期已完成訂單的,但篩選條件有訂單狀態、支付時間等,不適合分區。可以創建歷史表,將非熱點數據移除。
  • 管理系統拉取訂單,一些涉及全表數據的操作,除了堆硬件分庫外,沒有很好的解決方案。

結合上述業務特點,可以將order_info拆分為主表order_info和歷史表order_info_his,同時order_info_his表可以按user_id分區,保證用戶端拉取的速度。

主表order_info:

  • 保留最近3個月的訂單
  • 數據量不大,暫時不需要分區。以后業務增長了,這個表可以考慮分區
  • 熱點數據 - 未結束的訂單都在這個表,適合用戶拉取未完成訂單、后台結算統計等熱點行為

歷史表order_info_his:

  • 主要是用戶拉取歷史訂單和管理系統拉取訂單列表,這里以用戶為主,以user_id建立分區

代碼邏輯變更:

  • 用戶拉待付款、待收貨頁面,只查主表
  • 用戶拉待發貨頁面,由於發貨超時時間是手動配置的,部分測試商品沒配置發貨超時時間,不排除正式商品也有沒配置的,所以這個頁面的數據需要主表和歷史表都拉取
  • 其他用戶拉取訂單列表的頁面:拉取兩張表
  • 定時器任務:需要分析下主表是否包含了全部數據
  • 管理系統:訂單管理頁面涉及的數據量很大,全量拉取的耗時比較長,所以可以特殊處理,分兩次拉,第1次拉取並展示主表的數據,但分頁區不顯示訂單總數,第2次再拉取歷史表數據進行匯總


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM