1. 程式人生 > 實用技巧 >android 多執行緒資料庫讀寫分析與優化

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

  1. //SQLiteDatabase.java

  2. publiclonginsertWithOnConflict(Stringtable,StringnullColumnHack,

  3. ContentValuesinitialValues,intconflictAlgorithm){

  4. if(!isOpen()){

  5. thrownewIllegalStateException("databasenotopen");

  6. }

  7. ....省略

  8. lock();

  9. SQLiteStatementstatement=null;

  10. try{

  11. statement=compileStatement(sql.toString());

  12. //Bindthevalues

  13. if(entrySet!=null){

  14. intsize=entrySet.size();

  15. Iterator<Map.Entry<String,Object>>entriesIter=entrySet.iterator();

  16. for(inti=0;i<size;i++){

  17. Map.Entry<String,Object>entry=entriesIter.next();

  18. DatabaseUtils.bindObjectToProgram(statement,i+1,entry.getValue());

  19. }

  20. }

  21. //Runtheprogramandthencleanup

  22. statement.execute();

  23. longinsertedRowId=lastInsertRow();

  24. if(insertedRowId==-1){

  25. Log.e(TAG,"Errorinserting"+initialValues+"using"+sql);

  26. }else{

  27. if(Config.LOGD&&Log.isLoggable(TAG,Log.VERBOSE)){

  28. Log.v(TAG,"Insertingrow"+insertedRowId+"from"

  29. +initialValues+"using"+sql);

  30. }

  31. }

  32. returninsertedRowId;

  33. }catch(SQLiteDatabaseCorruptExceptione){

  34. onCorruption();

  35. throwe;

  36. }finally{

  37. if(statement!=null){

  38. statement.close();

  39. }

  40. unlock();

  41. }

  42. }



[java]view plaincopy

  1. //SQLiteDatabase.java

  2. privatefinalReentrantLockmLock=newReentrantLock(true);

  3. /*package*/voidlock(){

  4. if(!mLockingEnabled)return;

  5. mLock.lock();

  6. if(SQLiteDebug.DEBUG_LOCK_TIME_TRACKING){

  7. if(mLock.getHoldCount()==1){

  8. //Useelapsedreal-timesincetheCPUmaysleepwhenwaitingforIO

  9. mLockAcquiredWallTime=SystemClock.elapsedRealtime();

  10. mLockAcquiredThreadTime=Debug.threadCpuTimeNanos();

  11. }

  12. }

  13. }


通過原始碼可以知道,在執行插入時,會請求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

  1. /**

  2. *Readsrowsintoabuffer.Thismethodacquiresthedatabaselock.

  3. *

  4. *@paramwindowThewindowtofillinto

  5. *@returnnumberoftotalrowsinthequery

  6. */

  7. /*package*/intfillWindow(CursorWindowwindow,

  8. intmaxRead,intlastPos){

  9. longtimeStart=SystemClock.uptimeMillis();

  10. mDatabase.lock();

  11. mDatabase.logTimeStat(mSql,timeStart,SQLiteDatabase.GET_LOCK_LOG_PREFIX);

  12. try{

  13. acquireReference();

  14. try{

  15. window.acquireReference();

  16. //ifthestartposisnotequalto0,thenmostlikelywindowis

  17. //toosmallforthedataset,loadingbyanotherthread

  18. //isnotsafeinthissituation.thenativecodewillignoremaxRead

  19. intnumRows=native_fill_window(window,window.getStartPosition(),mOffsetIndex,

  20. maxRead,lastPos);

  21. //Logging

  22. if(SQLiteDebug.DEBUG_SQL_STATEMENTS){

  23. Log.d(TAG,"fillWindow():"+mSql);

  24. }

  25. mDatabase.logTimeStat(mSql,timeStart);

  26. returnnumRows;

  27. }catch(IllegalStateExceptione){

  28. //simplyignoreit

  29. return0;

  30. }catch(SQLiteDatabaseCorruptExceptione){

  31. mDatabase.onCorruption();

  32. throwe;

  33. }finally{

  34. window.releaseReference();

  35. }

  36. }finally{

  37. releaseReference();

  38. mDatabase.unlock();

  39. }

  40. }


