android 多線程資料庫讀寫分析與最佳化

來源:互聯網
上載者:User
文章目錄
  • 1,多線程寫,使用一個SQLiteOpenHelper。也就保證了多線程使用一個SQLiteDatabase。
  • 2,多線程寫,使用多個SQLiteOpenHelper,插入時可能引發異常,導致插入錯誤。
  • 3,多線程讀
  • 4,多線程讀寫

最新需要給軟體做資料庫讀寫方面的最佳化,之前無論讀寫,都是用一個 SQLiteOpenHelper.getWriteableDataBase() 來操作資料庫,現在需要多線程並發讀寫,項目用的是2.2的SDK。

android 的資料庫系統用的是sqlite ,sqlite的每一個資料庫其實都是一個.db檔案,它的同步鎖也就精確到資料庫級了,不能跟別的資料庫有表鎖,行鎖。

所以對寫實在有要求的,可以使用多個資料庫檔案。

哎,這資料庫在多線程並發讀寫方面本身就挺操蛋的。

下面分析一下不同情況下,在同一個資料庫檔案上操作,sqlite的表現。

測試程式在2.2虛擬手機,4.2.1虛擬手機,4.2.1真手機上跑。

1,多線程寫,使用一個SQLiteOpenHelper。也就保證了多線程使用一個SQLiteDatabase。

先看看相關的源碼

//SQLiteDatabase.java public long insertWithOnConflict(String table, String nullColumnHack,            ContentValues initialValues, int conflictAlgorithm) {        if (!isOpen()) {            throw new IllegalStateException("database not open");        }        .... 省略        lock();        SQLiteStatement statement = null;        try {            statement = compileStatement(sql.toString());            // Bind the values            if (entrySet != null) {                int size = entrySet.size();                Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();                for (int i = 0; i < size; i++) {                    Map.Entry<String, Object> entry = entriesIter.next();                    DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());                }            }            // Run the program and then cleanup            statement.execute();            long insertedRowId = lastInsertRow();            if (insertedRowId == -1) {                Log.e(TAG, "Error inserting " + initialValues + " using " + sql);            } else {                if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {                    Log.v(TAG, "Inserting row " + insertedRowId + " from "                            + initialValues + " using " + sql);                }            }            return insertedRowId;        } catch (SQLiteDatabaseCorruptException e) {            onCorruption();            throw e;        } finally {            if (statement != null) {                statement.close();            }            unlock();        }    }
//SQLiteDatabase.java  private final ReentrantLock mLock = new ReentrantLock(true);/* package */ void lock() {       if (!mLockingEnabled) return;              mLock.lock();              if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {                  if (mLock.getHoldCount() == 1) {                        // Use elapsed real-time since the CPU may sleep when waiting for IO                       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()

  /**     * Reads rows into a buffer. This method acquires the database lock.     *     * @param window The window to fill into     * @return number of total rows in the query     */    /* package */ int fillWindow(CursorWindow window,            int maxRead, int lastPos) {        long timeStart = SystemClock.uptimeMillis();        mDatabase.lock();        mDatabase.logTimeStat(mSql, timeStart, SQLiteDatabase.GET_LOCK_LOG_PREFIX);        try {            acquireReference();            try {                window.acquireReference();                // if the start pos is not equal to 0, then most likely window is                // too small for the data set, loading by another thread                // is not safe in this situation. the native code will ignore maxRead                int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex,                        maxRead, lastPos);                // Logging                if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {                    Log.d(TAG, "fillWindow(): " + mSql);                }                mDatabase.logTimeStat(mSql, timeStart);                return numRows;            } catch (IllegalStateException e){                // simply ignore it                return 0;            } catch (SQLiteDatabaseCorruptException e) {                mDatabase.onCorruption();                throw e;            } 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 。

//  SQLiteOpenHelper.java  public synchronized SQLiteDatabase getReadableDatabase() {        if (mDatabase != null && mDatabase.isOpen()) {            return mDatabase;  // The database is already open for business        }        if (mIsInitializing) {            throw new IllegalStateException("getReadableDatabase called recursively");        }        try {            return getWritableDatabase();        } catch (SQLiteException e) {            if (mName == null) throw e;  // Can't open a temp database read-only!            Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);        }        SQLiteDatabase db = null;        try {            mIsInitializing = true;            String path = mContext.getDatabasePath(mName).getPath();            db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);            if (db.getVersion() != mNewVersion) {                throw new SQLiteException("Can't upgrade read-only database from version " +                        db.getVersion() + " to " + mNewVersion + ": " + path);            }            onOpen(db);            Log.w(TAG, "Opened " + mName + " in read-only mode");            mDatabase = db;            return mDatabase;        } finally {            mIsInitializing = false;            if (db != null && db != mDatabase) db.close();        }    }

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

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

//DbHelper.java //DbHelper extends SQLiteOpenHelperpublic SQLiteDatabase getOnlyReadDatabase() {    try{    getWritableDatabase(); //保證資料庫版本最新    }catch(SQLiteException e){    Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):",e);    }            SQLiteDatabase db = null;        try {            String path = mContext.getDatabasePath(mName).getPath();            db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);            if (db.getVersion() != mNewVersion) {                throw new SQLiteException("Can't upgrade read-only database from version " +                        db.getVersion() + " to " + mNewVersion + ": " + path);            }            onOpen(db);            readOnlyDbs.add(db);            return db;        } 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以上,就開啟這個屬性

public DbHelper(Context context , boolean enableWAL) {this(context, DEFAULT_DB_NAME, null, DEFAULT_VERSION);if( enableWAL && Build.VERSION.SDK_INT >= 11){getWritableDatabase().enableWriteAheadLogging();}}

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

SQLiteSession.java

結論

想要多線程並發讀寫,3.0以下就不要想了,3.0以上,直接設定enableWriteAheadLogging()就ok。

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

另:

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

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

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

獨家之言,如有問題請回複我,謝謝!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.