HBase之六:HBase的RowKey設計


數據模型

 我們可以將一個表想象成一個大的映射關系,通過行健、行健+時間戳或行鍵+列(列族:列修飾符),就可以定位特定數據,Hbase是稀疏存儲數據的,因此某些列可以是空白的,

 

Row Key

Time Stamp

Column Family:c1

Column Family:c2

r1

t7

c1:1

value1-1/1

 

 

t6

c1:2

value1-1/2

 

 

t5

c1:3

value1-1/3

 

 

t4

 

 

c2:1

value1-2/1

t3

 

 

c2:2

value1-2/2

t2

t2

c1:1

value2-1/1

 

 

t1

 

 

c2:1

value2-1/1

從上表可以看出,test表有r1和r2兩行數據,並且c1和c2兩個列族,在r1中,列族c1有三條數據,列族c2有兩條數據;在r2中,列族c1有一條數據, 列族c2有一條數據,每一條數據對應的時間戳都用數字來表示,編號越大表示數據越舊,反而表示數據越新。

 

3:物理視圖

    雖然從概念視圖來看每個表格是由很多行組成的,但是在物理存儲上面,它是按照列來保存的。

   

Row Key

Time Stamp

Column Family:c1

r1

t7

c1:1

value1-1/1

t6

c1:2

value1-1/2

t5

c1:3

value1-1/3

             表:HBase數據的物理視圖(1)

 

 

Row Key

Time Stamp

Column Family:c2

r1

t4

c2:1

value1-2/1

t3

c2:2

value1-2/2

          表:HBase數據的物理視圖(2)

 

需要注意的是,在概念視圖上面有些列是空白的,這樣的列實際上並不會被存儲,當請求這些空白的單元格時,會返回null值。如果在查詢的時候不

提供時間戳,那么會返回距離現在最近的那一個版本的數據,因為在存儲的時候,數據會按照時間戳來排序。

 

通過shell操作hbase 會更清楚結構

 

這里我們用一個學生成績表作為例子,對HBase的基本操作和基本概念進行講解:

 

下面是學生的成績表:

name grad      course:math   course:art

Tom    1                87                    97

 

Jerry   2            100                  80

 

        這里grad對於表來說是一個列,course對於表來說是一個列族,這個列族由兩個列組成:math和art,當然我們可以根據我們的需要在course中建立更多的列族,如computer,physics等相應的列添加入course列族.

 

        有了上面的想法和需求,我們就可以在HBase中建立相應的數據表啦!

 

1, 建立一個表格 scores 具有兩個列族grad 和courese

 

hbase(main):002:0> create 'scores', 'grade', 'course'

 

0 row(s) in 4.1610 seconds

 

2,查看當先HBase中具有哪些表

 

hbase(main):003:0> list

 

scores

 

1 row(s) in 0.0210 seconds

 

3,查看表的構造

 

hbase(main):004:0> describe 'scores'

 

{NAME => 'scores', IS_ROOT => 'false', IS_META => 'false', FAMILIES => [{NAME => 'course', BLOOMFILTER => 'false', IN_MEMORY => 'false', LENGTH => '2147483647', BLOCKCACHE => 'false', VERSIONS => '3', TTL => '-1', COMPRESSION => 'NONE'}, {NAME => 'grade', BLOOMFILTER => 'false', IN_MEMORY => 'false', LENGTH => '2147483647', BLOCKCACHE => 'false', VERSIONS => '3', TTL => '-1', COMPRESSION => 'NONE'}]}

 

1 row(s) in 0.0130 seconds

 

4, 加入一行數據,行名稱為 Tom 列族grad的列名為”” 值位1

 

hbase(main):005:0> put 'scores', 'Tom', 'grade:', '1'

 

0 row(s) in 0.0070 seconds

 

5,給Tom這一行的數據的列族添加一列 <math,87>

 

hbase(main):006:0> put 'scores', 'Tom', 'course:math', '87'

 

0 row(s) in 0.0040 seconds

 

6,給Tom這一行的數據的列族添加一列 <art,97>

 

hbase(main):007:0> put 'scores', 'Tom', 'course:art', '97'

 

0 row(s) in 0.0030 seconds

 

7, 加入一行數據,行名稱為 Jerry 列族grad的列名為”” 值位2

 

hbase(main):008:0> put 'scores', 'Jerry', 'grade:', '2'

 

