一、前言
做過電商開發的同學對訂單的業務應該不陌生,比如對一條訂單數據來說,通常會有一個類似于status的字
段來標識這個訂單的完整的生命周期,從存儲的數據來看,一張表只需要存儲這一條數據即可。
但是對于數據分析來看,為了跟蹤這個訂單的全生命周期的完整過程來說,這并不是一個很好的設計,假如說訂
單到已支付但未發貨,而且在未發貨這一步停留的時間很長,對于大數據分析場景來說,這就是一個重要的分析
場景,但對于mysql存儲的訂單表來說,這就有些冗余了。這也就是說,mysql在設計表的時候,是會充分考慮
冗余數據量帶來的性能問題。
二、拉鏈表業務背景
我們知道,Hive在實際工作中主要用于構建離線數據倉庫,定期的從各種數據源中同步采集數據到Hive中,
經過分層轉換提供數據給上層其他應用使用。
例如:有一個定時任務每天從MySQL中同步最新的訂單信息、用戶信息、店鋪信息等到數據倉庫中,從而進行訂單分析、用戶分析等。
如下圖所示,為一個數倉簡單的業務流程圖;

2.1 數據同步引發的問題
有下面這樣一張用戶表tb_user,有過開發經驗的同學對類似的訂單表應該不陌生,比如每次注冊完一個用戶后,該表中就會產生一條新
的數據,記錄了該用戶的id、手機號碼、用戶名、性別、地址等信息。

關于該表在業務中的具體使用場景如下:
- 每天都會有用戶注冊,產生新的用戶信息;
- 每天都需要將MySQL中的用戶數據同步到Hive數據倉庫中;
- 需要對用戶的信息做統計分析,例如統計新增用戶的個數、用戶性別分布、地區分布、運營商分布等指標;
數據同步的過程大概長下面這樣

比如說,在2021-01-01這一天,MySQL中有10條用戶信息;

然后通過中間程序(或其他方式)同步到下面的Hive表中了;

現在,假如在 2021-01-02 這一天,在前一天的基礎上,MySQL中新增了2條用戶注冊數據,并且其中有1條用戶數據發生更新,
- 新增兩條用戶數據011和012;
- 008的addr發生了更新,從gz更新為sh;

到了2021-01-03這天,Hive需要對2號的數據進行同步更新處理,此時問題來了:
新增的數據會直接加載到Hive表中,但是更新的數據如何存儲在Hive表中?


2.1.1 解決方案1
在Hive中用新的addr覆蓋008的老的addr,直接更新

這么做的優點是:實現最簡單,使用起來最方便,但缺點也是很明顯的,沒有歷史狀態,008的地址是1月2號在sh
,但是1月2號之前是在gz的,如果要查詢008的1月2號之前的addr就無法查詢,也不能使用sh代替;
2.1.2 解決方案2
每次數據改變,根據日期構建一份全量的快照表,每天一張表

這樣做的優點是:記錄了所有數據在不同時間的狀態, 缺點:冗余存儲了很多沒有發生變化的數據,導致存儲的數據量過大;
2.1.3 解決方案3
構建拉鏈表,通過時間標記發生變化的數據的每種狀態的時間周期,如下圖表中數據所示,
它大意就是,當一條數據中的關鍵業務標識字段發送了變化,將新增加一條數據,將這
條數據的過期時間設置的非常大,作為這條數據的邊界,同樣主鍵的數據再次過來的時
候,在新增的一條記錄中只需要記錄變化的字段即可;關于拉鏈表,下文將做詳細的講述;

三、拉鏈表設計與原理
3.1 功能與應用場景
拉鏈表專門用于解決在數據倉庫中數據發生變化如何實現數據存儲的問題。
拉鏈表的設計是將更新的數據進行狀態記錄,沒有發生更新的數據不進行狀態存儲,用于存儲所有
數據在不同時間上的所有狀態,通過時間進行標記每個狀態的生命周期,查詢時,根據需求可以
獲取指定時間范圍狀態的數據,默認用9999-12-31等最大值來表示最新狀態。
如下圖所示,記錄了某些訂單的完整生命周期;

3.2 實現步驟
用下面這張圖來說明其完整的實現過程

具體來說,操作步驟如下:
3.2.1 Step1
增量采集變化數據,放入增量表中。

3.2.2 Step2
將Hive中的拉鏈表與臨時表的數據進行合并,合并結果寫入臨時表。
3.2.3 Step3
將臨時表的數據覆蓋寫入拉鏈表中。
3.3 操作演示
準備一份原始數據,內容如下

3.3.1 創建一張表并加載數據
——創建拉鏈表createtabledw_zipper(useridstring,phonestring,nickstring,genderint,addrstring,starttimestring,endtimestring)rowformatdelimitedfieldsterminatedby'\t';——加載模擬數據loaddatalocalinpath'/usr/local/soft/selectdata/zipper。txt'intotabledw_zipper;
執行過程

檢查數據是否加載進去

3.3.2 模擬增量數據變化
下面為兩條新增的數據,以及一條變化的數據

創建一張增量表,并加載數據
createtableods_zipper_update(useridstring,phonestring,nickstring,genderint,addrstring,starttimestring,endtimestring)rowformatdelimitedfieldsterminatedby'\t';loaddatalocalinpath'/usr/local/soft/selectdata/update。txt'intotableods_zipper_update;
執行過程

檢查數據是否加載成功

3.3.3 合并數據
創建一張臨時表
createtabletmp_zipper(useridstring,phonestring,nickstring,genderint,addrstring,starttimestring,endtimestring)rowformatdelimitedfieldsterminatedby'\t';
執行過程

合并拉鏈表與增量表
insertoverwritetabletmp_zipperselectuserid,phone,nick,gender,addr,starttime,endtimefromods_zipper_updateunionall——查詢原來拉鏈表的所有數據,并將這次需要更新的數據的endTime更改為更新值的startTimeselecta。userid,a。phone,a。nick,a。gender,a。addr,a。starttime,——如果這條數據沒有更新或者這條數據不是要更改的數據,就保留原來的值,否則就改為新數據的開始時間-1if(b。useridisnullora。endtime<'9999-12-31',a。endtime,date_sub(b。starttime,1))asendtimefromdw_zipperaleftjoinods_zipper_updatebona。userid=b。userid;
執行上面的sql

覆蓋拉鏈表
insert overwrite table dw_zipper select * from tmp_zipper;
執行過程

執行完成后,檢查拉鏈表的數據,可以看到新增了2條數據,同時對于相同的那條數據做了時間上的更新;

|