android 多執行緒資料庫讀寫分析與優化
最新需要給軟體做資料庫讀寫方面的優化,之前無論讀寫,都是用一個 SQLiteOpenHelper.getWriteableDataBase() 來操作資料庫,現在需要多執行緒併發讀寫,專案用的是2.2的SDK。
android 的資料庫系統用的是sqlite ,sqlite的每一個數據庫其實都是一個.db檔案,它的同步鎖也就精確到資料庫級了,不能跟別的資料庫有表鎖,行鎖。
所以對寫實在有要求的,可以使用多個數據庫檔案。
哎,這資料庫在多執行緒併發讀寫方面本身就挺操蛋的。
下面分析一下不同情況下,在同一個資料庫檔案上操作,sqlite的表現。
測試程式在2.2虛擬手機,4.2.1虛擬手機,4.2.1真手機上跑。
1,多執行緒寫,使用一個SQLiteOpenHelper。也就保證了多執行緒使用一個SQLiteDatabase。
先看看相關的原始碼
[java]view plaincopy
//SQLiteDatabase.java
publiclonginsertWithOnConflict(Stringtable,StringnullColumnHack,
ContentValuesinitialValues,intconflictAlgorithm){
if(!isOpen()){
thrownewIllegalStateException("databasenotopen");
}
....省略
lock();
SQLiteStatementstatement=null;
try{
statement=compileStatement(sql.toString());
//Bindthevalues
if(entrySet!=null){
intsize=entrySet.size();
Iterator<Map.Entry<String,Object>>entriesIter=entrySet.iterator();
for(inti=0;i<size;i++){
Map.Entry<String,Object>entry=entriesIter.next();
DatabaseUtils.bindObjectToProgram(statement,i+1,entry.getValue());
}
}
//Runtheprogramandthencleanup
statement.execute();
longinsertedRowId=lastInsertRow();
if(insertedRowId==-1){
Log.e(TAG,"Errorinserting"+initialValues+"using"+sql);
}else{
if(Config.LOGD&&Log.isLoggable(TAG,Log.VERBOSE)){
Log.v(TAG,"Insertingrow"+insertedRowId+"from"
+initialValues+"using"+sql);
}
}
returninsertedRowId;
}catch(SQLiteDatabaseCorruptExceptione){
onCorruption();
throwe;
}finally{
if(statement!=null){
statement.close();
}
unlock();
}
}
[java]view plaincopy
//SQLiteDatabase.java
privatefinalReentrantLockmLock=newReentrantLock(true);
/*package*/voidlock(){
if(!mLockingEnabled)return;
mLock.lock();
if(SQLiteDebug.DEBUG_LOCK_TIME_TRACKING){
if(mLock.getHoldCount()==1){
//Useelapsedreal-timesincetheCPUmaysleepwhenwaitingforIO
mLockAcquiredWallTime=SystemClock.elapsedRealtime();
mLockAcquiredThreadTime=Debug.threadCpuTimeNanos();
}
}
}
通過原始碼可以知道,在執行插入時,會請求SQLiteDatabase物件的成員物件 mlock 的鎖,來保證插入不會併發執行。
經測試不會引發異常。
但是我們可以通過使用多個SQLiteDatabase物件同時插入,來繞過這個鎖。
2,多執行緒寫,使用多個SQLiteOpenHelper,插入時可能引發異常,導致插入錯誤。
E/Database(1471): android.database.sqlite.SQLiteException: error code 5: database is locked08-01
E/Database(1471): at android.database.sqlite.SQLiteStatement.native_execute(Native Method)
E/Database(1471): at android.database.sqlite.SQLiteStatement.execute(SQLiteStatement.java:55)
E/Database(1471): at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1549)
多執行緒寫,每個執行緒使用一個SQLiteOpenHelper,也就使得每個執行緒使用一個SQLiteDatabase物件。多個執行緒同時執行insert, 最後呼叫到本地方法 SQLiteStatement.native_execute
丟擲異常,可見android 框架,多執行緒寫資料庫的本地方法裡沒有同步鎖保護,併發寫會丟擲異常。
所以,多執行緒寫必須使用同一個SQLiteOpenHelper物件。
3,多執行緒讀
看SQLiteDatabase的原始碼可以知道,insert , update , execSQL 都會 呼叫lock(), 乍一看唯有query 沒有呼叫lock()。可是。。。
仔細看,發現
最後,查詢結果是一個SQLiteCursor物件。
SQLiteCursor儲存了查詢條件,但是並沒有立即執行查詢,而是使用了lazy的策略,在需要時載入部分資料。
在載入資料時,呼叫了SQLiteQuery的fillWindow方法,而該方法依然會呼叫SQLiteDatabase.lock()
[java]view plaincopy
/**
*Readsrowsintoabuffer.Thismethodacquiresthedatabaselock.
*
*@paramwindowThewindowtofillinto
*@returnnumberoftotalrowsinthequery
*/
/*package*/intfillWindow(CursorWindowwindow,
intmaxRead,intlastPos){
longtimeStart=SystemClock.uptimeMillis();
mDatabase.lock();
mDatabase.logTimeStat(mSql,timeStart,SQLiteDatabase.GET_LOCK_LOG_PREFIX);
try{
acquireReference();
try{
window.acquireReference();
//ifthestartposisnotequalto0,thenmostlikelywindowis
//toosmallforthedataset,loadingbyanotherthread
//isnotsafeinthissituation.thenativecodewillignoremaxRead
intnumRows=native_fill_window(window,window.getStartPosition(),mOffsetIndex,
maxRead,lastPos);
//Logging
if(SQLiteDebug.DEBUG_SQL_STATEMENTS){
Log.d(TAG,"fillWindow():"+mSql);
}
mDatabase.logTimeStat(mSql,timeStart);
returnnumRows;
}catch(IllegalStateExceptione){
//simplyignoreit
return0;
}catch(SQLiteDatabaseCorruptExceptione){
mDatabase.onCorruption();
throwe;
}finally{
window.releaseReference();
}
}finally{
releaseReference();
mDatabase.unlock();
}
}
所以想要多執行緒讀,讀之間沒有同步鎖,也得每個執行緒使用各自的SQLiteOpenHelper物件,經測試,沒有問題。
4,多執行緒讀寫
我們最終想要達到的目的,是多執行緒併發讀寫
多執行緒寫之前已經知道結果了,同一時間只能有一個寫。
多執行緒讀可以併發
所以,使用下面的策略:
一個執行緒寫,多個執行緒同時讀,每個執行緒都用各自SQLiteOpenHelper。
這樣,在java層,所有執行緒之間都不會鎖住,也就是說,寫與讀之間不會鎖,讀與讀之間也不會鎖。
發現有插入異常。
E/SQLiteDatabase(18263): Error inserting descreption=InsertThread#01375493606407
E/SQLiteDatabase(18263): android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
E/SQLiteDatabase(18263): at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
插入異常,說明在有執行緒讀的時候寫資料庫,會丟擲異常。
分析原始碼可以知道, SQLiteOpenHelper.getReadableDatabase() 不見得獲得的就是隻讀SQLiteDatabase 。
[java]view plaincopy
//SQLiteOpenHelper.java
publicsynchronizedSQLiteDatabasegetReadableDatabase(){
if(mDatabase!=null&&mDatabase.isOpen()){
<spanstyle="color:#FF0000;">returnmDatabase;</span>//Thedatabaseisalreadyopenforbusiness
}
if(mIsInitializing){
thrownewIllegalStateException("getReadableDatabasecalledrecursively");
}
try{
returngetWritableDatabase();
}catch(SQLiteExceptione){
if(mName==null)throwe;//Can'topenatempdatabaseread-only!
Log.e(TAG,"Couldn'topen"+mName+"forwriting(willtryread-only):",e);
}
SQLiteDatabasedb=null;
try{
mIsInitializing=true;
Stringpath=mContext.getDatabasePath(mName).getPath();
db=SQLiteDatabase.openDatabase(path,mFactory,SQLiteDatabase.OPEN_READONLY);
if(db.getVersion()!=mNewVersion){
thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+
db.getVersion()+"to"+mNewVersion+":"+path);
}
onOpen(db);
Log.w(TAG,"Opened"+mName+"inread-onlymode");
mDatabase=db;
returnmDatabase;
}finally{
mIsInitializing=false;
if(db!=null&&db!=mDatabase)db.close();
}
}
因為它先看有沒有已經建立的SQLiteDatabase,沒有的話先嚐試建立讀寫 SQLiteDatabase ,失敗後才嘗試建立只讀SQLiteDatabase 。
所以寫了個新方法,來獲得只讀SQLiteDatabase
[java]view plaincopy
//DbHelper.java
//DbHelperextendsSQLiteOpenHelper
publicSQLiteDatabasegetOnlyReadDatabase(){
try{
getWritableDatabase();//保證資料庫版本最新
}catch(SQLiteExceptione){
Log.e(TAG,"Couldn'topen"+mName+"forwriting(willtryread-only):",e);
}
SQLiteDatabasedb=null;
try{
Stringpath=mContext.getDatabasePath(mName).getPath();
db=SQLiteDatabase.openDatabase(path,mFactory,SQLiteDatabase.OPEN_READONLY);
if(db.getVersion()!=mNewVersion){
thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+
db.getVersion()+"to"+mNewVersion+":"+path);
}
onOpen(db);
readOnlyDbs.add(db);
returndb;
}finally{
}
}
使用策略:一個執行緒寫,多個執行緒同時讀,只用一個SQLiteOpenHelper,讀執行緒使用自己寫的getOnlyReadDatabase()方法獲得只讀。
但是經過測試,還是會丟擲異常,2.2上只有插入異常,4.1.2上甚至還有讀異常。
4.1.2上測試,讀異常。
E/SQLiteLog(18263): (5) database is locked
W/dalvikvm(18263): threadid=21: thread exiting with uncaught exception (group=0x41e2c300)
E/AndroidRuntime(18263): FATAL EXCEPTION: onlyReadThread#8
E/AndroidRuntime(18263): android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5): , while compiling: SELECT * FROM test_t
看來此路不同啊。
其實SQLiteDataBase 在API 11 多了一個 屬性ENABLE_WRITE_AHEAD_LOGGING。
可以打,enableWriteAheadLogging(),可以關閉disableWriteAheadLogging(),預設是關閉的。
這個屬性是什麼意思呢?
參考api文件,這個屬性關閉時,不允許讀,寫同時進行,通過 鎖 來保證。
當開啟時,它允許一個寫執行緒與多個讀執行緒同時在一個SQLiteDatabase上起作用。實現原理是寫操作其實是在一個單獨的檔案,不是原資料庫檔案。所以寫在執行時,不會影響讀操作,讀操作讀的是原資料檔案,是寫操作開始之前的內容。
在寫操作執行成功後,會把修改合併會原資料庫檔案。此時讀操作才能讀到修改後的內容。但是這樣將花費更多的記憶體。
有了它,多執行緒讀寫問題就解決了,可惜只能在API 11 以上使用。
所以只能判斷sdk版本,如果3.0以上,就開啟這個屬性
[java]view plaincopy
publicDbHelper(Contextcontext,booleanenableWAL){
this(context,DEFAULT_DB_NAME,null,DEFAULT_VERSION);
if(enableWAL&&Build.VERSION.SDK_INT>=11){
getWritableDatabase().enableWriteAheadLogging();
}
}
關於SQLiteDatabase的這個屬性,參考api文件,也可以看看SQLiteSession.java裡對多執行緒資料庫讀寫的描述。
結論
想要多執行緒併發讀寫,3.0以下就不要想了,3.0以上,直接設定enableWriteAheadLogging()就ok。
如果還是達不到要求,就使用多個db檔案吧。
另:
單位有一個三星 note2手機,上面所有的例子跑起來都啥問題也沒有。。。。很好很強大。
最後,附上我的測試程式。
https://github.com/zebulon988/SqliteTest.git
轉載於:https://my.oschina.net/tingzi/blog/190744