0 row(s) in 0.0040 seconds

 

8,給Jerry這一行的數據的列族添加一列 <math,100>

 

hbase(main):009:0> put 'scores', 'Jerry', 'course:math', '100'

 

0 row(s) in 0.0030 seconds

 

9,給Jerry這一行的數據的列族添加一列 <art,80>

 

hbase(main):010:0> put 'scores', 'Jerry', 'course:art', '80'

 

0 row(s) in 0.0050 seconds

 

10,查看scores表中Tom的相關數據

 

hbase(main):011:0> get 'scores', 'Tom'

 

COLUMN                       CELL

 

course:art                  timestamp=1224726394286, value=97

 

course:math                 timestamp=1224726377027, value=87

 

grade:                      timestamp=1224726360727, value=1

 

3 row(s) in 0.0070 seconds

 

11,查看scores表中所有數據

 

hbase(main):012:0> scan 'scores'

 

ROW                          COLUMN+CELL

 

Tom                         column=course:art, timestamp=1224726394286, value=97

 

Tom                         column=course:math, timestamp=1224726377027, value=87

 

Tom                         column=grade:, timestamp=1224726360727, value=1

 

Jerry                        column=course:art, timestamp=1224726424967, value=80

 

Jerry                        column=course:math, timestamp=1224726416145, value=100

 

Jerry                        column=grade:, timestamp=1224726404965, value=2

 

6 row(s) in 0.0410 seconds

 

12,查看scores表中所有數據courses列族的所有數據

 

hbase(main):013:0> scan 'scores', ['course:']

 

ROW                          COLUMN+CELL

 

Tom                         column=course:art, timestamp=1224726394286, value=97

 

Tom                         column=course:math, timestamp=1224726377027, value=87

 

Jerry                        column=course:art, timestamp=1224726424967, value=80

 

Jerry                        column=course:math, timestamp=1224726416145, value=100

 

4 row(s) in 0.0200 seconds

 

        上面就是HBase的基本shell操作的一個例子,可以看出,hbase的shell還是比較簡單易用的,從中也可以看出HBase shell缺少很多傳統sql中的一些類似於like等相關操作,當然,HBase作為BigTable的一個開源實現,而BigTable是作為 google業務的支持模型,很多sql語句中的一些東西可能還真的不需要.

1 概述

HBase是一個分布式的、面向列的數據庫,它和一般關系型數據庫的最大區別是:HBase很適合於存儲非結構化的數據,還有就是它基於列的而不是基於行的模式。

既然HBase是采用KeyValue的列存儲,那Rowkey就是KeyValue的Key了,表示唯一一行。Rowkey也是一段二進制碼流,最大長度為64KB,內容可以由使用的用戶自定義。數據加載時,一般也是根據Rowkey的二進制序由小到大進行的。

HBase是根據Rowkey來進行檢索的,系統通過找到某個Rowkey (或者某個 Rowkey 范圍)所在的Region,然后將查詢數據的請求路由到該Region獲取數據。HBase的檢索支持3種方式:

(1) 通過單個Rowkey訪問,即按照某個Rowkey鍵值進行get操作,這樣獲取唯一一條記錄;

(2) 通過Rowkey的range進行scan,即通過設置startRowKey和endRowKey,在這個范圍內進行掃描。這樣可以按指定的條件獲取一批記錄;

(3) 全表掃描,即直接掃描整張表中所有行記錄。

HBASE按單個Rowkey檢索的效率是很高的,耗時在1毫秒以下,每秒鍾可獲取1000~2000條記錄,不過非key列的查詢很慢。

 

Table中Family和Qualifier的關系與區別

就像用MySQL一樣,我們要做的是表設計,MySQL中的表,行,列的在HBase已經有所區別了,在HBase中主要是TableFamilyQualifier,這三個概念。Table可以直接理解為表,而Family和Qualifier其實都可以理解為列,一個Family下面可以有多個Qualifier,所以可以簡單的理解為,HBase中的列是二級列,也就是說Family是第一級列,Qualifier是第二級列。兩個是父子關系。

測試發現:

在實際應用場景中,對於單column qualifier和多column qualifier兩種情況,

如果value長度越長,row key長度越短,字段數(column qualifier數)越少,前者和后者在實際傳輸數據量上會相差小些;反之則相差較大。

