sqlite之多线程总结
12.android 多线程数据库读写分析与优化
11.多线程操作Sqlite?
====
11.android 多线程数据库读写分析与优化
最新需要给软件做数据库读写方面的优化,之前无论读写,都是用一个 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 SQLiteOpenHelper
public 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里对多线程数据库读写的描述。
结论
想要多线程并发读写,3.0以下就不要想了,3.0以上,直接设置enableWriteAheadLogging()就ok。
如果还是达不到要求,就使用多个db文件吧。
另:单位有一个三星 note2手机,上面所有的例子跑起来都啥问题也没有。。。。很好很强大。
最后,附上我的测试程序。https://github.com/zebulon988/SqliteTest.git
sqlite之多线程总结的更多相关文章
- SQLite在多线程环境下的应用
文一 SQLite的FAQ里面已经专门说明,先贴出来.供以后像我目前的入门者学习. (7) 多个应用程序或者同一个应用程序的多个例程能同时存取同一个数据库文件吗? 多进程可以同时打开同一个数据库,也可 ...
- sqlite:多线程操作数据库“database is locked”解决方法(二)
上一篇博客<sqlite:多线程操作数据库“database is locked”解决方法>通过注册延时函数的方法来处理数据库被锁的问题.此方法固然能解决问题,但是在多个线程向数据库写入大 ...
- sqlite:多线程操作数据库“database is locked”解决方法
1. 使sqlite支持多线程(不确定是否非加不可,暂且加上,以备后患) 可以在编译时/启动时/运行时选择线程模式,参考:http://www.cnblogs.com/liaj/p/4015219.h ...
- sqlite与多线程
数据库支持三种线程模式 Single-thread. In this mode, all mutexes are disabled and SQLite is unsafe to use in mor ...
- Android中Sqlite数据库多线程并发问题
最近在做一个Android项目, 为了改善用户体验,把原先必须让用户“等待”的过程改成在新线程中异步执行.但是这样做遇到了多个线程同时需要写Sqlite数据库,导致操作数据库失败. 本人对Java并不 ...
- SQLite数据库在多线程写锁文件的解决办法
参考了很多SQLITE数据库多线程的解决办法 我自己写了一个SQLITEHELPER 来解决这个问题 希望大家多多指教 调用的时候 SQLLiteDBHelper _SQLLiteDBHelper ...
- Sqlite学习笔记(二)&&性能测试
测试目标 获取SQlite的常规性能指标 测试环境 CPU:8核,Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz 内存:16G 磁盘:SSD Linux 2.6.32 ...
- sqlite3 多线程和锁 ,优化插入速度及性能优化
一. 是否支持多线程? SQLite官网上的"Is SQLite threadsafe?"这个问答. 简单来说,从3.3.1版本开始,它就是线程安全的了.而iOS的SQLite ...
- sqlite线程模式的设置
(1)编译阶段 这几种模式可以通过参数SQLITE_THREADSAFE在编译阶段指定,可以取值0,1,2,默认是1.这三种取值的含义如下: 0:单线程模式,即内部不做mutex保护,多线程运行sql ...
随机推荐
- Go Example--结构体
package main import "fmt" //定义一个私有结构体 type person struct { name string age int } func main ...
- Python知识点整理,基础5 - 文件操作
- gcc的编译属性和选项
1.指定内存默认对其参数: __attribute__((packed)):按一字节对其__attribute__((aligned(n))):从此之后默认按n字节对其 例如: struct stu ...
- terraform 几个方便的工具
几个方便的terraform 工具,方便了解terraform terraform-docs 方便的查看资源的信息(支持markdown,json 格式),对于ci/cd 很方便 项目地址 https ...
- nginx unit nodejs 模块试用(续)
最新(应该是18 年了)nginx unit 发布了新的版本,对于nodejs 的支持有很大的改进,上次测试过,问题还是 比较多,这次使用新版本在测试下对于nodejs 的支持,以及以前block ...
- graalvm 简单试用
安装 地址 http://www.graalvm.org/downloads/ 配置环境变量 linux: export PATH=/path/to/graalvm/bin:$PATH mac: ...
- thinkphp5 列表页数据分页查询-带搜索条件
一.控制器部分 <?php namespace app\user\controller; use app\user\model\HelpCenterManual as HelpCenterMan ...
- 八、springboot(五)配置定时器
1.修改启动类 @SpringBootApplication @EnableScheduling public class DemoApplication { public static void m ...
- 怎么理解Python画图中的X,y
X_outliers=np.array([[3.4, 1.3], [3.2, 0.8]]) y_outliers=np.array([0, 0]) 要明白X,y不再是我们高中时候学的x,y轴的坐标:首 ...
- LOJ 2339 「WC2018」通道——边分治+虚树
题目:https://loj.ac/problem/2339 两棵树的话,可以用 CTSC2018 暴力写挂的方法,边分治+虚树.O(nlogn). 考虑怎么在这个方法上再加一棵树.发现很难弄. 看了 ...