2012/07/30

Android:SQLiteのロックとTransaction Immediate/Exclusive


!この記事は古くなっています. 更新版は下記をご覧ください.!
  Android: SQLite3 LockとTransaction Immediate/Exclusive
  http://yuki312.blogspot.jp/2014/10/android-sqlite3-locktransaction.html


SQLiteのロック機構と、Transactionで使用できるロック2種の選択基準についての考察。

●SQLiteのロック

SQLiteのロック単位は"データベース単位"。
このため、ロックを1つ取得すると同データベース上にある全てのテーブルに影響がある。

Oracle等の巨大なDBMSでは"行ロック"なんてものがあったりして細かくロックを制御できるが、軽量なSQLiteは"データベース単位"でのロックのみサポートしている。
そのため、長い時間ロックし続けると他テーブルになかなかアクセスできない状態になる。
全く問題にならないケースもあるが、"データベースを分割するかしないか"の判断基準の1つにはなりそう。


●Transaction Immediate

Transactionで指定できるロック種別の1つがImmediate。
Immediateロックモードのポイントは下記
・ロック中、他ユーザはデータの読み取りはできても書き込みはできない
長所は、ロック中でも他ユーザがデータを読めるため"ロック解除されるまで待たなくて良い"こと。
逆に"読めてしまうことによる弊害"がある場合はExclusiveロックの使用を検討する。

Immediateロックではマズいケースは下記のような場合かな?あまり良い例が思いつかない。
  1.  アプリの設定値を保持したテーブルがある
  2.  アプリの設定値は他モジュールから参照される
  3.  アプリの設定値は可能な限り最新値を返す
  4.  アプリの設定値の更新には多少時間が掛かる
アプリの設定値を更新する必要があるシーン(つまりDBテーブルの値が古い状態)で、設定値の更新を試みる。
ただし、設定値の更新は多少時間がかかる(4)。
Immediateロックでは、更新中(ロック中)にデータベースを参照されると古い値が返されるのでNG(3)。
そのため、参照もできなくするExclusiveロックを掛ける。


●Transaction Exclusive

Transactionで指定できるもう1つのロック種別がExclusive。
Exclusiveロックモードのポイントは下記
・ロック中、他ユーザはデータの読み書きができない
長所は、"ロック中に読み書きできるのは自ユーザのみ"なこと。
逆に"他ユーザの読み取りを待たせてしまうことによる弊害"がある場合はImmediateロックの使用を検討する。

Exclusiveロックだとマズいケースは下記のような場合かな?
  1.  とあるデータAは多数のユーザから頻繁に読み取りアクセスされる
  2.  とあるデータAを読み取るアプリは高い応答性が求められる
  3.  データ更新中に並行してデータ参照されても平気
Exclusiveロックを取得している間、他ユーザは同データベース内にあるテーブルや行/列のデータを参照できない。
そのため、Exclusiveロックが長くなればなるほど、データを参照したいアプリの応答性を損なう原因となり兼ねない(2)。
(3)があてはまるならImmediateロックで問題ないかな?

# 上記2つのケースがImmediate/Exclusiveロックの考え方として正しいのか自信なし。
# もっと良いケースがあれば後日修正します。


●ソースコード

AndroidでのTransactionはSQLiteDataBaseクラスを使用します。

基本的な使い方は割愛。下記サイトで詳しく取り上げられています。
AABlog:Androidアプリ開発 SQLiteデータベースを使用する(トランザクション)

AndroidでTransactionのロック種別を指定するにはSQLiteDataBaseクラスを使用します。
使用するのは下記のAPI
※beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveはAPI Level11(Honeycomb)で追加されたAPIです。

beginTransaction/beginTransactionWithListenerで開始されたトランザクションは「Exclusiveモード」で、
beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveで開始されたトランザクションは「Immediateモード」でデータベースをロックします。
// 「Exclusiveモード」でロック
db.beginTransaction();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

// 「Immediateモード」でロック
db.beginTransactionNonExclusive();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
簡単ですね。

●SQLiteDatabaseLockedException

ロックされているデータベースに対してSQLiteDatabase経由でクエリを実行した場合、、、

・SQLiteDatabase.query
ロックが取得できるまで待ちます。
API Lv16(JellyBean)以降はCancellationSignalを使ってクエリをキャンセルすることが出来ます。
ContentResolver.query()
CancellationSignal

・SQLiteDatabase.insert
SQLiteDatabaseがSQLiteDatabaseLockedExceptionをキャッチし、呼出し元に-1を返します。
public long insert(String table, String nullColumnHack, ContentValues values) {
    try {
        return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
    } catch (SQLException e) {
        Log.e(TAG, "Error inserting " + values, e);
        return -1;
    }
}
・SQLiteDatabase.insertOrThrow
SQLiteDatabaseLockedExceptionが投げられます。
public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
        throws SQLException {
    return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}

・SQLiteDatabase.update
SQLiteDatabaseLockedExceptionが投げられます。

・SQLiteDatabase.delete
SQLiteDatabaseLockedExceptionが投げられます。


また、トランザクション開始のbeginTransactionでも同様。

・SQLiteDatabase.beginTransaction
SQLiteDatabaseLockedExceptionが投げられます。


以上です。