スキーマ設計
それでは、スキーマの設計を考えていきます。
まず、簡単に前回に説明したリレーションシップのスキーマのおさらいをします。リレーションシップのスキーマは以下になります。
RowKey | ColumnFamily | Column | Timestamp | Value |
---|---|---|---|---|
hash(startNodeId)-1-startNodeId-type-endNodeId | "g" | "c" | timestamp | createTimestmp |
hash(startNodeId)-1-startNodeId-type-endNodeId | "g" | "p" | timestamp | {PropertyName:PropertyValue, …} |
hash(startNodeId)-1-startNodeId-type-endNodeId | "g" | "u" | timestamp | updateTimestamp |
hash(endNodeId)-2-endNodeId-INCOMING-type-(Long.MAX_VALUE - createTimestmp)-startNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
hash(startNodeId)-2-startNodeId-OUTGOING-type-(Long.MAX_VALUE - createTimestmp)-endNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
最初にhash値があり、次にRowKeyのタイプが入っています。RowKeyのタイプは1がリレーションシップのRow、2が最新順インデックスのRowとなっています。
リレーションシップのRowでは、プロパティと作成時間(createTimestmp)と更新時間(updateTimestamp)が同じRowの中で別のColumnとして入っています。
最新順インデックスは取得する方向によって、INCOMINGとOUTGOINGの2種類のRowがあります。また、最新順インデックスのRowにもプロパティの値が入っており、最新順インデックスをScanするだけでプロパティの値をルックアップする必要がない設計(非正規化)になっています。
セカンダリインデックスも、最新順インデックスと基本的な考え方は同様です。ただし、プロパティの値によってソートされる必要があるので、RowKeyの中にpropertyValueを入れます。また、制約として、propertyValueの最大のサイズを決める必要があります。プロパティの値をRowKeyの中に入れて正しくソートするためには、固定長でなくてはならないからです。最大のサイズに満たない場合は、0で埋める必要があります。
今回は、最大のサイズを100バイトとして実装します。
HBaseでは、RowをRowKeyの昇順でScanすることはできますが、降順でScanすることはできません。そのため、昇順用のセカンダリインデックス以外に、降順用のセカンダリインデックスも作成する必要があります。降順用のセカンダリインデックスは、プロパティの値をビット反転させた値を用いることで作成できます。
ここまでをまとめると、以下のスキーマになります。
RowKey | ColumnFamily | Column | Timestamp | Value |
---|---|---|---|---|
endNodeId-INCOMING-type-propertyName-ASC-propertyValue-(Long.MAX_VALUE - createTimestmp)-startNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
endNodeId-INCOMING-type-propertyName-DESC-(inverse propertyValue)-(Long.MAX_VALUE - createTimestmp)-startNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
startNodeId-OUTGOING-type-propertyName-ASC-propertyValue-(Long.MAX_VALUE - createTimestmp)-endNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
startNodeId-OUTGOING-type-propertyName-DESC-(inverse propertyValue)-(Long.MAX_VALUE - createTimestmp)-endNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
最新順インデックスと同様に、ColumnFamilyは固定値"g"としており、Columnは空になっています。また、propertyValueの後ろに(Long.MAX_VALUE - createTimestmp)があるのは、プロパティの値が同じだった場合に、より新しいリレーションシップが先になるようにするためです。
最後に、書き込みや読み込みの分散のために、RowKeyのプレフィックスにhash値をつけます。また、それぞれのRowを区別するために、それぞれのRowのhash値の後にRowKeyのタイプを入れます。
RowKey | ColumnFamily | Column | Timestamp | Value |
---|---|---|---|---|
hash(endNodeId)-3-endNodeId-INCOMING-type-propertyName-ASC-propertyValue-(Long.MAX_VALUE - createTimestmp)-startNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
hash(endNodeId)-3-endNodeId-INCOMING-type-propertyName-DESC-(inverse propertyValue)-(Long.MAX_VALUE - createTimestmp)-startNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
hash(startNodeId)-3-startNodeId-OUTGOING-type-propertyName-ASC-propertyValue-(Long.MAX_VALUE - createTimestmp)-endNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
hash(startNodeId)-3-startNodeId-OUTGOING-type-propertyName-DESC-(inverse propertyValue)-(Long.MAX_VALUE - createTimestmp)-endNodeId | "g" | "" | timestamp | {PropertyName:PropertyValue, …} |
ここで、今回作成しているグラフDBの整合性について書きます。
読者の中には、1つのリレーションシップを格納するために複数RowをPutすると、Putが一部失敗した際に不整合が起こるだろうと考える方もいるのではないかと思います。HBaseは、基本的にはRow内のトランザクションしかサポートしていませんので、今回作成しているグラフDBの設計では実際に不整合が発生する可能性はあります。
今回作成しているグラフDBの戦略としては、データの不整合が生じた場合は、後からそれを修復するというアプローチをとっています。最新順インデックスのRowやセカンダリインデックスのRowで不整合が生じた場合は、リレーションシップのRowから復旧させることになります。
また、別の問題として、1つのリレーションシップを格納するための複数RowのPutはアトミックに行われないということがあります。つまり、更新が最新順インデックスのRowには反映されているが、セカンダリインデックスのRowには反映されていないという状態が一瞬だけ起こり得るということです。
ただし、クエリパターンとして、実際に同時にScanされるのは、最新順インデックスのRowかセカンダリインデックスのRowかのいずれかです。一瞬だけ不整合な状態があったとしても、同時に取得されることはないので問題はないという考え方をとっています。
さらに、今回追加するセカンダリインデックスにおける整合性の問題があります。これは実装に依存した話なので、そちらで書きたいと思います。また、HBaseにおけるトランザクションについては、本連載の中でもっと詳細に取り扱う予定となっています。