SQLiteDatabase 多线程数据库读写分析与优化

    最新需要给软件做数据库读写方面的优化,之前无论读写,都是用一个 SQLiteOpenHelper.getWriteableDataBase() 来操作数据库,现在需要多线程并发读写。 Android 的数据库系统用的是sqlite ,sqlite的每一个数据库其实都是一个.db文件,它的同步锁也就精确到文件级别了。 下面分析一下不同情况下,在同一个数据库文件上操作sqlite的表现:

1,多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。
  先看看相关的源码
  java代码
  1. //SQLiteDatabase.java

  2.   public long insertWithOnConflict(String table, String nullColumnHack,
  3.   ContentValues initialValues, int conflictAlgorithm) {
  4.   if (!isOpen()) {
  5.   throw new IllegalStateException("database not open");
  6.   }
  7.   .... 省略
  8.   lock();
  9.   SQLiteStatement statement = null;
  10.   try {
  11.   statement = compileStatement(sql.toString());
  12.   // Bind the values
  13.   if (entrySet != null) {
  14.   int size = entrySet.size();
  15.   Iterator> entriesIter = 
  16. entrySet.iterator();
  17.   for (int i = 0; i < size; i++) {
  18.   Map.Entry entry = entriesIter.next();
  19.   DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());
  20.   }
  21.   }
  22.   // Run the program and then cleanup
  23.   statement.execute();
  24.   long insertedRowId = lastInsertRow();
  25.   if (insertedRowId == -1) {
  26.   Log.e(TAG, "Error inserting " + initialValues + " using " + sql);
  27.   } else {
  28.   if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
  29.   Log.v(TAG, "Inserting row " + insertedRowId + " from "
  30.   + initialValues + " using " + sql);
  31.   }
  32.   }
  33.   return insertedRowId;
  34.   } catch (SQLiteDatabaseCorruptException e) {
  35.   onCorruption();
  36.   throw e;
  37.   } finally {
  38.   if (statement != null) {
  39.   statement.close();
  40.   }
  41.   unlock();
  42.   }
  43.   }
