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

2.1 數(shù)據(jù)同步引發(fā)的問(wèn)題
有下面這樣一張用戶(hù)表tb_user,有過(guò)開(kāi)發(fā)經(jīng)驗(yàn)的同學(xué)對(duì)類(lèi)似的訂單表應(yīng)該不陌生,比如每次注冊(cè)完一個(gè)用戶(hù)后,該表中就會(huì)產(chǎn)生一條新
的數(shù)據(jù),記錄了該用戶(hù)的id、手機(jī)號(hào)碼、用戶(hù)名、性別、地址等信息。

關(guān)于該表在業(yè)務(wù)中的具體使用場(chǎng)景如下:
- 每天都會(huì)有用戶(hù)注冊(cè),產(chǎn)生新的用戶(hù)信息;
- 每天都需要將MySQL中的用戶(hù)數(shù)據(jù)同步到Hive數(shù)據(jù)倉(cāng)庫(kù)中;
- 需要對(duì)用戶(hù)的信息做統(tǒng)計(jì)分析,例如統(tǒng)計(jì)新增用戶(hù)的個(gè)數(shù)、用戶(hù)性別分布、地區(qū)分布、運(yùn)營(yíng)商分布等指標(biāo);
數(shù)據(jù)同步的過(guò)程大概長(zhǎng)下面這樣

比如說(shuō),在2021-01-01這一天,MySQL中有10條用戶(hù)信息;

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

現(xiàn)在,假如在 2021-01-02 這一天,在前一天的基礎(chǔ)上,MySQL中新增了2條用戶(hù)注冊(cè)數(shù)據(jù),并且其中有1條用戶(hù)數(shù)據(jù)發(fā)生更新,
- 新增兩條用戶(hù)數(shù)據(jù)011和012;
- 008的addr發(fā)生了更新,從gz更新為sh;

到了2021-01-03這天,Hive需要對(duì)2號(hào)的數(shù)據(jù)進(jìn)行同步更新處理,此時(shí)問(wèn)題來(lái)了:
新增的數(shù)據(jù)會(huì)直接加載到Hive表中,但是更新的數(shù)據(jù)如何存儲(chǔ)在Hive表中?


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

這么做的優(yōu)點(diǎn)是:實(shí)現(xiàn)最簡(jiǎn)單,使用起來(lái)最方便,但缺點(diǎn)也是很明顯的,沒(méi)有歷史狀態(tài),008的地址是1月2號(hào)在sh
,但是1月2號(hào)之前是在gz的,如果要查詢(xún)008的1月2號(hào)之前的addr就無(wú)法查詢(xún),也不能使用sh代替;
2.1.2 解決方案2
每次數(shù)據(jù)改變,根據(jù)日期構(gòu)建一份全量的快照表,每天一張表

這樣做的優(yōu)點(diǎn)是:記錄了所有數(shù)據(jù)在不同時(shí)間的狀態(tài), 缺點(diǎn):冗余存儲(chǔ)了很多沒(méi)有發(fā)生變化的數(shù)據(jù),導(dǎo)致存儲(chǔ)的數(shù)據(jù)量過(guò)大;
2.1.3 解決方案3
構(gòu)建拉鏈表,通過(guò)時(shí)間標(biāo)記發(fā)生變化的數(shù)據(jù)的每種狀態(tài)的時(shí)間周期,如下圖表中數(shù)據(jù)所示,
它大意就是,當(dāng)一條數(shù)據(jù)中的關(guān)鍵業(yè)務(wù)標(biāo)識(shí)字段發(fā)送了變化,將新增加一條數(shù)據(jù),將這
條數(shù)據(jù)的過(guò)期時(shí)間設(shè)置的非常大,作為這條數(shù)據(jù)的邊界,同樣主鍵的數(shù)據(jù)再次過(guò)來(lái)的時(shí)
候,在新增的一條記錄中只需要記錄變化的字段即可;關(guān)于拉鏈表,下文將做詳細(xì)的講述;

三、拉鏈表設(shè)計(jì)與原理
3.1 功能與應(yīng)用場(chǎng)景
拉鏈表專(zhuān)門(mén)用于解決在數(shù)據(jù)倉(cāng)庫(kù)中數(shù)據(jù)發(fā)生變化如何實(shí)現(xiàn)數(shù)據(jù)存儲(chǔ)的問(wèn)題。
拉鏈表的設(shè)計(jì)是將更新的數(shù)據(jù)進(jìn)行狀態(tài)記錄,沒(méi)有發(fā)生更新的數(shù)據(jù)不進(jìn)行狀態(tài)存儲(chǔ),用于存儲(chǔ)所有
數(shù)據(jù)在不同時(shí)間上的所有狀態(tài),通過(guò)時(shí)間進(jìn)行標(biāo)記每個(gè)狀態(tài)的生命周期,查詢(xún)時(shí),根據(jù)需求可以
獲取指定時(shí)間范圍狀態(tài)的數(shù)據(jù),默認(rèn)用9999-12-31等最大值來(lái)表示最新?tīng)顟B(tài)。
如下圖所示,記錄了某些訂單的完整生命周期;

3.2 實(shí)現(xiàn)步驟
用下面這張圖來(lái)說(shuō)明其完整的實(shí)現(xiàn)過(guò)程

具體來(lái)說(shuō),操作步驟如下:
3.2.1 Step1
增量采集變化數(shù)據(jù),放入增量表中。

3.2.2 Step2
將Hive中的拉鏈表與臨時(shí)表的數(shù)據(jù)進(jìn)行合并,合并結(jié)果寫(xiě)入臨時(shí)表。
3.2.3 Step3
將臨時(shí)表的數(shù)據(jù)覆蓋寫(xiě)入拉鏈表中。
3.3 操作演示
準(zhǔn)備一份原始數(shù)據(jù),內(nèi)容如下

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

檢查數(shù)據(jù)是否加載進(jìn)去

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

創(chuàng)建一張?jiān)隽勘,并加載數(shù)據(jù)
createtableods_zipper_update(useridstring,phonestring,nickstring,genderint,addrstring,starttimestring,endtimestring)rowformatdelimitedfieldsterminatedby'\t';loaddatalocalinpath'/usr/local/soft/selectdata/update。txt'intotableods_zipper_update;
執(zhí)行過(guò)程

檢查數(shù)據(jù)是否加載成功

3.3.3 合并數(shù)據(jù)
創(chuàng)建一張臨時(shí)表
createtabletmp_zipper(useridstring,phonestring,nickstring,genderint,addrstring,starttimestring,endtimestring)rowformatdelimitedfieldsterminatedby'\t';
執(zhí)行過(guò)程

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

覆蓋拉鏈表
insert overwrite table dw_zipper select * from tmp_zipper;
執(zhí)行過(guò)程

執(zhí)行完成后,檢查拉鏈表的數(shù)據(jù),可以看到新增了2條數(shù)據(jù),同時(shí)對(duì)于相同的那條數(shù)據(jù)做了時(shí)間上的更新;

|