如果采用多column qualifier的方式存儲,且客戶端采取批量寫入的方式,則可以根據實際情況,適當增大客戶端的write buffer大小,以便能夠提高客戶端的寫入吞吐量。

 

從性能的角度談table中family和qualifier的設置
  對於傳統關系型數據庫中的一張table,在業務轉換到hbase上建模時,從性能的角度應該如何設置family和qualifier呢?
  最極端的,①每一列都設置成一個family,②一個表僅有一個family,所有列都是其中的一個qualifier,那么有什么區別呢?


  從讀的方面考慮:
  family越多,那么獲取每一個cell數據的優勢越明顯,因為io和網絡都減少了。
  如果只有一個family,那么每一次讀都會讀取當前rowkey的所有數據,網絡和io上會有一些損失。
  當然如果要獲取的是固定的幾列數據,那么把這幾列寫到一個family中比分別設置family要更好,因為只需一次請求就能拿回所有數據。

  從寫的角度考慮:
  首先,內存方面來說,對於一個Region,會為每一個表的每一個Family分配一個Store,而每一個Store,都會分配一個MemStore,所以更多的family會消耗更多的內存。
  其次,從flush和compaction方面說,目前版本的hbase,在flush和compaction都是以region為單位的,也就是說當一個family達到flush條件時,該region的所有family所屬的memstore都會flush一次,即使memstore中只有很少的數據也會觸發flush而生成小文件。這樣就增加了compaction發生的機率,而compaction也是以region為單位的,這樣就很容易發生compaction風暴從而降低系統的整體吞吐量。
  第三,從split方面考慮,由於hfile是以family為單位的,因此對於多個family來說,數據被分散到了更多的hfile中,減小了split發生的機率。這是把雙刃劍。更少的split會導致該region的體積比較大,由於balance是以region的數目而不是大小為單位來進行的,因此可能會導致balance失效。而從好的方面來說,更少的split會讓系統提供更加穩定的在線服務。而壞處我們可以通過在請求的低谷時間進行人工的split和balance來避免掉。
     因此對於寫比較多的系統,如果是離線應該,我們盡量只用一個family好了,但如果是在線應用,那還是應該根據應用的情況合理地分配family。

 

首先,不同的family是在同一個region下面。而每一個family都會分配一個memstore,所以更多的family會消耗更多的內存。
其次,目前版本的hbase,在flush和compaction都是以region為單位的,也就是說當一個family達到flush條件時,該region的所有family所屬的memstore都會flush一次,即使memstore中只有很少的數據也會觸發flush而生成小文件。這樣就增加了compaction發生的機率,而compaction也是以region為單位的,這樣就很容易發生compaction風暴從而降低系統的整體吞吐量。
第三,由於hfile是以family為單位的,因此對於多個family來說,數據被分散到了更多的hfile中,減小了split發生的機率。這是把雙刃劍。更少的split會導致該region的體積比較大,由於balance是以region的數目而不是大小為單位來進行的,因此可能會導致balance失效。而從好的方面來說,更少的split會讓系統提供更加穩定的在線服務。
上述第三點的好處對於在線應用來說是明顯的,而壞處我們可以通過在請求的低谷時間進行人工的split和balance來避免掉。
因此對於寫比較多的系統,如果是離線應該,我們盡量只用一個family好了,但如果是在線應用,那還是應該根據應用的情況合理地分配family。

2 HBase的RowKey設計

2.1 設計原則

2.1.1 Rowkey長度原則

Rowkey是一個二進制碼流,Rowkey的長度被很多開發者建議說設計在10~100個字節,不過建議是越短越好,不要超過16個字節。

原因如下:

(1)數據的持久化文件HFile中是按照KeyValue存儲的,如果Rowkey過長比如100個字節,1000萬列數據光Rowkey就要占用100*1000萬=10億個字節,將近1G數據,這會極大影響HFile的存儲效率;

(2)MemStore將緩存部分數據到內存,如果Rowkey字段過長內存的有效利用率會降低,系統將無法緩存更多的數據,這會降低檢索效率。因此Rowkey的字節長度越短越好。

(3)目前操作系統是都是64位系統,內存8字節對齊。控制在16個字節,8字節的整數倍利用操作系統的最佳特性。

2.1.2 Rowkey散列原則

