HBase1.2 checkAndPut & scan を用いて行の論理削除を行う

要約

  • HBaseの行を論理削除することを実現する方法として、checkAndPutとscanを用いて実装する
  • 将来的に、複数の論理削除やフラグが追加される場合でも、拡張を容易に行うことができる

やりたいこと

  • 論理削除/解除される時間はtimestampで与えられ、論理削除フラグはtimestampのlast winで最新のものが有効な値として反映される必要がある。
  • つまり、ある行rに対してt1 -> t2 -> t3の順番で発生した論理削除/解除のイベントをHBaseに適用した時に、どの順番で適用しても、最終的にはt3の状態が反映されることが期待される。
  • しかし、ある行rが存在している間にのみ論理削除/解除を適用したい
  • 論理削除されているものを取り出すか、取り出さないかは柔軟に判断して分けたい。

実装

論理削除を実行

final Table table;
final byte[] rowKeyBytes; // flagを立てる対象のrow key bytes
final byte[] familyBytes; // flagを立てる対象のfamily bytes
final byte[] flagQualifierBytes; // flag自体のqualifier bytes
final byte[] flagTimestamp; // flagのtimestamp
final byte[] mustExistFamily; // 存在している必要があるデータのfamily bytes
final byte[] mustExistQualifier; // 存在している必要があるデータのqualifier bytes

table.checkAndPut(
        rowKeyBytes,
        mustExistFamily,
        mustExistQualifier,
        CompareOp.GREATER_OR_EQUAL,
        new byte[] { 0 },
        new Put(rowKeyBytes)
                .addColumn(
                        familyBytes,
                        flagQualifierBytes,
                        flagTimestamp,
                        HConstants.EMPTY_BYTE_ARRAY
                )
);

対象のカラムに何か値が存在する場合にのみPutを実行する為に、入りうるバイト値の中で最小の 0 を設定し、それよりも大きいか等しいものが存在する場合のみPutを実行するように条件を設定する。

HBaseのcheckAndPutでは値が無いということ自体を表現する値が用意されていない為、このように対応している。 HConstants.EMPTY_BYTE_ARRAY は、「空の値が存在する」表現であり、値が存在しないことを表現しない。

HBaseのcheckAndPutは、同一のFamilyに属するColumnに対してのみ条件を設定できる。これはHBaseの内部構造に起因するもので、同じFamilyに属するColumnは同じファイルに属している為、checkAndPut操作はアトミック & 高速に実行できることが期待される。 checkAndPut操作は新しいHBaseではDeprecatedになっており、checkAndMutateが推奨されている。これは複数行に対してもアトミックに操作することが可能となっているが、パフォーマンスについて懸念があるように思える。(全然調べていない)

--- 余談

今回の条件は、以下のようなnullとNOT_EQUALの組み合わせで実装できるのではないかと考えていたが、うまく動作せずどのような場合でもPutが成功してしまった。

table.checkAndPut(
        rowKeyBytes,
        mustExistFamily,
        mustExistQualifier,
        CompareOp.NOT_EQUAL,
        null,
        new Put(rowKeyBytes)
                .addColumn(
                        familyBytes,
                        flagQualifierBytes,
                        flagTimestamp,
                        HConstants.EMPTY_BYTE_ARRAY
                )
);

論理削除の解除を実行

final Table table;
final byte[] rowKeyBytes; // flagを立てる対象のrow key bytes
final byte[] familyBytes; // flagを立てる対象のfamily bytes
final byte[] flagQualifierBytes; // flag自体のqualifier bytes
final byte[] flagTimestamp; // flag解除のtimestamp

table.delete(
        new Delete(rowKeyBytes)
                .addColumns(familyBytes, flagQualifierBytes, flagTimestamp)
);

なお、.addColumns(familyBytes, flagQualifierBytes, flagTimestamp) でtimestampを指定しているが、この場合timestamp以下のcolumnは全て削除される。

論理削除を除去したscan

final Table table;
// ...
final byte[] flagQualifierBytes; // flag自体のqualifier bytes

final Scan scan = new Scan();
// add scan configurations
// ...
final var logicallyDeleteQualifierSkipFilter =
        new SkipFilter(new QualifierFilter(
                CompareOp.NOT_EQUAL, new BinaryComparator(flagQualifierBytes)));
scan.setFilter(logicallyDeleteQualifierSkipFilter);

table.getScanner(scan);

new BinaryComparator(flagQualifierBytes) だけの場合、対象のQualifierのみがフィルタされ、行自体と他のQualifierについてはフィルタされない。 SkipFilterを組み合わせることで、条件に一つでもマッチした場合に行自体のscanをスキップする挙動になる。

別途 FilterListなどを組み合わせることで、複数のフラグを組み合わせたscan条件を構築することが可能。 フラグの追加側は、同じFamily内で複数のフラグ用Columnを用意すれば、同様に実装できる。

論理削除を含む通常のscan

フィルタを特別に設定しなければ、scan対象になる。