表格存儲(TableStore)新功能Stream初探
阿裏雲自研PB級nosql數據庫TableStore近期發布了新功能Stream,也就是增量通道,可以讓用戶實時的獲取數據庫中的增刪改操作。很多使用TableStore的用戶會定期把數據導入各類計算平台做數據的離線分析,以前的做法是使用DATAX或者使用TableStore的SDK定期拉取數據。之前我們隻能采用全量拉取的辦法,定期的全量拉取勢必會帶來很多不必要的開銷,並且也失去了新增數據實時處理的可能。那有了Stream增量通道後,之前的這些痛點都會被迎刃而解。
這個功能究竟怎麼使用,又可以用在哪裏呢?下麵我就帶大家初探TableStore的Stream功能。大家也可以先閱讀下Stream的原理
產品功能
Stream功能和其他表格存儲的很多功能一樣,是用戶表的一個屬性。用戶在創建表的時候可以指定是否開啟Stream功能。用戶也可以通過UpdateTable操作在後續需要使用Stream的時候開啟。當用戶開啟Stream後,用戶的修改記錄在生命周期內(周期長短由用戶開啟Stream的時候指定,目前默認最大是一天,如有更長周期需要可以在官網提工單)會被一直保留。
除了表操作以外,Stream的API具體有以下:
- ListStreams 獲取當前表的 Stream 信息,例如 StreamID。具體請參見ListStreams
- DescribeStream 獲取當前表增量數據的分區信息,熟悉表格存儲特性的同學會知道表格存儲會自動根據用戶指定的分片鍵做分區來實現負載均衡。而我們的增量數據也是通過分區來進行組織,所以消費增量數據之前需要了解當前的分區信息,也就是Stream中的Shard信息。具體參見DescribeStream
- GetShardIterator 獲取具體某個分區的讀取iterator。這個iterator可以簡單理解為一個偏移量標記我們可以從哪裏開始消費增量數據。具體參見GetShardIterator
- GetStreamRecord 拉取增量數據,每次拉取結束後,會更新iterator用來下次拉取。如果返回數據為空表示當前尚未讀取到新數據。如果返回null說明這個分區已經不存在沒有後續的增量數據了。GetStreamRecord
如何理解TableStore的增量數據
介紹了Stream API,可能還不是很直觀理解TableStore的Stream數據是如何組織的,下麵就以用戶軌跡為例來介紹如何使用。如何基於表格存儲實現用戶軌跡數據的存儲,可以參考如何高效存儲海量GPS數據。
在這裏,我們假設你使用如下的表結構存儲你的海量軌跡數據,
主鍵順序 | 名稱 | 類型 | 值 | 備注 |
---|---|---|---|---|
1 | partition_key | string | md5(user_id)前四位 | 為了負載均衡 |
2 | user_id | string/int | 用戶id | 可以是字符串也可以是長整型數字 |
3 | task_id | string/int | 此次軌跡圖的id | 可以使字符串也可以是長整型數字 |
4 | timestamp | int | 時間戳 | 使用長整型,64位,足夠保存毫秒級別的時間戳 |
假如你有原始數據如下:
2017/5/20 10:10:10的時候小王在杭州虎跑路,開著私家寶馬車,速度25km/h,當時風速2m/s,溫度20度,已經開了8公裏。
在表格存儲中存儲的是(11列);
part_key | user_id | task_id | timestamp | longitude | latitude | brand | speed | wind_speed | temperature | distance |
---|---|---|---|---|---|---|---|---|---|---|
04fc | 000001 | 001 | 1495246210 | 120.1516525097 | 30.2583277934 | BMW | 25 | 2 | 20 | 8000 |
當用戶的位置不斷發生變化,我們會產生一係列類似上麵的軌跡數據,例如我們的粒度可以是10秒一個軌跡點。這樣在一段時間內,我們可以積累海量的軌跡數據。那對於業務方,往往要做一些運營分析。
分析話題1:統計過去10分鍾內是否有一個區域有駕駛熱點,會帶來交通擁堵。發現潛在擁堵點後,提前做一些車流疏散。
分析話題2:又或者我們希望在晚飯時間點,統計一下來某個商圈吃飯的客戶都是從哪些地方開車過來的,日後可以在輻射區域內做一些精準推廣。
這類問題的共同點是需要在這張軌跡表中獲取一個時間段內新寫入的數據,針對我們的表結構設計,如果沒有增量通道的時候,我們能做的就是拿到所有的用戶id和taskid進行時間段內的getrange讀,這樣如果同時的軌跡用戶較多,會帶來大量的getrange並發訪問,而且我們還需要一張額外的表記錄用戶和軌跡id的關係。如果我們修改表結構,把時間作為第一主鍵,又會帶來嚴重的數據寫入尾部熱點,數據分布不均勻等問題。
那麼我們的架構就由以前的 加上大量的range讀變為了下圖的基於增量獲取:
Stream 的數據返回格式
當你使用我們的Stream APi讀取增量數據的時候上麵的數據會以下麵的形式返回,我們以Go Sdk為例返回如下格式的Stream record。
record 0: {"Type":"PutRow", "PrimaryKey":{[{"Name": "pk1", "Value": "04fc"} {"Name": "pk2", "Value": "000001"} {"Name": "pk3", "Value": "001"} {"Name": "pk4", "Value": "%!s(int64=1495246210)"}]}, "Info":{"Epoch":0, "Timestamp": 1503555067832234, "RowIndex": 1}, "Columns":[{"Name":"longitude", "Type":"Put", "Timestamp":1503555067833, "Value":1e+02} {"Name":"latitude", "Type":"Put", "Timestamp":1503555067833, "Value":30.2583277934} {"Name":"brand", "Type":"Put", "Timestamp":1503555067833, "Value":BMW} {"Name":"speed", "Type":"Put", "Timestamp":1503555067833, "Value":25} {"Name":"wind_speed", "Type":"Put", "Timestamp":1503555067833, "Value":2} {"Name":"temperature", "Type":"Put", "Timestamp":1503555067833, "Value":20} {"Name":"distance", "Type":"Put", "Timestamp":1503555067833, "Value":8000}]}
record 1: {"Type":"PutRow", "PrimaryKey":{[{"Name": "pk1", "Value": "04fc"} {"Name": "pk2", "Value": "000001"} {"Name": "pk3", "Value": "001"} {"Name": "pk4", "Value": "%!s(int64=1495246310)"}]}, "Info":{"Epoch":0, "Timestamp": 1503555068082609, "RowIndex": 1}, "Columns":[{"Name":"longitude", "Type":"Put", "Timestamp":1503555068083, "Value":1e+02} {"Name":"latitude", "Type":"Put", "Timestamp":1503555068083, "Value":30.2583277934} {"Name":"brand", "Type":"Put", "Timestamp":1503555068083, "Value":BMW} {"Name":"speed", "Type":"Put", "Timestamp":1503555068083, "Value":25} {"Name":"wind_speed", "Type":"Put", "Timestamp":1503555068083, "Value":2} {"Name":"temperature", "Type":"Put", "Timestamp":1503555068083, "Value":20} {"Name":"distance", "Type":"Put", "Timestamp":1503555068083, "Value":8001}]}
我們可以發現,表格的一次操作對應Stream的一條記錄,記錄中會涵蓋這次操作的類型,操作的主鍵以及修改列的內容。有了這些數據我們可以方便做以下事情:
- 將數據做清洗寫入另一張TableStore表
- 將數據寫入流計算平台,做實時計算分析
- 將數據寫入MaxCompute做進行分析
下麵羅列下我們目前有如下幾種方式可以讀區表格存儲的增量數據:
- SDK直接訪問,目前我們的Java SDK和Go SDK已經支持Stream的Api,具體的使用可以參考Java Stream 示例和Go Stream 示例
- DATAX 離線讀取stream數據到odps,具體使用參考DATAX 訪問TableStore增量
- 基於Stream Client,用戶自己開發實時數據通道將數據導出至不同的數據源。使用可以參考Stream Client使用
- Stream對接FC,通過FC觸發數據處理邏輯。 即將發布,敬請期待
下麵我們就用外賣訂單係統為例再說明下Stream如何可以方便我們簡化,高效的實現我們的應用。
外賣訂單係統
場景描述
現在外賣行業非常火熱,幾家大廠都在角逐這個領域。而外賣也確實給我們的日常生活帶來的很多便利,那如何基於表格存儲打造一款高效的外賣應用呢,下麵我們來詳細介紹下。
係統特點
很多外賣會在不同時間有明顯的波峰波穀,例如食品外賣,三餐點和夜宵時間點會有明顯的波峰。那麼表格存儲這類海量高性能彈性計費的數據庫產品就非常適合。除此之外,外賣係統還要基於一個區域內所有用戶的下單情況做一個做優化的配送,實現效率最優,那麼這樣的係統我們如何設計表結構呢。
表結構設計
表1 訂單表
主鍵順序 | 名稱 | 類型 | 值 | 備注 |
---|---|---|---|---|
1 | partition_key | string | md5(user_id)前四位 | 為了負載均衡 |
2 | user_id | string/int | 用戶id | 可以是字符串也可以是長整型數字 |
3 | timestamp | int | 時間戳 | 使用長整型,64位,足夠保存毫秒級別的時間戳 |
4 | order_id | string/int | 訂單Id | 可以使字符串也可以是長整型數字 |
表2 配送表
主鍵順序 | 名稱 | 類型 | 值 | 備注 |
---|---|---|---|---|
1 | partition_key | string | md5(user_id)前四位 | 為了負載均衡 |
2 | user_id | string/int | 配送員id | 可以是字符串也可以是長整型數字 |
3 | delivery_id | int | 配送序列 | 使用長整型,基於表格存儲主鍵自增列 |
數據存儲示例
設計好表結構後,我們看下具體如何存儲:
訂單表原始數據是:
2017/5/20 10:12:20小王下了訂單,訂單號10005,購買了兩串烤肉和一杯咖啡,總共支付來51元,收獲地址是西湖區XXX路XX號。
配送表原始數據是:
2017/5/20 10:12:20配送員小李收到配送訂單信息,訂單號10005,購買了兩串烤肉和一杯咖啡,收獲地址是西湖區XXX路XX號。
part_key | user_id | timestamp | order_id | merchant_id | commodity | price | address | payment_type | status |
---|---|---|---|---|---|---|---|---|---|
01f3 | 000001 | 1495246210 | 10005 | 黑暗料理 | 2烤肉,1咖啡 | 51 | 西湖區XXX路XX號 | alipay | 等待配送 |
part_key | user_id | delivery_id | order_id | merchant_id | commodity | price | address | payment_type | status |
---|---|---|---|---|---|---|---|---|---|
01f3 | 000001 | 1495249230 | 10005 | 黑暗料理 | 2烤肉,1咖啡 | 51 | 西湖區XXX路XX號 | alipay | 配送中 |
主鍵
訂單表
- part_key:第一個主鍵,分區建,主要是為了負載均衡,保證數據可以均勻分布在所有機器上,提高並發度和性能。如果業務主鍵user_id可以保證均勻分布,那麼可以不需要這個主鍵。
- user_id:第二個主鍵,用戶ID,可以是字符串也可以是數字,唯一標識一個用戶。
- timestamp:第三個主鍵,時間戳,表示某一個時刻,單位可以是秒或者毫秒,用來表示用戶訂單的時間戳。在這裏放置時間,是因為係統往往需要查詢某個用戶一段時間內的所有訂單信息。
- order_id:第四個主鍵,訂單號。
- 至此,上述四個主鍵可以唯一確定某一個用戶在某一個時間點下的一個訂單。
配送表
- part_key:第一個主鍵,分區建,主要是為了負載均衡,保證數據可以均勻分布在所有機器上,提高並發度和性能。如果業務主鍵user_id可以保證均勻分布,那麼可以不需要這個主鍵。
- user_id:第二個主鍵,配送員ID,可以是字符串也可以是數字,唯一標識一個用戶。
- delivery_id:第三個主鍵,配送號,注意不是用戶的訂單號,這一列使用自增列,配送員的客戶端可以根據這個id拉去更新的配送信息。
- 至此,上述三個主鍵可以獲取一個配送訂單的詳細信息。
屬性列
訂單表
- merchant_id :商家id
- commodity:商品內容。
- price:訂單價格。
- address: 配送地址
- payment_type:用戶支付類型。
- status:訂單的狀態。
配送表
- order_id :訂單id
- commodity:商品內容。
- price:訂單價格。
- address: 配送地址
- payment_type:用戶支付類型。
- status:訂單的狀態。
由於表格存儲的分區鍵可以在數據訪問增加時進行分裂,當我們有百萬用戶同時在高峰期下單時我們可以分裂出較多的分區輕鬆應對每秒數十萬甚至數百萬的新增訂單。有了這樣的一個訂單存儲係統後,如何銜接我們的派單係統呢,這時候我們就可以使用增量功能,把近期的訂單信息導入排單係統進行線路優化計算。前麵我們也提到了外賣訂單的伸縮特性,所以我們推薦使用函數計算進行訂單的派送計算,我們表格存儲Stream對接函數計算的功能也即將上線,屆時我們是需要一些配置就可以打通表格存儲和函數計算這兩款全托管完全彈性計費的存儲,計算產品。讓我們的外賣訂單飛的再快一點吧。
最後更新:2017-08-29 21:32:20