如果Rowkey是按時間戳的方式遞增,不要將時間放在二進制碼的前面,建議將Rowkey的高位作為散列字段,由程序循環生成,低位放時間字段,這樣將提高數據均衡分布在每個Regionserver實現負載均衡的幾率。如果沒有散列字段,首字段直接是時間信息將產生所有新數據都在一個 RegionServer上堆積的熱點現象,這樣在做數據檢索的時候負載將會集中在個別RegionServer,降低查詢效率。

2.1.3 Rowkey唯一原則

必須在設計上保證其唯一性。

2.2 應用場景

基於Rowkey的上述3個原則,應對不同應用場景有不同的Rowkey設計建議。

2.2.1 針對事務數據Rowkey設計

事務數據是帶時間屬性的,建議將時間信息存入到Rowkey中,這有助於提示查詢檢索速度。對於事務數據建議缺省就按天為數據建表,這樣設計的好處是多方面的。按天分表后,時間信息就可以去掉日期部分只保留小時分鍾毫秒,這樣4個字節即可搞定。加上散列字段2個字節一共6個字節即可組成唯一 Rowkey。如下圖所示:

事務數據Rowkey設計
第0字節 第1字節 第2字節 第3字節 第4字節 第5字節
散列字段 時間字段(毫秒) 擴展字段
0~65535(0x0000~0xFFFF) 0~86399999(0x00000000~0x05265BFF)  

這樣的設計從操作系統內存管理層面無法節省開銷,因為64位操作系統是必須8字節對齊。但是對於持久化存儲中Rowkey部分可以節省25%的開銷。也許有人要問為什么不將時間字段以主機字節序保存,這樣它也可以作為散列字段了。這是因為時間范圍內的數據還是盡量保證連續,相同時間范圍內的數據查找的概率很大,對查詢檢索有好的效果,因此使用獨立的散列字段效果更好,對於某些應用,我們可以考慮利用散列字段全部或者部分來存儲某些數據的字段信息,只要保證相同散列值在同一時間(毫秒)唯一。

2.2.2 針對統計數據的Rowkey設計

統計數據也是帶時間屬性的,統計數據最小單位只會到分鍾(到秒預統計就沒意義了)。同時對於統計數據我們也缺省采用按天數據分表,這樣設計的好處無需多說。按天分表后,時間信息只需要保留小時分鍾,那么0~1400只需占用兩個字節即可保存時間信息。由於統計數據某些維度數量非常龐大,因此需要4個字節作為序列字段,因此將散列字段同時作為序列字段使用也是6個字節組成唯一Rowkey。如下圖所示:

統計數據Rowkey設計
第0字節 第1字節 第2字節 第3字節 第4字節 第5字節
散列字段(序列字段) 時間字段(分鍾) 擴展字段
0x00000000~0xFFFFFFFF) 0~1439(0x0000~0x059F)  

同樣這樣的設計從操作系統內存管理層面無法節省開銷,因為64位操作系統是必須8字節對齊。但是對於持久化存儲中Rowkey部分可以節省25%的開銷。預統計數據可能涉及到多次反復的重計算要求,需確保作廢的數據能有效刪除,同時不能影響散列的均衡效果,因此要特殊處理。

2.2.3 針對通用數據的Rowkey設計

通用數據采用自增序列作為唯一主鍵,用戶可以選擇按天建分表也可以選擇單表模式。這種模式需要確保同時多個入庫加載模塊運行時散列字段(序列字段)的唯一性。可以考慮給不同的加載模塊賦予唯一因子區別。設計結構如下圖所示。

通用數據Rowkey設計
第0字節 第1字節 第2字節 第3字節
散列字段(序列字段) 擴展字段(控制在12字節內)
0x00000000~0xFFFFFFFF) 可由多個用戶字段組成
2.2.4 支持多條件查詢的RowKey設計

HBase按指定的條件獲取一批記錄時,使用的就是scan方法。 scan方法有以下特點:

(1)scan可以通過setCaching與setBatch方法提高速度(以空間換時間);

(2)scan可以通過setStartRow與setEndRow來限定范圍。范圍越小,性能越高。

通過巧妙的RowKey設計使我們批量獲取記錄集合中的元素挨在一起(應該在同一個Region下),可以在遍歷結果時獲得很好的性能。

(3)scan可以通過setFilter方法添加過濾器,這也是分頁、多條件查詢的基礎。