所以想要多執行緒讀,讀之間沒有同步鎖,也得每個執行緒使用各自的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

  1. //SQLiteOpenHelper.java

  2. publicsynchronizedSQLiteDatabasegetReadableDatabase(){

  3. if(mDatabase!=null&&mDatabase.isOpen()){

  4. <spanstyle="color:#FF0000;">returnmDatabase;</span>//Thedatabaseisalreadyopenforbusiness

  5. }

  6. if(mIsInitializing){

  7. thrownewIllegalStateException("getReadableDatabasecalledrecursively");

  8. }

  9. try{

  10. returngetWritableDatabase();

  11. }catch(SQLiteExceptione){

  12. if(mName==null)throwe;//Can'topenatempdatabaseread-only!

  13. Log.e(TAG,"Couldn'topen"+mName+"forwriting(willtryread-only):",e);

  14. }

  15. SQLiteDatabasedb=null;

  16. try{

  17. mIsInitializing=true;

  18. Stringpath=mContext.getDatabasePath(mName).getPath();

  19. db=SQLiteDatabase.openDatabase(path,mFactory,SQLiteDatabase.OPEN_READONLY);

  20. if(db.getVersion()!=mNewVersion){

  21. thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+

  22. db.getVersion()+"to"+mNewVersion+":"+path);

  23. }

  24. onOpen(db);

  25. Log.w(TAG,"Opened"+mName+"inread-onlymode");

  26. mDatabase=db;

  27. returnmDatabase;

  28. }finally{

  29. mIsInitializing=false;

  30. if(db!=null&&db!=mDatabase)db.close();

  31. }

  32. }

因為它先看有沒有已經建立的SQLiteDatabase,沒有的話先嚐試建立讀寫 SQLiteDatabase ,失敗後才嘗試建立只讀SQLiteDatabase 。

所以寫了個新方法,來獲得只讀SQLiteDatabase


[java]view plaincopy

  1. //DbHelper.java

  2. //DbHelperextendsSQLiteOpenHelper

  3. publicSQLiteDatabasegetOnlyReadDatabase(){

  4. try{

  5. getWritableDatabase();//保證資料庫版本最新

  6. }catch(SQLiteExceptione){

  7. Log.e(TAG,"Couldn'topen"+mName+"forwriting(willtryread-only):",e);

  8. }

  9. SQLiteDatabasedb=null;

  10. try{

  11. Stringpath=mContext.getDatabasePath(mName).getPath();

  12. db=SQLiteDatabase.openDatabase(path,mFactory,SQLiteDatabase.OPEN_READONLY);

  13. if(db.getVersion()!=mNewVersion){

  14. thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+

  15. db.getVersion()+"to"+mNewVersion+":"+path);

  16. }

  17. onOpen(db);

  18. readOnlyDbs.add(db);

  19. returndb;

  20. }finally{

  21. }

  22. }


使用策略:一個執行緒寫,多個執行緒同時讀,只用一個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

  1. publicDbHelper(Contextcontext,booleanenableWAL){

  2. this(context,DEFAULT_DB_NAME,null,DEFAULT_VERSION);

  3. if(enableWAL&&Build.VERSION.SDK_INT>=11){

  4. getWritableDatabase().enableWriteAheadLogging();

  5. }

  6. }


關於SQLiteDatabase的這個屬性,參考api文件,也可以看看SQLiteSession.java裡對多執行緒資料庫讀寫的描述。

SQLiteSession.java


結論

想要多執行緒併發讀寫,3.0以下就不要想了,3.0以上,直接設定enableWriteAheadLogging()就ok。

如果還是達不到要求,就使用多個db檔案吧。


另:

單位有一個三星 note2手機,上面所有的例子跑起來都啥問題也沒有。。。。很好很強大。


最後,附上我的測試程式。

https://github.com/zebulon988/SqliteTest.git


轉載於:https://my.oschina.net/tingzi/blog/190744