JanusGraph - 分散式id的生成策略
大家好,我是洋仔,JanusGraph圖解系列文章,實時更新
~
本次更新時間:2020-9-1
文章為作者跟蹤原始碼和檢視官方文件整理,如有任何問題,請聯絡我或在評論區指出,感激不盡!
圖資料庫網上資源太少,評論區評論 or 私信我,邀你加入“相簿交流微信群”,一起交流學習!
原始碼分析相關:
原始碼相簿-一文搞定janusgraph圖資料庫的本地原始碼編譯(janusgraph source code compile)
圖解相簿JanusGraph系列-一文知曉匯入資料流程(待發布)
圖解相簿JanusGraph系列-簡要分析查詢讀資料流程(待發布)
圖解相簿JanusGraph系列-一文知曉鎖機制(本地鎖+分散式鎖)(待發布)
圖解相簿JanusGraph系列-一文知曉分散式id生成策略
圖解相簿JanusGraph系列-一文知曉相簿儲存分割槽策略(待發布)
儲存結構相關:
圖解相簿JanusGraph系列-一文知曉圖資料底層儲存結構
其他:
圖解相簿JanusGraph系列-官方測試圖:諸神之圖分析(待發布)
原始碼分析相關可檢視github(求star~~)
: https://github.com/YYDreamer/janusgraph
下述流程高清大圖地址:https://www.processon.com/view/link/5f471b2e7d9c086b9903b629
版本:JanusGraph-0.5.2
轉載文章請保留以下宣告:
作者:洋仔聊程式設計
微信公眾號:匠心Java
正文
在介紹JanusGraph的分散式ID生成策略之前,我們來簡單分析一下分散式ID
應該滿足哪些特徵?
- 全域性唯一:必須保證ID是分散式環境中全域性性唯一的,這是基本要求
- 高效能:高可用低延時,ID生成響應快;否則可能會成為業務瓶頸
- 高可用:提供分散式id的生成的服務要保證高可用,不能隨隨便便就掛掉了,會對業務產生影響
- 趨勢遞增:主要看業務場景,類似於圖儲存中節點的唯一id就儘量保持趨勢遞增;但是如果類似於電商訂單就儘量不要趨勢遞增,因為趨勢遞增會被惡意估算出當天的訂單量和成交量,洩漏公司資訊
- 接入方便:如果是中介軟體,要秉著拿來即用的設計原則,在系統設計和實現上要儘可能的簡單
一:常用分散式id生成策略
當前常用的
分散式id的生成策略主要分為以下四種:
- UUID
- 資料庫+號段模式(優化:資料庫+號段+雙buffer)
- 基於Redis實現
- 雪花演算法(SnowFlake)
還有一些其他的比如:基於資料庫自增id、資料庫多主模式等,這些在小併發的情況下可以使用,大併發的情況下就不太ok了
市面上有一些生成分散式id的開源元件,包括滴滴基於資料庫+號段
實現的TinyID
、百度基於SnowFlake
的Uidgenerator
、美團支援號段
和SnowFlake
的Leaf
等
那麼,在JanusGraph中分散式id的生成是採用的什麼方式呢?
二:JanusGraph的分散式id策略
在JanusGraph中,分散式id的生成採用的是資料庫+號段+雙buffer優化
的模式; 下面我們來具體分析一下:
分散式id生成使用的資料庫就是JanusGraph當前使用的第三方儲存後端,這裡我們以使用的儲存後端Hbase
為例;
JanusGraph分散式id生成所需後設資料儲存位置:
在Hbase中有column family 列族
的概念; JanusGraph在初始化Hbase表時預設建立了9大列族,用於儲存不同的資料, 具體看《圖解相簿JanusGraph系列-一文知曉圖資料底層儲存結構》;
其中有一個列族janusgraph_ids
簡寫為i
這個列族,主要儲存的就是JanusGraph分散式id生成所需要的後設資料!
JanusGraph的分散式id的組成結構:
// 原始碼中有一句話體現
/* --- JanusGraphElement id bit format ---
* [ 0 | count | partition | ID padding (if any) ]
*/
主要分為4部分:0、count、partition、ID padding(每個型別是固定值)
;
其實這4部分的順序在序列化為二進位制資料時,順序會有所改變;這裡只是標明瞭id的組成部分!
上述部分的partition
+ count
來保證分散式節點的唯一性;
- partition id:分割槽id值,JanusGraph預設分了32個邏輯分割槽;節點分到哪個分割槽採用的是
隨機分配
; - count:每個partition都有對應的一個count範圍:0-2的55次冪;JanusGraph每次拉取一部分的範圍作為節點的count取值;JanusGraph保證了針對相同的partition,不會重複獲取同一個count值!
保證count在partition維度保持全域性唯一性,就保證了生成的最終id的全域性唯一性!!
則分散式id的唯一性保證,就在於count
基於partition
維度的唯一性!下面我們的分析也是著重在count
的獲取!
JanusGraph分散式id生成的主要邏輯流程如下圖所示:(推薦結合原始碼分析觀看!)
分析過程中有一個概念為
id block
:指當前獲取的號段範圍
JanusGraph主要使用``PartitionIDPool 類來儲存不同型別的
StandardIDPool; 在
StandardIDPool`中主要包含兩個id Block:
- current block:當前生成id使用的block
- next block:double buffer中的另一個已經準備好的block
為什麼要有兩個block呢?
主要是如果只有一個block的話,當我們在使用完當前的block時,需要阻塞等待區獲取下一個block,這樣便會導致分散式id生成較長時間的阻塞等待block的獲取;
怎麼優化上述問題呢? double buffer
;
除了當前使用的block,我們再儲存一個next block
;當正在使用的block假設已經使用了50%,觸發next block
的非同步獲取,如上圖的藍色部分所示;
這樣當current block
使用完成後可以直接無延遲的切換到next block
如上圖中綠色部分所示;
在執行過程中可能會因為一些異常導致節點id獲取失敗,則會進行重試;重試次數預設為1000次;
private static final int MAX_PARTITION_RENEW_ATTEMPTS = 1000;
for (int attempt = 0; attempt < MAX_PARTITION_RENEW_ATTEMPTS; attempt++) {
// 獲取id的過程
}
ps:上述所說的IDPool和block是基於當前
圖例項
維度共用的!
三:原始碼分析
在JanusGraph的原始碼中,主要包含兩大部分和其他的一些元件:
- Graph相關類:用於對節點、屬性、邊的操作
- Transaction相關類:用於在對資料或者Schema進行CURD時,進行事務處理
- 其他一些:分散式節點id生成類;序列化類;第三方索引操作類等等
Graph和Transaction相關類的類圖如下所示:
分散式id涉及到id生成的類圖如下所示:
初始資料:
@Test
public void addVertexTest(){
List<Object> godProperties = new ArrayList<>();
godProperties.add(T.label);
godProperties.add("god");
godProperties.add("name");
godProperties.add("lyy");
godProperties.add("age");
godProperties.add(18);
JanusGraphVertex godVertex = graph.addVertex(godProperties.toArray());
assertNotNull(godVertex);
}
在諸神之圖
中新增一個name為lyy
節點;看下執行流程,注意,此處主要分析的節點的分散式id生成程式碼!
1、呼叫JanusGraphBlueprintsGraph
類的AddVertex方法
@Override
public JanusGraphVertex addVertex(Object... keyValues) {
// 新增節點
return getAutoStartTx().addVertex(keyValues);
}
2、呼叫JanusGraphBlueprintsTransaction
的addVertex
方法
public JanusGraphVertex addVertex(Object... keyValues) {
// 。。。省略了其他的處理
// 該處生成節點物件,包含節點的唯一id生成邏輯
final JanusGraphVertex vertex = addVertex(id, label);
// 。。。省略了其他的處理
return vertex;
}
3、呼叫StandardJanusGraphTx
的addVertex
方法
@Override
public JanusGraphVertex addVertex(Long vertexId, VertexLabel label) {
// 。。。省略了其他的處理
if (vertexId != null) {
vertex.setId(vertexId);
} else if (config.hasAssignIDsImmediately() || label.isPartitioned()) {
graph.assignID(vertex,label); // 為節點分配正式的節點id!
}
// 。。。省略了其他的處理
return vertex;
}
4、呼叫VertexIDAssigner
的assignID(InternalElement element, IDManager.VertexIDType vertexIDType)
方法
private void assignID(InternalElement element, IDManager.VertexIDType vertexIDType) {
// 開始獲取節點分散式唯一id
// 因為一些異常導致獲取節點id失敗,進行重試,重試此為預設為1000次
for (int attempt = 0; attempt < MAX_PARTITION_RENEW_ATTEMPTS; attempt++) {
// 初始化一個partiiton id
long partitionID = -1;
// 獲取一個partition id
// 不同型別的資料,partition id的獲取方式也有所不同
if (element instanceof JanusGraphSchemaVertex) {
// 為partition id賦值
}
try {
// 正式分配節點id, 依據partition id 和 節點型別
assignID(element, partitionID, vertexIDType);
} catch (IDPoolExhaustedException e) {
continue; //try again on a different partition
}
assert element.hasId();
// 。。。省略了其他程式碼
}
}
5、呼叫了VertexIDAssigner
的assignID(final InternalElement element, final long partitionIDl, final IDManager.VertexIDType userVertexIDType)
方法
private void assignID(final InternalElement element, final long partitionIDl, final IDManager.VertexIDType userVertexIDType) {
final int partitionID = (int) partitionIDl;
// count為分散式id組成中的一部分,佔55個位元組
// 分散式id的唯一性保證,就在於`count`基於`partition`維度的唯一性
long count;
if (element instanceof JanusGraphSchemaVertex) { // schema節點處理
Preconditions.checkArgument(partitionID==IDManager.SCHEMA_PARTITION);
count = schemaIdPool.nextID();
} else if (userVertexIDType==IDManager.VertexIDType.PartitionedVertex) { // 配置的熱點節點,類似於`makeVertexLabel('product').partition()`的處理
count = partitionVertexIdPool.nextID();
} else { // 普通節點和邊型別的處理
// 首先獲取當前partition敵營的idPool
PartitionIDPool partitionPool = idPools.get(partitionID);
// 如果當前分割槽對應的IDPool為空,則建立一個預設的IDPool,預設size = 0
if (partitionPool == null) {
// 在PartitionIDPool中包含多種型別對應的StandardIDPool型別
// StandardIDPool中包含對應的block資訊和count資訊
partitionPool = new PartitionIDPool(partitionID, idAuthority, idManager, renewTimeoutMS, renewBufferPercentage);
// 快取下來
idPools.putIfAbsent(partitionID,partitionPool);
// 從快取中再重新拿出
partitionPool = idPools.get(partitionID);
}
// 確保partitionPool不為空
Preconditions.checkNotNull(partitionPool);
// 判斷當前分割槽的IDPool是否枯竭;已經被用完
if (partitionPool.isExhausted()) {
// 如果被用完,則將該分割槽id放到對應的快取中,避免之後獲取分割槽id再獲取到該分割槽id
placementStrategy.exhaustedPartition(partitionID);
// 丟擲IDPool異常, 最外層捕獲,然後進行重試獲取節點id
throw new IDPoolExhaustedException("Exhausted id pool for partition: " + partitionID);
}
// 儲存當前型別對應的IDPool,因為partitionPool中儲存好幾個型別的IDPool
IDPool idPool;
if (element instanceof JanusGraphRelation) {
idPool = partitionPool.getPool(PoolType.RELATION);
} else {
Preconditions.checkArgument(userVertexIDType!=null);
idPool = partitionPool.getPool(PoolType.getPoolTypeFor(userVertexIDType));
}
try {
// 重要!!!! 依據給定的IDPool獲取count值!!!!
// 在此語句中設計 block的初始化 和 double buffer block的處理!
count = idPool.nextID();
partitionPool.accessed();
} catch (IDPoolExhaustedException e) { // 如果該IDPool被用完,丟擲IDPool異常, 最外層捕獲,然後進行重試獲取節點id
log.debug("Pool exhausted for partition id {}", partitionID);
placementStrategy.exhaustedPartition(partitionID);
partitionPool.exhaustedIdPool();
throw e;
}
}
// 組裝最終的分散式id:[count + partition id + ID padding]
long elementId;
if (element instanceof InternalRelation) {
elementId = idManager.getRelationID(count, partitionID);
} else if (element instanceof PropertyKey) {
elementId = IDManager.getSchemaId(IDManager.VertexIDType.UserPropertyKey,count);
} else if (element instanceof EdgeLabel) {
elementId = IDManager.getSchemaId(IDManager.VertexIDType.UserEdgeLabel, count);
} else if (element instanceof VertexLabel) {
elementId = IDManager.getSchemaId(IDManager.VertexIDType.VertexLabel, count);
} else if (element instanceof JanusGraphSchemaVertex) {
elementId = IDManager.getSchemaId(IDManager.VertexIDType.GenericSchemaType,count);
} else {
elementId = idManager.getVertexID(count, partitionID, userVertexIDType);
}
Preconditions.checkArgument(elementId >= 0);
// 對節點物件賦值其分散式唯一id
element.setId(elementId);
}
上述程式碼,我們拿到了對應的IdPool,有兩種情況:
- 第一次獲取分散式id時,分割槽對應的IDPool初始化為預設的size = 0的IDPool
- 分割槽對應的IDPool不是初次獲取
這兩種情況的處理,都在程式碼count = idPool.nextID()
的StandardIDPool
類中的nextID()
方法中被處理!
在分析該程式碼之前,我們需要知道 PartitionIDPool
和StandardIDPool
的關係:
每個partition都有一個對應的PartitionIDPool extends EnumMap<PoolType,IDPool>
是一個列舉map型別;
每一個PartitionIDPool
都有對應的不同型別的StandardIDPool
:
- NORMAL_VERTEX:用於vertex id的分配
- UNMODIFIABLE_VERTEX:用於schema label id的分配
- RELATION:用於edge id的分配
在StandardIDPool
中包含多個欄位,分別代表不同的含義,抽取幾個重要的欄位進行介紹:
private static final int RENEW_ID_COUNT = 100;
private final long idUpperBound; // Block的最大值,預設為2的55次冪
private final int partition; // 當前pool對應的分割槽
private final int idNamespace; // 標識pool為那種型別的pool,上述的三種型別NORMAL_VERTEX、UNMODIFIABLE_VERTEX、RELATION;值為當前列舉值在列舉中的位置
private final Duration renewTimeout;// 重新獲取block的超時時間
private final double renewBufferPercentage;// 雙buffer中,當第一個buffer block使用的百分比,到達配置的百分比則觸發other buffer block的獲取
private IDBlock currentBlock; // 當前的block
private long currentIndex; // 標識當前block使用到那一個位置
private long renewBlockIndex; // 依據currentBlock.numIds()*renewBufferPercentage來獲取這個值,主要用於在當前的block在消費到某個index的時候觸發獲取下一個buffer block
private volatile IDBlock nextBlock;// 雙buffer中的另外一個block
private final ThreadPoolExecutor exec;// 非同步獲取雙buffer的執行緒池
6、呼叫了StandardIDPool
類中的nextID
方法
經過上述分析,我們知道,分散式唯一id的唯一性是由在partition維度下的count的值的唯一性來保證的;
上述程式碼通過呼叫IDPool的nextId來獲取count值;
下述程式碼就是獲取count的邏輯;
@Override
public synchronized long nextID() {
// currentIndex標識當前的index小於current block的最大值
assert currentIndex <= currentBlock.numIds();
// 此處涉及兩種情況:
// 1、分割槽對應的IDPool是第一次被初始化;則currentIndex = 0; currentBlock.numIds() = 0;
// 2、分割槽對應的該IDPool不是第一次,但是此次的index正好使用到了current block的最後一個count
if (currentIndex == currentBlock.numIds()) {
try {
// 將current block賦值為next block
// next block置空 並計算renewBlockIndex
nextBlock();
} catch (InterruptedException e) {
throw new JanusGraphException("Could not renew id block due to interruption", e);
}
}
// 在使用current block的過程中,當current index == renewBlockIndex時,觸發double buffer next block的非同步獲取!!!!
if (currentIndex == renewBlockIndex) {
// 非同步獲取next block
startIDBlockGetter();
}
// 生成最終的count
long returnId = currentBlock.getId(currentIndex);
// current index + 1
currentIndex++;
if (returnId >= idUpperBound) throw new IDPoolExhaustedException("Reached id upper bound of " + idUpperBound);
log.trace("partition({})-namespace({}) Returned id: {}", partition, idNamespace, returnId);
// 返回最終獲取的分割槽維度的全域性唯一count
return returnId;
}
上述程式碼中進行了兩次判斷:
- currentIndex == currentBlock.numIds():
- 第一次生成分散式id:此處判斷即為 0==0;然後生成新的block
- 非第一次生成分散式id:等於情況下標識當前的block已經使用完了,需要切換為next block
- currentIndex == renewBlockIndex
- renew index:標識index使用多少後開始獲取下一個double buffer 的next block;有一個預設值100,主要為了相容第一次分散式id的生成;相等則會觸發非同步獲取下一個next block
下面我們分別對nextBlock();
邏輯和startIDBlockGetter();
進行分析;
7、呼叫了StandardIDPool
類中的nextBlock
方法
private synchronized void nextBlock() throws InterruptedException {
// 在分割槽對應的IDPool第一次使用時,double buffer的nextBlock為空
if (null == nextBlock && null == idBlockFuture) {
// 非同步啟動 獲取id block
startIDBlockGetter();
}
// 也是在分割槽對應的IDPool第一次使用時,因為上述為非同步獲取,所以在執行到這一步時nextBlock可能還沒拿到
// 所以需要阻塞等待block的獲取
if (null == nextBlock) {
waitForIDBlockGetter();
}
// 將當前使用block指向next block
currentBlock = nextBlock;
// index清零
currentIndex = 0;
// nextBlock置空
nextBlock = null;
// renewBlockIndex用於雙buffer中,當第一個buffer block使用的百分比,到達配置的百分比則觸發other buffer block的獲取
// 值current block 對應的count數量 - (值current block 對應的count數量 * 為renewBufferPercentage配置的剩餘空間百分比)
// 在使用current block的時候,當current index == renewBlockIndex時,觸發double buffer next block的非同步獲取!!!!
renewBlockIndex = Math.max(0,currentBlock.numIds()-Math.max(RENEW_ID_COUNT, Math.round(currentBlock.numIds()*renewBufferPercentage)));
}
主要是做了三件事:
- 1、block是否為空,為空的話則非同步獲取一個block
- 2、nextBlock不為空的情況下:next賦值到current、next置空、index置零
- 3、計算獲取下一個nextBlock的觸發index renewBlockIndex值
8、呼叫了StandardIDPool
類中的startIDBlockGetter
方法
private synchronized void startIDBlockGetter() {
Preconditions.checkArgument(idBlockFuture == null, idBlockFuture);
if (closed) return; //Don't renew anymore if closed
//Renew buffer
log.debug("Starting id block renewal thread upon {}", currentIndex);
// 建立一個執行緒物件,包含給定的許可權控制類、分割槽、名稱空間、超時時間
idBlockGetter = new IDBlockGetter(idAuthority, partition, idNamespace, renewTimeout);
// 提交獲取double buffer的執行緒任務,非同步執行
idBlockFuture = exec.submit(idBlockGetter);
}
其中建立一個執行緒任務,提交到執行緒池exec
進行非同步執行;
下面看下,執行緒類的call
方法主要是呼叫了 idAuthority.getIDBlock
方法,這個方法主要是基於Hbase來獲取還未使用的block;
/**
* 獲取double buffer block的執行緒類
*/
private static class IDBlockGetter implements Callable<IDBlock> {
// 省略部分程式碼
@Override
public IDBlock call() {
Stopwatch running = Stopwatch.createStarted();
try {
// 此處呼叫idAuthority 呼叫HBase進行佔用獲取Block
IDBlock idBlock = idAuthority.getIDBlock(partition, idNamespace, renewTimeout);
return idBlock;
} catch (BackendException e) {}
}
}
9、呼叫ConsistentKeyIDAuthority
類的getIDBlock
方法
@Override
public synchronized IDBlock getIDBlock(final int partition, final int idNamespace, Duration timeout) throws BackendException {
// 開始時間
final Timer methodTime = times.getTimer().start();
// 獲取當前名稱空間配置的blockSize,預設值10000;可自定義配置
final long blockSize = getBlockSize(idNamespace);
// 獲取當前名稱空間配置的最大id值idUpperBound;值為:2的55次冪大小
final long idUpperBound = getIdUpperBound(idNamespace);
// uniqueIdBitWidth標識uniqueId佔用的位數;uniqueId為了相容“關閉分散式id唯一性保障”的開關情況,uniqueIdBitWidth預設值=4
// 值:64-1(預設0)-5(分割槽佔用位數)-3(ID Padding佔用位數)-4(uniqueIdBitWidth) = 51;標識block中的上限為2的51次冪大小
final int maxAvailableBits = (VariableLong.unsignedBitLength(idUpperBound)-1)-uniqueIdBitWidth;
// 標識block中的上限為2的51次冪大小
final long idBlockUpperBound = (1L <<maxAvailableBits);
// UniquePID用盡的UniquePID集合,預設情況下,randomUniqueIDLimit = 0;
final List<Integer> exhaustedUniquePIDs = new ArrayList<>(randomUniqueIDLimit);
// 預設0.3秒 用於處理TemporaryBackendException異常情況(後端儲存出現問題)下:阻塞一斷時間,然後進行重試
Duration backoffMS = idApplicationWaitMS;
// 從開始獲取IDBlock開始,持續超時時間(預設2分鐘)內重試獲取IDBlock
while (methodTime.elapsed().compareTo(timeout) < 0) {
final int uniquePID = getUniquePartitionID(); // 獲取uniquePID,預設情況下“開啟分散式id唯一性控制”,值 = 0; 當“關閉分散式id唯一性控制”時為一個隨機值
final StaticBuffer partitionKey = getPartitionKey(partition,idNamespace,uniquePID); // 依據partition + idNamespace + uniquePID組裝一個RowKey
try {
long nextStart = getCurrentID(partitionKey); // 從Hbase中獲取當前partition對應的IDPool中被分配的最大值,用來作為當前申請新的block的開始值
if (idBlockUpperBound - blockSize <= nextStart) { // 確保還未被分配的id池中的id個數,大於等於blockSize
// 相應處理
}
long nextEnd = nextStart + blockSize; // 獲取當前想要獲取block的最大值
StaticBuffer target = null;
// attempt to write our claim on the next id block
boolean success = false;
try {
Timer writeTimer = times.getTimer().start(); // ===開始:開始進行插入自身的block需求到Hbase
target = getBlockApplication(nextEnd, writeTimer.getStartTime()); // 組裝對應的Column: -nextEnd + 當前時間戳 + uid(唯一標識當前圖例項)
final StaticBuffer finalTarget = target; // copy for the inner class
BackendOperation.execute(txh -> { // 非同步插入當前生成的RowKey 和 Column
idStore.mutate(partitionKey, Collections.singletonList(StaticArrayEntry.of(finalTarget)), KeyColumnValueStore.NO_DELETIONS, txh);
return true;
},this,times);
writeTimer.stop(); // ===結束:插入完成
final boolean distributed = manager.getFeatures().isDistributed();
Duration writeElapsed = writeTimer.elapsed(); // ===獲取方才插入的時間耗時
if (idApplicationWaitMS.compareTo(writeElapsed) < 0 && distributed) { // 判斷是否超過配置的超時時間,超過則報錯TemporaryBackendException,然後等待一斷時間進行重試
throw new TemporaryBackendException("Wrote claim for id block [" + nextStart + ", " + nextEnd + ") in " + (writeElapsed) + " => too slow, threshold is: " + idApplicationWaitMS);
} else {
assert 0 != target.length();
final StaticBuffer[] slice = getBlockSlice(nextEnd); // 組裝下述基於上述Rowkey的Column的查詢範圍:(-nextEnd + 0 : 0nextEnd + 最大值)
final List<Entry> blocks = BackendOperation.execute( // 非同步獲取指定Rowkey和指定Column區間的值
(BackendOperation.Transactional<List<Entry>>) txh -> idStore.getSlice(new KeySliceQuery(partitionKey, slice[0], slice[1]), txh),this,times);
if (blocks == null) throw new TemporaryBackendException("Could not read from storage");
if (blocks.isEmpty())
throw new PermanentBackendException("It seems there is a race-condition in the block application. " +
"If you have multiple JanusGraph instances running on one physical machine, ensure that they have unique machine idAuthorities");
if (target.equals(blocks.get(0).getColumnAs(StaticBuffer.STATIC_FACTORY))) { // 如果獲取的集合中,當前的圖例項插入的資料是第一條,則表示獲取block; 如果不是第一條,則獲取Block失敗
// 組裝IDBlock物件
ConsistentKeyIDBlock idBlock = new ConsistentKeyIDBlock(nextStart,blockSize,uniqueIdBitWidth,uniquePID);
if (log.isDebugEnabled()) {
idBlock, partition, idNamespace, uid);
}
success = true;
return idBlock; // 返回
} else { }
}
} finally {
if (!success && null != target) { // 在獲取Block失敗後,刪除當前的插入; 如果沒有失敗,則保留當前的插入,在hbase中標識該Block已經被佔用
//Delete claim to not pollute id space
for (int attempt = 0; attempt < ROLLBACK_ATTEMPTS; attempt++) { // 回滾:刪除當前插入,嘗試次數5次
}
}
}
} catch (UniqueIDExhaustedException e) {
// No need to increment the backoff wait time or to sleep
log.warn(e.getMessage());
} catch (TemporaryBackendException e) {
backoffMS = Durations.min(backoffMS.multipliedBy(2), idApplicationWaitMS.multipliedBy(32));
sleepAndConvertInterrupts(backoffMS); \
}
}
throw new TemporaryLockingException();
}
主要的邏輯就是:
組裝Rowkey
:partition + idNameSpace+unquePId組裝Column
:-nextEnd+now time+uid- 將
RowKey+Column
插入Hbase - 獲取的上述組裝的RowKey 基於(-nextEnd + 0 : -nextEnd + max)範圍的所有Column集合
- 判斷集合的第一個Column是不是當前插入的Column,是的話則佔用block成功,不是的話則佔用失敗,刪除剛才佔用並進行重試
最終:非同步獲取到了唯一佔用的Block,然後生成對應的唯一count,組裝最後的唯一id
整體的呼叫流程如下:
四:其他型別的id生成
上述我們主要依據生成節點id(vertex id)的過程來進行分析
在JanusGraph
中還包含edge id
、property id
、schema label id
等幾種的分散式id生成
所有型別的分散式id的生成主要思想和邏輯都幾乎相同,只是一些具體的邏輯可能有所不同,我們理解了vertex id
的分散式id生成流程,其他的也可以理解了。
1、property id的生成
在JanusGraph中的property
的分散式唯一id的生成,整體邏輯和vertex id
的生成邏輯大體相同;
property id
的 生成和 vertex id
有兩點不同:
- ID的組成部分: 在
vertex id
中組成部分包含count
+partition
+ID Padding
; 而在property id
中沒有ID Padding
部分,其組成為count + partition
long id = (count<<partitionBits)+partition;
if (type!=null) id = type.addPadding(id); // 此時,type = null
return id;
- partition id的獲取方式:在生成
vertex id
時,partition id是隨機獲取的;而在生成property id
時,partition id是獲取的當前節點對應的partition id,如果節點獲取不到分割槽id,則隨機生成一個;
if (element instanceof InternalRelation) { // 屬性 + 邊
InternalRelation relation = (InternalRelation)element;
if (attempt < relation.getLen()) {
InternalVertex incident = relation.getVertex(attempt);
Preconditions.checkArgument(incident.hasId());
if (!IDManager.VertexIDType.PartitionedVertex.is(incident.longId()) || relation.isProperty()) { // 獲取對應節點已有的partition id
partitionID = getPartitionID(incident);
} else {
continue;
}
} else { // 如果對應的節點都沒有,則隨機獲取一個partition id
partitionID = placementStrategy.getPartition(element);
}
2、Edge id的生成
在JanusGraph中的edge
的分散式唯一id的生成,整體邏輯和vertex id
的生成邏輯大體相同;
edge id
的 生成和 vertex id
有兩點不同:
- ID的組成部分: 在
vertex id
中組成部分包含count
+partition
+ID Padding
; 而在edge id
中沒有ID Padding
部分,其組成為count + partition
,程式碼同property id
的生成程式碼 - partition id的獲取方式:在生成
vertex id
時,partition id是隨機獲取的;而在生成edge id
時,partition id是獲取的當前source vertex
或者target vertex
對應的partition id,如果節點獲取不到分割槽id,則隨機生成一個,程式碼同property id
的生成程式碼;
3、Schema相關id的生成
在JanusGraph中的schema相關id
的分散式唯一id的生成,整體邏輯和vertex id
的生成邏輯大體相同;
schema相關id的生成分為四種:PropertyKey
、EdgeLabel
、VertexLabel
、JanusGraphSchemaVertex
- ID的組成部分: 在
vertex id
中組成部分包含count
+partition
+ID Padding
; 在schema
對應的id生成,這四種產生的id對應的結構都是一樣的:count + 對應型別的固定字尾
return (count << offset()) | suffix();
- partition id的獲取方式:在生成
vertex id
時,partition id是隨機獲取的;而在生成schema id
時,partition id是預設的partition id = 0
;
public static final int SCHEMA_PARTITION = 0;
if (element instanceof JanusGraphSchemaVertex) {
partitionID = IDManager.SCHEMA_PARTITION; // 預設分割槽
}
總結
本文總結了JanusGraph
的分散式唯一id的生成邏輯,也進行的原始碼分析;
下一篇,JanusGraph的鎖機制分析,包含本地鎖和分散式鎖相關的分析,我是“洋仔”,我們下期見~