在滿足長度、三列、唯一原則后,我們需要考慮如何通過巧妙設計RowKey以利用scan方法的范圍功能,使得獲取一批記錄的查詢速度能提高。下例就描述如何將多個列組合成一個RowKey,使用scan的range來達到較快查詢速度。

例子:

我們在表中存儲的是文件信息,每個文件有5個屬性:文件id(long,全局唯一)、創建時間(long)、文件名(String)、分類名(String)、所有者(User)。

我們可以輸入的查詢條件:文件創建時間區間(比如從20120901到20120914期間創建的文件),文件名(“中國好聲音”),分類(“綜藝”),所有者(“浙江衛視”)。

假設當前我們一共有如下文件:

ID CreateTime Name Category UserID
1 20120902 中國好聲音第1期 綜藝 1
2 20120904 中國好聲音第2期 綜藝 1
3 20120906 中國好聲音外卡賽 綜藝 1
4 20120908 中國好聲音第3期 綜藝 1
5 20120910 中國好聲音第4期 綜藝 1
6 20120912 中國好聲音選手采訪 綜藝花絮 2
7 20120914 中國好聲音第5期 綜藝 1
8 20120916 中國好聲音錄制花絮 綜藝花絮 2
9 20120918 張瑋獨家專訪 花絮 3
10 20120920 加多寶涼茶廣告 綜藝廣告 4

這里UserID應該對應另一張User表,暫不列出。我們只需知道UserID的含義:

1代表 浙江衛視; 2代表 好聲音劇組; 3代表 XX微博; 4代表贊助商。調用查詢接口的時候將上述5個條件同時輸入find(20120901,20121001,”中國好聲音”,”綜藝”,”浙江衛視”)。此時我們應該得到記錄應該有第1、2、3、4、5、7條。第6條由於不屬於“浙江衛視”應該不被選中。我們在設計RowKey時可以這樣做:采用 UserID + CreateTime + FileID組成RowKey,這樣既能滿足多條件查詢,又能有很快的查詢速度。

需要注意以下幾點:

(1)每條記錄的RowKey,每個字段都需要填充到相同長度。假如預期我們最多有10萬量級的用戶,則userID應該統一填充至6位,如000001,000002…

(2)結尾添加全局唯一的FileID的用意也是使每個文件對應的記錄全局唯一。避免當UserID與CreateTime相同時的兩個不同文件記錄相互覆蓋。

按照這種RowKey存儲上述文件記錄,在HBase表中是下面的結構:

rowKey(userID 6 + time 8 + fileID 6) name category ….

00000120120902000001

00000120120904000002

00000120120906000003

00000120120908000004

00000120120910000005

00000120120914000007

00000220120912000006

00000220120916000008

00000320120918000009

00000420120920000010

怎樣用這張表?

在建立一個scan對象后,我們setStartRow(00000120120901),setEndRow(00000120120914)。

這樣,scan時只掃描userID=1的數據,且時間范圍限定在這個指定的時間段內,滿足了按用戶以及按時間范圍對結果的篩選。並且由於記錄集中存儲,性能很好。

然后使用 SingleColumnValueFilter(org.apache.hadoop.hbase.filter.SingleColumnValueFilter),共4個,分別約束name的上下限,與category的上下限。滿足按同時按文件名以及分類名的前綴匹配。

(注意:使用SingleColumnValueFilter會影響查詢性能,在真正處理海量數據時會消耗很大的資源,且需要較長的時間)

如果需要分頁還可以再加一個PageFilter限制返回記錄的個數。

以上,我們完成了高性能的支持多條件查詢的HBase表結構設計。

 

HBase的rowkey的設計原則

HBase是三維有序存儲的,通過rowkey(行鍵),column key(column family和qualifier)和TimeStamp(時間戳)這個三個維度可以對HBase中的數據進行快速定位。
HBase中rowkey可以唯一標識一行記錄,在HBase查詢的時候,有兩種方式:
1、通過get方式,指定rowkey獲取唯一一條記錄 
2、通過scan方式,設置startRow和stopRow參數進行范圍匹配 
3、全表掃描,即直接掃描整張表中所有行記錄
rowkey長度原則:
rowkey是一個二進制碼流,可以是任意字符串,最大長度64kb,實際應用中一般為10-100bytes,以byte[]形式保存,一般設計成定長。建議越短越好,不要超過16個字節,原因如下:
數據的持久化文件HFile中是按照KeyValue存儲的,如果rowkey過長,比如超過100字節,1000w行數據,光rowkey就要占用100*1000w=10億個字節,將近1G數據,這樣會極大影響HFile的存儲效率; 
MemStore將緩存部分數據到內存,如果rowkey字段過長,內存的有效利用率就會降低,系統不能緩存更多的數據,這樣會降低檢索效率。 
目前操作系統都是64位系統,內存8字節對齊,控制在16個字節,8字節的整數倍利用了操作系統的最佳特性。
 
