Cassandraのはじめ方─手を動かしてNoSQLを体感しよう

第6回Cassandraでデータの更新・削除をするには

前回はCassandraへデータを投入する方法を説明しました。今回はデータの更新と削除についてご説明します。

データ更新ではタイムスタンプに注意

Cassandraにおけるデータ更新は、実はインサートのときと同じAPIを使います。1点だけ違うのは、更新の場合はタイムスタンプが重要になることです。タイムスタンプが以前に入れたものより後になっていないと、データが更新されません。

以下のコードでそれを確かめてみましょう。

リスト1 SimpleUpdate
Date oldDate = new SimpleDateFormat("yyyy/MM/dd").parse("1970/01/01");
long oldTimestamp = oldDate.getTime();
Cassandra.Client client = new Cassandra.Client(protocol);
try {
	final String key = "sample_update1";
	final String columnName = "update_hoge";
	String value = "original_value";
	long timestamp = System.currentTimeMillis();

	final ColumnPath columnPath = new ColumnPath(COLUMN_FAMILY);
	columnPath.setColumn(columnName.getBytes());
	// 1件カラムをインサート
	client.insert(KEYSPACE, key, columnPath, value.getBytes(),
			timestamp, ConsistencyLevel.ONE);

	value = "update_value";
	timestamp = System.currentTimeMillis();
	// 同一カラムを更新日付を後にしてアップデート
	client.insert(KEYSPACE, key, columnPath, value.getBytes(),
			timestamp, ConsistencyLevel.ONE);

	value = "update2_value";
	timestamp = oldTimestamp;
	// 同一カラムを更新日付を前にしてアップデート
	client.insert(KEYSPACE, key, columnPath, value.getBytes(),
			timestamp, ConsistencyLevel.ONE);

} catch (InvalidRequestException e) {
    ...

Cassandra-cliを使って確認してみてください。

cassandra> get Keyspace1.Standard1['sample_update1']
=> (column=7570646174655f686f6765, value=update_value, timestamp=1277138220269)
Returned 1 results.

データが更新日付が後のもので上書きされているのがわかるでしょうか。

実行してみるとわかるのですが、Cassandraでは更新日付がきちんとデータ投入時より後になってないと更新できません。例外も出さないため、注意が必要です。タイムスタンプは日付をきちんととらなくてはいけない理由がこのあたりにあります。

batch_mutateを使った場合でも、カラムがあれば同様に更新できます。

1行ならremove、まとめてならbatch_mutateで削除

次はデータの削除です。データを削除するには以下の2つの方法があります。

  • removeメソッド削除する
  • batch_mutateでDeletionオブジェクトをセットして削除する

1行を削除するような処理は前者で問題ありません。一方、まとめて削除したいようなケースでは後者のほうがbatch_mutateメソッドで、削除を表現するDeletionオブジェクトを使って削除するのがよいです。ThriftのAPIを1行ずつコールしないので、効率よく処理できます。

ではそれぞれの詳細を見ていきましょう。

removeメソッドで削除する場合の3つの選択肢

removeメソッドで削除する場合、以下の3つの選択肢があります。

  • ① キーで指定したロウ内のカラムを削除する
  • ② キーで指定したロウ内のスーパーカラムを削除する
  • ③ そのキーのロウごと削除する

これらは、インサートのときと同じように、すべてColumnPathで指定します。ColumnPathにはカラムファミリの指定が必須ですが、それ以外は上記の①~③に応じて設定する内容を変えます。

①キーで指定したロウ内のカラムを削除する

以下は特定カラムを削除するサンプルコードの抜粋です。このサンプルでは、カラムを1つ追加して、そのデータを削除することを試みています。

注目していただきたいことが2点あります。1点目は、ColumnPathを使う際に、setColumnで明示的に削除するColumnを指定していることです。

2点目は、更新のときと同様に古いタイムスタンプで削除を試みますが、それはうまくいかずにデータは削除されないことです。削除されたデータを検索メソッドであるgetで読み出そうとすると、NotFoundExceptionが発生します。タイムスタンプをインサート時より後にしてあげるときちんとデータが削除されます。

リスト2 DeleteColumnWithRemove
Date oldDate = new SimpleDateFormat("yyyy/MM/dd").parse("1970/01/01");
long oldTimestamp = oldDate.getTime();
Cassandra.Client client = new Cassandra.Client(protocol);
try {
	final String key = "sample_delete1";
	final String columnName = "delete_hoge";
	String value = "削除のサンプルです";
	long timestamp = System.currentTimeMillis();

	final ColumnPath columnPath = new ColumnPath(COLUMN_FAMILY);
	columnPath.setColumn(columnName.getBytes());
	// 1件カラムをインサート
	client.insert(KEYSPACE, key, columnPath, value.getBytes(),
			timestamp, ConsistencyLevel.ONE);

	// 古いタイムスタンプで削除しようとすると、削除できない.特に例外も出ない。
	client.remove(KEYSPACE, key, columnPath, oldTimestamp,
			ConsistencyLevel.ALL);

	// 削除されていない事を確認
	ColumnOrSuperColumn ret = client.get(KEYSPACE, key, columnPath,
			ConsistencyLevel.ONE);
	Column retColumn = ret.getColumn();
	String retName = new String(retColumn.getName());
	String retValue = new String(retColumn.getValue());
	System.out.println("カラムキー : " + retName);
	System.out.println("カラム値: " + retValue);
	System.out.println("タイムスタンプ: " + retColumn.getTimestamp());

	timestamp = System.currentTimeMillis();
	// タイムスタンプを更新して削除.
	client.remove(KEYSPACE, key, columnPath, timestamp,
			ConsistencyLevel.ALL);

	// 削除されていてカラムが既に無いのでNotFoundException発生
	ret = client.get(KEYSPACE, key, columnPath, ConsistencyLevel.ONE);

②キーで指定したロウ内のスーパーカラムを削除する

スーパーカラムを削除する場合は、ColumnPathでsetSuperColumn()でスーパーカラムを指定してください。

さらにスーパーカラム内の特定カラムだけを削除することもできます。下記のサンプルでは、ColumnPath#setSuperColumn()とsetColumn()を併用して、スーパーカラム内の特定カラムだけを削除しています。

リスト3 DeleteOneColumnOfSuperColumnWithRemove
final String superColumnName = "hogehoge1";
final String key = "delete_one_column";
long timestamp = System.currentTimeMillis();

// スーパーカラムをインサートしておく
insertSuperColumn(superColumnName, key, client, timestamp);

// データがきちんと入ったかgetメソッドで確認する
ColumnPath columnPath = new ColumnPath(COLUMN_FAMILY);
columnPath.setSuper_column(superColumnName.getBytes());
ColumnOrSuperColumn ret = client.get(KEYSPACE, key, columnPath,
		ConsistencyLevel.QUORUM);
SuperColumn superColumn = ret.getSuper_column();
System.out.println("削除前");
for (Column c : superColumn.getColumns()) {
	String retName = new String(c.getName());
	String retValue = new String(c.getValue());
	System.out.printf("\tカラムキー :\t%s\n", retName);
	System.out.printf("\tカラム値:\t%s\n", retValue);
	System.out.printf("\tタイムスタンプ:\t%s\n", c.getTimestamp());
}

timestamp = System.currentTimeMillis();

// 削除用のColumnPathを作る。スーパーカラム内の特定カラムが削除したいのでそのカラム名"fooKey"を指定する。
ColumnPath deletePath = new ColumnPath(COLUMN_FAMILY);
deletePath.setSuper_column(superColumnName.getBytes());
deletePath.setColumn("fooKey".getBytes());
client.remove(KEYSPACE, key, deletePath, timestamp,
		ConsistencyLevel.ALL);

// 削除されたかどうか確認する。このケースでは、スーパーカラム自体は残り、カラム"fooKey"だけがなくなっている。
ret = client
		.get(KEYSPACE, key, columnPath, ConsistencyLevel.QUORUM);
superColumn = ret.getSuper_column();
System.out.println("削除後");
for (Column c : superColumn.getColumns()) {
	String retName = new String(c.getName());
	String retValue = new String(c.getValue());
	System.out.printf("\tカラムキー :\t%s\n", retName);
	System.out.printf("\tカラム値:\t%s\n", retValue);
	System.out.printf("\tタイムスタンプ:\t%s\n", c.getTimestamp());
}

③キーで指定したロウごと削除する

キーで指定したロウごと削除することもできます。この方法が最も簡単なのですが、ColumnPathでsetSuperColumn()もsetColumn()せずに削除するとロウそのものを削除することになる点に注意してください。

リスト4 DeleteEntireRowWithRemove
final String superColumnName = "sample1";
final String key = "delete_entire_row";
long timestamp = System.currentTimeMillis();

// スーパーカラムをインサートしておく
insertSuperColumn(superColumnName, key, client, timestamp);

ColumnPath columnPath = new ColumnPath(COLUMN_FAMILY);
columnPath.setSuper_column(superColumnName.getBytes());

// スーパーカラムがデータとしてあるか確認する。
ColumnOrSuperColumn ret = client.get(KEYSPACE, key, columnPath,
		ConsistencyLevel.QUORUM);
SuperColumn superColumn = ret.getSuper_column();
for (Column c : superColumn.getColumns()) {
	String retName = new String(c.getName());
	String retValue = new String(c.getValue());
	System.out.println("カラムキー : " + retName);
	System.out.println("カラム値: " + retValue);
	System.out.println("タイムスタンプ: " + c.getTimestamp());
}

timestamp = System.currentTimeMillis();

// ロウごと削除する。
client.remove(KEYSPACE, key, new ColumnPath(COLUMN_FAMILY),
		timestamp, ConsistencyLevel.ALL);

// ロウごと消えているため、NotFoundExceptionが発生する。
client.get(KEYSPACE, key, columnPath, ConsistencyLevel.QUORUM);

batch_mutateメソッドで削除する

removeメソッドでも十分機能的には事足りるのですが、スーパーカラムなどをもっと大量に削除したい場合はbatch_mutateを使ったほうが効果的です。batch_mutateのDeletionにある以下の2つのオプションが役立ちます。

  • スーパーカラムを設定して削除する
  • SlicePredicate(カラム名の集合か、一定数のレンジ)を設定して削除する

では実際のサンプルコードを見てみましょう。

リスト5 SuperColumnDeleteWithDeletion
final String superColumnName = "sample1";
final String key = "super_sample1";
long timestamp = System.currentTimeMillis();

// スーパーカラムをインサート
insertSuperColumn(superColumnName, key, client, timestamp);

ColumnPath columnPath = new ColumnPath(COLUMN_FAMILY);
columnPath.setSuper_column(superColumnName.getBytes());

// データが投入されたか確認
ColumnOrSuperColumn ret = client.get(KEYSPACE, key, columnPath,
		ConsistencyLevel.QUORUM);
SuperColumn superColumn = ret.getSuper_column();
for (Column c : superColumn.getColumns()) {
	String retName = new String(c.getName());
	String retValue = new String(c.getValue());
	System.out.printf("\tカラムキー \t\t:%s\n", retName);
	System.out.printf("\tカラム値\t\t:%s\n", retValue);
	System.out.printf("\tタイムスタンプ\t:%s\n", c.getTimestamp());
}

timestamp = System.currentTimeMillis();

// batch_mutateの構造を作って、Deletionをセットする
Map<String, Map<String, List<Mutation>>> mutationMap = new HashMap<String, Map<String, List<Mutation>>>();
Map<String, List<Mutation>> map = new HashMap<String, List<Mutation>>();
List<Mutation> list = new ArrayList<Mutation>();
Mutation mutation = new Mutation();
// Deletionを作成し、スーパーカラムで所定の名前を指定する
Deletion deletion = new Deletion(timestamp);
deletion.setSuper_column(superColumnName.getBytes());
mutation.setDeletion(deletion);
list.add(mutation);
map.put(COLUMN_FAMILY, list);
mutationMap.put(key, map);

// 削除実行。
client.batch_mutate(KEYSPACE, mutationMap, ConsistencyLevel.ALL);

// 削除されたことを確認する。削除されていればNotFoundExceptionが発生する。
client.get(KEYSPACE, key, columnPath, ConsistencyLevel.QUORUM);

ほとんどのコードが前回のbatch_mutateを使ったインサートのときと同じなのですが、ポイントはDeletionを使うところです。

Deletionオブジェクトを設定するだけで削除処理とみなされるので、ユーティリティやフレームワークは比較的楽に作ることができそうです。もし削除の処理をライブラリやフレームワークなどで統一するのであれば、batch_mutateで統一するほうが実行効率がよくなります。

分散データベースでの削除について

ここまで削除処理を見てきました。比較的簡単に実行できるのでAPIレベルで難しいところはないと思います。ところが、Cassandraが分散データベースである点を考慮すると、実はこの削除という処理は厄介なことになってきます。

Cassandraは複数ノードを立てて運用する場合、レプリケーション数を3かそれ以上にするのが一般的です。つまり複数ノード間でデータのレプリケーションを行うようになっています。

また、Cassandraは一貫性モデルをイベンチュアルコンシステンシに沿って設計されているので、内部的には以下の処理が行われています。

  • ユーザの検索処理が終わった後に各ノード間の整合性をとる(read repair)
  • 一時的に死んでいたサーバに後からデータの同期を行う(hinted handoff)
  • 非同期に定期的に各ノードが保持しているデータを同期する(antientropy)

このため、本当にただデータを削除するだけだと、ノード間の整合性でデータが復旧されてしまったり、システムとしてのデータ一貫性のレベルが低下してしまう危険性があります。これは避けねばなりません。そこでCassandraでは、データを単純に物理削除するのではなく、データ削除の特別な値におきかえる処理(いわゆる論理削除)を最初に行っています。

この削除用の特別な値のことをトゥームストーン(墓標とか墓石という意味です)と呼んでいます。この仕組みのおかげで、レプリカ間でもトゥームストーンが伝播するので、削除された値であることがすぐにわかります。

ただし、これだけだと、削除されたデータが物理的には削除されないため、ディスクを圧迫していくばかりです。そこで、トゥームストーンがついたデータはある一定のタイミングで物理的に削除される仕組みになっています。このタイミングはCassandraのstorage-conf.xmlのGCGraceSecondsで変更することができます。デフォルトは10日です。また、運用管理者の操作によって、永続化されたデータをコンパクトにする処理を行った際にトゥームストーンも削除します。

なお、今回作成したサンプルプログラム全体は、以下からダウンロード可能です。

以上、今回は更新と削除についてまとめました。次回は検索をしてみます。お楽しみに。

おすすめ記事

記事・ニュース一覧