复制代码
  java代码
  1. //SQLiteDatabase.java

  2.   private final ReentrantLock mLock = new ReentrantLock(true);
  3.   /* package */ void lock() {
  4.   if (!mLockingEnabled) return;
  5.   mLock.lock();
  6.   if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
  7.   if (mLock.getHoldCount() == 1) {
  8.   // Use elapsed real-time since the CPU may sleep when waiting for IO
  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()。可是。。。
  仔细看,发现
  [attach]/data/attachment/forum/201308/eyeandroid.com69985050700111.jpg[/attach]
  最后,查询结果是一个SQLiteCursor对象。
  SQLiteCursor保存了查询条件,但是并没有立即执行查询,而是使用了lazy的策略,在需要时加载部分数据。
  [attach]/data/attachment/forum/201308/eyeandroid.com69985050700112.jpg[/attach]
  在加载数据时,调用了SQLiteQuery的fillWindow方法,而该方法依然会调用SQLiteDatabase.lock()
  java代码
  1. /**

  2.   * Reads rows into a buffer. This method acquires the database lock.
  3.   *
  4.   * @param window The window to fill into
  5.   * @return number of total rows in the query
  6.   */
  7.   /* package */ int fillWindow(CursorWindow window,
  8.   int maxRead, int lastPos) {
  9.   long timeStart = SystemClock.uptimeMillis();
  10.   mDatabase.lock();
  11.   mDatabase.logTimeStat(mSql, timeStart, 
  12. SQLiteDatabase.GET_LOCK_LOG_PREFIX);
  13.   try {
  14.   acquireReference();
  15.   try {
  16.   window.acquireReference();
  17.   // if the start pos is not equal to 0, then most likely window is
  18.   // too small for the data set, loading by another thread
  19.   // is not safe in this situation. the native code will ignore maxRead
  20.   int numRows = native_fill_window(window, window.getStartPosition(), 
  21. mOffsetIndex,
  22.   maxRead, lastPos);
  23.   // Logging
  24.   if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
  25.   Log.d(TAG, "fillWindow(): " + mSql);
  26.   }
  27.   mDatabase.logTimeStat(mSql, timeStart);
  28.   return numRows;
  29.   } catch (IllegalStateException e){
  30.   // simply ignore it
  31.   return 0;
  32.   } catch (SQLiteDatabaseCorruptException e) {
  33.   mDatabase.onCorruption();
  34.   throw e;
  35.   } finally {
  36.   window.releaseReference();
  37.   }
  38.   } finally {
  39.   releaseReference();
  40.   mDatabase.unlock();
  41.   }
  42.   }
复制代码
  所以想要多线程读,读之间没有同步锁,也得每个线程使用各自的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代码
  1. // SQLiteOpenHelper.java

  2.   public synchronized SQLiteDatabase getReadableDatabase() {
  3.   if (mDatabase != null && mDatabase.isOpen()) {
  4.   "color:#FF0000;"> return mDatabase; // The database is already open for 
  5. business
  6.   }
  7.   if (mIsInitializing) {
  8.   throw new IllegalStateException("getReadableDatabase called 
  9. recursively");
  10.   }
  11.   try {
  12.   return getWritableDatabase();
  13.   } catch (SQLiteException e) {
  14.   if (mName == null) throw e; // Can't open a temp database read-only!
  15.   Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", 
  16. e);
  17.   }
  18.   SQLiteDatabase db = null;
  19.   try {
  20.   mIsInitializing = true;
  21.   String path = mContext.getDatabasePath(mName).getPath();
  22.   db = SQLiteDatabase.openDatabase(path, mFactory, 
  23. SQLiteDatabase.OPEN_READONLY);
  24.   if (db.getVersion() != mNewVersion) {
  25.   throw new SQLiteException("Can't upgrade read-only database from version " 
  26. +
  27.   db.getVersion() + " to " + mNewVersion + ": " + path);
  28.   }
  29.   onOpen(db);
  30.   Log.w(TAG, "Opened " + mName + " in read-only mode");
  31.   mDatabase = db;
  32.   return mDatabase;
  33.   } finally {
  34.   mIsInitializing = false;
  35.   if (db != null && db != mDatabase) db.close();
  36.   }
  37.   }
复制代码
  因为它先看有没有已经创建的SQLiteDatabase,没有的话先尝试创建读写 SQLiteDatabase ,失败后才尝试创建只读SQLiteDatabase 。
  所以写了个新方法,来获得只读SQLiteDatabase
  java代码
  1. //DbHelper.java

  2.   //DbHelper extends SQLiteOpenHelper
  3.   public SQLiteDatabase getOnlyReadDatabase() {
  4.   try{
  5.   getWritableDatabase(); //保证数据库版本最新
  6.   }catch(SQLiteException e){
  7.   Log.e(TAG, "Couldn't open " + mName + " for writing (will try 
  8. read-only):",e);
  9.   }
  10.   SQLiteDatabase db = null;
  11.   try {
  12.   String path = mContext.getDatabasePath(mName).getPath();
  13.   db = SQLiteDatabase.openDatabase(path, mFactory, 
  14. SQLiteDatabase.OPEN_READONLY);
  15.   if (db.getVersion() != mNewVersion) {
  16.   throw new SQLiteException("Can't upgrade read-only database from version " 
  17. +
  18.   db.getVersion() + " to " + mNewVersion + ": " + path);
  19.   }
  20.   onOpen(db);
  21.   readOnlyDbs.add(db);
  22.   return db;
  23.   } finally {
  24.   }
  25.   }
复制代码
  使用策略:一个线程写,多个线程同时读,只用一个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代码
  1. public DbHelper(Context context , boolean enableWAL) {

  2.   this(context, DEFAULT_DB_NAME, null, DEFAULT_VERSION);
  3.   if( enableWAL && Build.VERSION.SDK_INT >= 11){
  4.   getWritableDatabase().enableWriteAheadLogging();
  5.   }
  6.   }
复制代码

结论:想要多线程并发读写,3.0以下就不要想了,3.0以上,直接设置enableWriteAheadLogging()就ok。如果还是达不到要求,就使用多个db文件吧。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值