皆さん、
一括挿入 (数十秒かかる)、クイック挿入、および読み取りが可能で、UI スレッドをブロックしないクライアント側の SQLite データベースと UI スレッドが対話できるようにする設計パターンを探しています。
最近、デッドロックと同期の問題をデバッグしており、最終製品に 100% の自信があるわけではないので、これに最適な設計パターンを使用しているかどうかについてアドバイスをお願いします。
すべての DB アクセスは、シングルトン クラスによってボトルネックになりました。シングルトン DataManager で書き込みにどのようにアプローチしているかを示す疑似コードを次に示します。
public class DataManager {
private SQLiteDatabase mDb;
private ArrayList<Message> mCachedMessages;
public ArrayList<Message> readMessages() {
return mCachedMessages;
}
public void writeMessage(Message m) {
new WriteMessageAsyncTask().execute(m);
}
protected synchronized void dbWriteMessage(Message m) {
this.mDb.replace(MESSAGE_TABLE_NAME, null, m.toContentValues());
}
protected ArrayList<Message> dbReadMessages() {
// SQLite query for messages
}
private class WriteMessageAsyncTask extends AsyncTask<Message, Void, ArrayList<Messages>> {
protected Void doInBackground(Message... args) {
DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
DataManager.this.dbWriteMessage(args[0]);
// More possibly expensive DB writes
DataManager.this.mDb.execSQL("COMMIT TRANSACTION;");
ArrayList<Messages> newMessages = DataManager.this.dbReadMessages();
return newMessages;
}
protected void onPostExecute(ArrayList<Message> newMessages) {
DataManager.this.mCachedMessages = newMessages;
}
}
}
ハイライト:
- 最初: すべての公開書き込み操作 (writeMessage) は AsyncTask を介して発生し、メイン スレッドでは発生しません。
- 次へ: すべての書き込み操作が同期され、BEGIN TRANSACTIONS にラップされます。
- 次へ: 書き込み中にブロックする必要がないため、読み取り操作は非同期です。
- 最後に: 読み取り操作の結果は、onPostExecute のメイン スレッドにキャッシュされます。
これは、UI スレッドへの影響を最小限に抑えながら、潜在的に大量のデータを SQLite データベースに書き込むための Android のベスト プラクティスを表していますか? 上記の疑似コードに明らかな同期の問題はありますか?
アップデート
上記のコードには重大なバグがあり、次のようになっています。
DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
その行は、データベースのロックを取得します。ただし、これは DEFERRED ロックであるため、書き込みが発生するまで、他のクライアントは読み取りと書き込みの両方を行うことができます。
DataManager.this.dbWriteMessage(args[0]);
その行は実際にデータベースを変更します。この時点で、ロックは RESERVED ロックであるため、他のクライアントは書き込むことができません。
最初の dbWriteMessage 呼び出しの後に、より高価な DB 書き込みが発生する可能性があることに注意してください。各書き込み操作は保護された同期メソッドで発生すると仮定します。つまり、DataManager でロックが取得され、書き込みが行われ、ロックが解放されます。WriteAsyncMessageTask が唯一のライターである場合、これで問題ありません。
ここで、書き込み操作も行うが、トランザクションを使用しない他のタスクがあると仮定しましょう (高速書き込みであるため)。これは次のようになります。
private class WriteSingleMessageAsyncTask extends AsyncTask<Message, Void, Message> {
protected Message doInBackground(Message... args) {
DataManager.this.dbWriteMessage(args[0]);
return args[0];
}
protected void onPostExecute(Message newMessages) {
if (DataManager.this.mCachedMessages != null)
DataManager.this.mCachedMessages.add(newMessages);
}
}
この場合、WriteSingleMessageAsyncTask が WriteMessageAsyncTask と同時に実行されており、WriteMessageAsyncTask が少なくとも 1 つの書き込みを既に実行している場合、WriteSingleMessageAsyncTask が dbWriteMessage を呼び出し、DataManager のロックを取得する可能性があります。 RESERVED ロック。WriteMessageAsyncTask は、DataManager のロックを繰り返し取得して放棄しています。これが問題です。
要点: トランザクションとシングルトン オブジェクト レベルのロックを組み合わせると、デッドロックが発生する可能性があります。トランザクションを開始する前に、オブジェクト レベルのロックがあることを確認してください。
元の WriteMessageAsyncTask クラスの修正:
synchronized(DataManager.this) {
DataManager.this.mDb.execSQL("BEGIN TRANSACTION;");
DataManager.this.dbWriteMessage(args[0]);
// More possibly expensive DB writes
DataManager.this.mDb.execSQL("COMMIT TRANSACTION;");
}
更新 2
Google I/O 2012 のビデオをご覧ください: http://youtu.be/gbQb1PVjfqM?t=19m13s
組み込みの排他的トランザクションを利用し、 yieldIfContendedSafelyを使用する設計パターンを提案します。