rowkey散列原則:
如果rowkey按照時間戳的方式遞增,不要將時間放在二進制碼的前面,建議將rowkey的高位作為散列字段,由程序隨機生成,低位放時間字段,這樣將提高數據均衡分布在每個RegionServer,以實現負載均衡的幾率。如果沒有散列字段,首字段直接是時間信息,所有的數據都會集中在一個RegionServer上,這樣在數據檢索的時候負載會集中在個別的RegionServer上,造成熱點問題,會降低查詢效率。
 
rowkey唯一原則:
必須在設計上保證其唯一性,rowkey是按照字典順序排序存儲的,因此,設計rowkey的時候,要充分利用這個排序的特點,將經常讀取的數據存儲到一塊,將最近可能會被訪問的數據放到一塊。
 
什么是熱點:
HBase中的行是按照rowkey的字典順序排序的,這種設計優化了scan操作,可以將相關的行以及會被一起讀取的行存取在臨近位置,便於scan。然而糟糕的rowkey設計是熱點的源頭。熱點發生在大量的client直接訪問集群的一個或極少數個節點(訪問可能是讀,寫或者其他操作)。大量訪問會使熱點region所在的單個機器超出自身承受能力,引起性能下降甚至region不可用,這也會影響同一個RegionServer上的其他region,由於主機無法服務其他region的請求。設計良好的數據訪問模式以使集群被充分,均衡的利用。
為了避免寫熱點,設計rowkey使得不同行在同一個region,但是在更多數據情況下,數據應該被寫入集群的多個region,而不是一個。
 
下面是一些常見的避免熱點的方法以及它們的優缺點:
  • 加鹽
這里所說的加鹽不是密碼學中的加鹽,而是在rowkey的前面增加隨機數,具體就是給rowkey分配一個隨機前綴以使得它和之前的rowkey的開頭不同。分配的前綴種類數量應該和你想使用數據分散到不同的region的數量一致。加鹽之后的rowkey就會根據隨機生成的前綴分散到各個region上,以避免熱點。
  • 哈希
哈希會使同一行永遠用一個前綴加鹽。哈希也可以使負載分散到整個集群,但是讀卻是可以預測的。使用確定的哈希可以讓客戶端重構完整的rowkey,可以使用get操作准確獲取某一個行數據
  • 反轉

第三種防止熱點的方法時反轉固定長度或者數字格式的rowkey。這樣可以使得rowkey中經常改變的部分(最沒有意義的部分)放在前面。這樣可以有效的隨機rowkey,但是犧牲了rowkey的有序性。

反轉rowkey的例子 
以手機號為rowkey,可以將手機號反轉后的字符串作為rowkey,這樣的就避免了以手機號那樣比較固定開頭導致熱點問題
  • 時間戳反轉
一個常見的數據處理問題是快速獲取數據的最近版本,使用反轉的時間戳作為rowkey的一部分對這個問題十分有用,可以用Long.Max_Value - timestamp追加到key的末尾,例如[key][reverse_timestamp],[key]的最新值可以通過scan [key]獲得[key]的第一條記錄,因為HBase中rowkey是有序的,第一條記錄是最后錄入的數據。
比如需要保存一個用戶的操作記錄,按照操作時間倒序排序,在設計rowkey的時候,可以這樣設計 
[userId反轉][Long.Max_Value - timestamp],在查詢用戶的所有操作記錄數據的時候,直接指定反轉后的userId,startRow是[userId反轉][000000000000],stopRow是[userId反轉][Long.Max_Value - timestamp] 
如果需要查詢某段時間的操作記錄,startRow是[user反轉][Long.Max_Value - 起始時間],stopRow是[userId反轉][Long.Max_Value - 結束時間]


免責聲明!

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



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