Sqlite多线程读写 wal机制

在项目中遇到一个sqlite3的错误,相信很多人都遇到过,如下:

    android.database.sqlite.SQLiteDiskIOException: disk I/O error (code 1802)
     06-25 14:18:46.340 E/CrashHandler( 2840): at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method)
     06-25 14:18:46.340 E/CrashHandler( 2840): at android.database.sqlite.SQLiteConnection.executeForChangedRowCount(SQLiteConnection.java:734)
     06-25 14:18:46.340 E/CrashHandler( 2840): at android.database.sqlite.SQLiteSession.executeForChangedRowCount(SQLiteSession.java:754)
     06-25 14:18:46.340 E/CrashHandler( 2840): at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:64)
     06-25 14:18:46.340 E/CrashHandler( 2840): at android.database.sqlite.SQLiteDatabase.delete(SQLiteDatabase.java:1497)

发生这种的异常的原因大多都是因为sqlite数据库在多线程下读写造成的。查了一下相关资料,发现sqlite还有一种叫wal的事务机制,在此记录,以备自己复习。

下面列举Android各个版本对应的sqlite版本,其实3.0以上都用了sqlite3.7了,但android3.0以下版本的活跃用户还是有不少的。

SQLite 3.7.11:

	19-4.4-KitKat
	18-4.3-Jelly Bean
	17-4.2-Jelly Bean
	16-4.1-Jelly Bean

SQLite 3.7.4:

	15-4.0.3-Ice Cream Sandwich
	14-4.0-Ice Cream Sandwich
	13-3.2-Honeycomb
	12-3.1-Honeycomb
	11-3.0-Honeycomb

SQLite 3.6.22:

	10-2.3.3-Gingerbread
	9-2.3.1-Gingerbread
	8-2.2-Froyo

SQLite 3.5.9:

	7-2.1-Eclair
	4-1.6-Donut
	3-1.5-Cupcake

在默认情况下,sqlite使用的是rollback journal机制实现原子事务。

rollback journal机制的原理是:在修改数据库文件中的数据之前,先将修改所在分页中的数据备份在另外一个地方,然后才将修改写入到数据库文件中;如果事务失败,则将备份数据拷贝回来,撤销修改;如果事务成功,则删除备份数据,提交修改。在这种机制下,读写事务不可以同时进行。你在读的时候可以有多个线程同时读,但在写的时候不能和读或者写同时进行,也即写时只能单线程操作。要不然就可能发生上面的error。在此种机制下,我们可以给多线程的读写加上同步synchronized。在写操作时,如果是同一个事务,尽量使用beginTransaction(),让写事务成为一个原子操作。

WAL机制的原理是:修改并不直接写入到数据库文件中,而是写入到另外一个称为WAL的文件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。开启WAL模式可以提高写入数据库的速度,读和写之间不会阻塞,但是写与写之间依然是阻塞的。在项目中开启WAL模式,可以提高并发。由于使用WAL比rollback journal的模式减少了写的i/o,所以写入时速度较快,但是由于在读取数据时也需要读取WAL日志验证数据的正确性,所以读取数据相对要慢。所以大家也要根据自己应用的场景去使用这种模式。

在项目中可以通过以下方式打开wal模式。

	SQLiteDatabase dbName = dbHelper.getWritableDatabase();
	dbName.enableWriteAheadLogging();  

我们可以查看一下enableWriteAheadLoggin的源码:

	public boolean enableWriteAheadLogging() {
        synchronized (mLock) {
            throwIfNotOpenLocked();


            if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0) {
                return true;
            }


            if (isReadOnlyLocked()) {
                // WAL doesn't make sense for readonly-databases.
                // TODO: True, but connection pooling does still make sense...
                return false;
            }


            if (mConfigurationLocked.isInMemoryDb()) {
                Log.i(TAG, "can't enable WAL for memory databases.");
                return false;
            }


            // make sure this database has NO attached databases because sqlite's write-ahead-logging
            // doesn't work for databases with attached databases
            if (mHasAttachedDbsLocked) {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "this database: " + mConfigurationLocked.label
                        	+ " has attached databases. can't  enable WAL.");
                }
                return false;
            }


            mConfigurationLocked.openFlags |= ENABLE_WRITE_AHEAD_LOGGING;
            try {
                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
            } catch (RuntimeException ex) {
                mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING;
                throw ex;
            }
        }
        return true;
    }


    //需要注意的是,不能在有事务操作的时候进行关闭wal模式,不然会抛异常。
    public void disableWriteAheadLogging() {
        synchronized (mLock) {
            throwIfNotOpenLocked();


            if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) == 0) {
                return;
            }


            mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING;
            try {
                mConnectionPoolLocked.reconfigure(mConfigurationLocked);
            } catch (RuntimeException ex) {
                mConfigurationLocked.openFlags |= ENABLE_WRITE_AHEAD_LOGGING;
                throw ex;
            }
        }
    }

如果使用了wal模式,在创建数据库时,它会在数据库目录上建一个xxx.db.wal的库。

文章目录
|