最新需要给软件做数据库读写方面的优化,之前无论读写,都是用一个 SQLiteOpenHelper.getWriteableDataBase() 来操作数据库,现在需要多线程并发读写。
Android 的数据库系统用的是sqlite ,sqlite的每一个数据库其实都是一个.db文件,它的同步锁也就精确到文件级别了。
下面分析一下不同情况下,在同一个数据库文件上操作sqlite的表现:
1,多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。
先看看相关的源码
java代码
java代码
通过源码可以知道,在执行插入时,会请求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代码
所以想要多线程读,读之间没有同步锁,也得每个线程使用各自的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代码
因为它先看有没有已经创建的SQLiteDatabase,没有的话先尝试创建读写 SQLiteDatabase ,失败后才尝试创建只读SQLiteDatabase 。
所以写了个新方法,来获得只读SQLiteDatabase
java代码
使用策略:一个线程写,多个线程同时读,只用一个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,多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。
先看看相关的源码
java代码
java代码
通过源码可以知道,在执行插入时,会请求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代码
所以想要多线程读,读之间没有同步锁,也得每个线程使用各自的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代码
因为它先看有没有已经创建的SQLiteDatabase,没有的话先尝试创建读写 SQLiteDatabase ,失败后才尝试创建只读SQLiteDatabase 。
所以写了个新方法,来获得只读SQLiteDatabase
java代码
使用策略:一个线程写,多个线程同时读,只用一个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代码
结论:想要多线程并发读写,3.0以下就不要想了,3.0以上,直接设置enableWriteAheadLogging()就ok。如果还是达不到要求,就使用多个db文件吧。