SQLite在Android一般應用中還是比較常用,早期的時候碰到過不少坑,其中最煩的就是多線程並發讀寫問題,今天正好整理一下,做個筆記,也歡迎指正、討論和補充。
一、查詢優化
1、wal模式
開啟wal模式,可以實現並發讀,且讀寫不阻塞,當然寫與寫之間仍然阻塞,該模式需要android3.0+才支持。
當開啟了wal模式更新數據時,會先將數據寫入到*.db-wal文件中,而不是直接修改數據庫文件,當執行checkpoint時或某個時間點才會將數據更新到數據庫文件(執行endTransaction時會提交checkpoint)。當出現rollback也只是清除wal日志文件,而ROLLBACK JOURNAL模式,也就是關閉wal模式時,當數據有更新時,先將需要修改的數據備份到journal文件中,然后修改數據庫文件,當發生rollback,從journal日志中取出數據,並修改數據庫文件,然后清除journal日志。 從以上流程來看wal在數據更新上I/O量要小,所以寫操作要快。由於在讀取數據時也需要讀取wal日志驗證數據的正確性,所以讀取數據相對要慢,但使用wal還是提高了讀取的並發性。
開啟wal模式后,一定要使用beginTransactionNonExclusive來提交事務。db.beginTransaction()相當於execSQL("BEGIN EXCLUSIVE;"),在當前事務沒有結束之前任何其他線程或進程都無法對數據庫進行讀寫操作。當開啟wal模式時,使用db.beginTransactionNonExclusive(),相當於execSQL("BEGIN IMMEDIATE;"),只會限制其他線程對數據庫的寫操作,不會阻塞讀操作。
2、建立索引,推薦看這個文章,足夠了解索引的簡單使用和優點了http://www.trinea.cn/android/database-performance/,總而言之,索引會增加SQLite體積,且增刪改時也要維護索引,會對增刪改性能存在一定影響,如果數據量不大,不建議使用。使用時一定要根據需求建立合適的索引,勿濫用。
3、當某張表可預見數據量很大時,可以適當的進行表的細化、后期可以分表分庫,查詢時也可以使用異步查詢。
二、批量插入優化
1、事務提交
批量插入,包括更新刪除,一定要加事務,如果不加事務,則默認會為每一次插入開啟一個事務並自動提交,是非常慢的。
2、開啟wal模式,參見上文中解釋;
3、SQLiteStatement優化
我們每次執行的sql語句最終會轉化為一個SQLiteStatement對象來進行處理,可以預先使用db.compileStatement方法獲取SQLiteStatement對象並重用,而不是讓系統每次insert都構造一個對應的SQLiteStatement對象,這樣能夠提高內存的使用率。
補充:網上有人解釋“
比如insert into xxx,一般情況下執行多少次,就要編譯多少次
”,關於這點,首先我認為不對,我闡述一下我自己的分析:SQLite想要執行操作,需要將程序中的sql語句進行“預編譯”。例如批量插入,我們可以使用“顯式預編譯”來做到重用SQLiteStatement,也就是使用compileStatement方法。其實重點在於SQLiteStatement對象在new時,會通過SQLiteSession獲取連接池中某個SQLiteConnection,通過調用SQLiteConnection的prepare方法,會從PreparedStatement鏈表中獲取,如果沒有可重用的則會創建一個PreparedStatement對象,其中會做一些native操作,例如給PreparedStatement的mStatementPtr賦值,通過注釋,我們可以了解到這個mStatementPtr就是一個指向sqlite3_stmt類型的指針,而sqlite3_stmt是sqlite自己內部的數據結構,用來記錄“sql語句”,這個sql語句是解析后的,也就是“預編譯”后的。
一句話,就是new SQLiteStatement會對sql做預編譯,如果已經預編譯過,會直接從緩存鏈表中拿。
其實從上面分析,我們知道每個SQLiteConnection都包含一個鏈表結構的PreparedStatemnt對象集合,每次獲取SQLiteConnection都會優先找到包含sql預編譯的PreparedStatement實例的數據庫連接,這樣就不會每次都去預編譯sql。所以除非這個connection剛好被其他線程拿去用了,否則都獲取相同的connection,不用重復預編譯。也就是說在已經執行過一次預編譯(生成PreparedStatement實例)的SQLiteConnection中,不會再反復預編譯,即使你inser into n次,而導致你需要重新預編譯sql的情況是SQLiteConnection恰巧被其他線程使用,就會重新acquirePreparedStatement。
private PreparedStatement acquirePreparedStatement(String sql) { PreparedStatement statement = mPreparedStatementCache.get(sql); boolean skipCache = false; if (statement != null) { if (!statement.mInUse) { return statement; } // The statement is already in the cache but is in use (this statement appears // to be not only re-entrant but recursive!). So prepare a new copy of the // statement but do not cache it. skipCache = true; } final long statementPtr = nativePrepareStatement(mConnectionPtr, sql); try { final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr); final int type = DatabaseUtils.getSqlStatementType(sql); final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr); statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly); if (!skipCache && isCacheable(type)) { mPreparedStatementCache.put(sql, statement); statement.mInCache = true; } } catch (RuntimeException ex) { // Finalize the statement if an exception occurred and we di
4、分多個db來實現並發寫;
三、getReadableDatabase()和getWritableDatabase()
getReadableDatabase()和getWritableDatabase()首先都會嘗試以讀寫方式打開數據庫。其中getReadableDatabase()如果因為磁盤空間已滿等原因導致以讀寫方式打開數據庫失敗,會改以只讀方式打開,而getWritableDatabase()會拋異常。若只需要一個只讀的數據庫,可以使用SQLiteDatabase.OPEN_READONLY標志,通過SQLiteDatabase#openDatabase(String, CursorFactory, int)方法手動打開。
這兩個方法成功返回后,會回調onOpen()方法,且OpenHelper會緩存該數據庫實例。這兩個方法調用時,如果因為數據庫文件不存在需要創建會觸發SQLiteOpenHelper#onCreate()回調,如果因為數據庫版本不一致升或降會觸發SQLiteOpenHelper#onUpgrade()、SQLiteOpenHelper#onDowngrade()回調。
差不多就這些,歡迎大家補充和指正。最后是個人寫的SQLite輔助管理類,維護一個SQLiteOpenHelper實例,並提供全局SQLiteDatabase實例打開和關閉,避免因為多線程操作或重復打開關閉導致的database is locked、reopen and already closed等異常,並且支持wal模式。因為比較簡單,就不做單獨講解了。
public class SQLiteDbManager { private SQLiteDbManager() { } private AtomicInteger mOpenCounter = new AtomicInteger(); private static SQLiteDbManager mDatabaseHelper; private static SQLiteOpenHelper mSQLiteDbMaintain; private SQLiteDatabase mDatabase; private boolean mEnableWAL; public static void initializeInstance(SQLiteOpenHelper dbMaintain, boolean enableWAL) { if (mDatabaseHelper == null) { mDatabaseHelper = new SQLiteDbManager(); mDatabaseHelper.mEnableWAL = enableWAL; mSQLiteDbMaintain = dbMaintain; } } public static SQLiteDbManager getInstance() { return mDatabaseHelper; } public synchronized SQLiteDatabase openDatabase() { if (mOpenCounter.incrementAndGet() == 1) { try { mDatabase = mSQLiteDbMaintain.getReadableDatabase(); // 並發讀 if (mEnableWAL && Build.VERSION.SDK_INT >= 11) { mDatabase.enableWriteAheadLogging(); } } catch (SQLiteException ex) { mOpenCounter.decrementAndGet(); mDatabase = null; Logger.getInstance().error(ex.toString()); } } return mDatabase; } public void beginTransaction() { if (Build.VERSION.SDK_INT >= 11 && mEnableWAL) { mDatabase.beginTransactionNonExclusive(); return; } mDatabase.beginTransaction(); } public void beginTransactionWithListener(SQLiteTransactionListener listener) { if (Build.VERSION.SDK_INT >= 11 && mEnableWAL) { mDatabase.beginTransactionWithListenerNonExclusive(listener); return; } mDatabase.beginTransactionWithListener(listener); } public synchronized void closeDatabase() { if (mOpenCounter.decrementAndGet() == 0) { mDatabase.close(); } } }
參考鏈接: