最新需要给软件做数据库读写方面的优化,之前无论读写,都是用一个 SQLiteOpenHelper.getWriteableDataBase() 来操作数据库,现在需要多线程并发读写,项目用的是2.2的SDK。

android 的数据库系统用的是sqlite ,sqlite的每一个数据库其实都是一个.db文件,它的同步锁也就精确到数据库级了,不能跟别的数据库有表锁,行锁。

所以对写实在有要求的,可以使用多个数据库文件。

哎,这数据库在多线程并发读写方面本身就挺操蛋的。

下面分析一下不同情况下,在同一个数据库文件上操作,sqlite的表现。

测试程序在2.2虚拟手机,4.2.1虚拟手机,4.2.1真手机上跑。

1,多线程写,使用一个SQLiteOpenHelper。也就保证了多线程使用一个SQLiteDatabase。

先看看相关的源码

  1. //SQLiteDatabase.java
  2.  
  3. public long insertWithOnConflict(String table, String nullColumnHack,
  4. ContentValues initialValues, int conflictAlgorithm) {
  5. if (!isOpen()) {
  6. throw new IllegalStateException("database not open");
  7. }
  8.  
  9. .... 省略
  10.  
  11. lock();
  12. SQLiteStatement statement = null;
  13. try {
  14. statement = compileStatement(sql.toString());
  15.  
  16. // Bind the values
  17. if (entrySet != null) {
  18. int size = entrySet.size();
  19. Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
  20. for (int i = 0; i < size; i++) {
  21. Map.Entry<String, Object> entry = entriesIter.next();
  22. DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());
  23. }
  24. }
  25.  
  26. // Run the program and then cleanup
  27. statement.execute();
  28.  
  29. long insertedRowId = lastInsertRow();
  30. if (insertedRowId == -1) {
  31. Log.e(TAG, "Error inserting " + initialValues + " using " + sql);
  32. } else {
  33. if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
  34. Log.v(TAG, "Inserting row " + insertedRowId + " from "
  35. + initialValues + " using " + sql);
  36. }
  37. }
  38. return insertedRowId;
  39. } catch (SQLiteDatabaseCorruptException e) {
  40. onCorruption();
  41. throw e;
  42. } finally {
  43. if (statement != null) {
  44. statement.close();
  45. }
  46. unlock();
  47. }
  48. }
  1. //SQLiteDatabase.java
  2.  
  3.  private final ReentrantLock mLock = new ReentrantLock(true);
  4.  
  5. /* package */ void lock() {
  6.  
  7.   if (!mLockingEnabled) return;
  8.  
  9. mLock.lock();
  10.  
  11. if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
  12.  
  13. if (mLock.getHoldCount() == 1) {
  14.  
  15. // Use elapsed real-time since the CPU may sleep when waiting for IO
  16.  
  17.   mLockAcquiredWallTime = SystemClock.elapsedRealtime();
  18.  
  19. mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
  20.  
  21. }
  22.  
  23. }
  24.  
  25. }

通过源码可以知道,在执行插入时,会请求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()

  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, SQLiteDatabase.GET_LOCK_LOG_PREFIX);
  12. try {
  13. acquireReference();
  14. try {
  15. window.acquireReference();
  16. // if the start pos is not equal to 0, then most likely window is
  17. // too small for the data set, loading by another thread
  18. // is not safe in this situation. the native code will ignore maxRead
  19. int numRows = native_fill_window(window, window.getStartPosition(), mOffsetIndex,
  20. maxRead, lastPos);
  21.  
  22. // Logging
  23. if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
  24. Log.d(TAG, "fillWindow(): " + mSql);
  25. }
  26. mDatabase.logTimeStat(mSql, timeStart);
  27. return numRows;
  28. } catch (IllegalStateException e){
  29. // simply ignore it
  30. return 0;
  31. } catch (SQLiteDatabaseCorruptException e) {
  32. mDatabase.onCorruption();
  33. throw e;
  34. } finally {
  35. window.releaseReference();
  36. }
  37. } finally {
  38. releaseReference();
  39. mDatabase.unlock();
  40. }
  41. }

所以想要多线程读,读之间没有同步锁,也得每个线程使用各自的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 。

  1. // SQLiteOpenHelper.java
  2.  
  3. public synchronized SQLiteDatabase getReadableDatabase() {
  4. if (mDatabase != null && mDatabase.isOpen()) {
  5. return mDatabase; // The database is already open for business
  6. }
  7.  
  8. if (mIsInitializing) {
  9. throw new IllegalStateException("getReadableDatabase called recursively");
  10. }
  11.  
  12. try {
  13. return getWritableDatabase();
  14. } catch (SQLiteException e) {
  15. if (mName == null) throw e; // Can't open a temp database read-only!
  16. Log.e(TAG, "Couldn't open " + mName + " for writing (will try read-only):", e);
  17. }
  18.  
  19. SQLiteDatabase db = null;
  20. try {
  21. mIsInitializing = true;
  22. String path = mContext.getDatabasePath(mName).getPath();
  23. db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
  24. if (db.getVersion() != mNewVersion) {
  25. throw new SQLiteException("Can't upgrade read-only database from version " +
  26. db.getVersion() + " to " + mNewVersion + ": " + path);
  27. }
  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

  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 read-only):",e);
  8. }
  9.  
  10. SQLiteDatabase db = null;
  11. try {
  12. String path = mContext.getDatabasePath(mName).getPath();
  13. db = SQLiteDatabase.openDatabase(path, mFactory, SQLiteDatabase.OPEN_READONLY);
  14. if (db.getVersion() != mNewVersion) {
  15. throw new SQLiteException("Can't upgrade read-only database from version " +
  16. db.getVersion() + " to " + mNewVersion + ": " + path);
  17. }
  18.  
  19. onOpen(db);
  20. readOnlyDbs.add(db);
  21. return db;
  22. } finally {
  23. }
  24. }

使用策略:一个线程写,多个线程同时读,只用一个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以上,就打开这个属性

  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. }

关于SQLiteDatabase的这个属性,参考api文档,也可以看看SQLiteSession.java里对多线程数据库读写的描述。

SQLiteSession.java

结论

想要多线程并发读写,3.0以下就不要想了,3.0以上,直接设置enableWriteAheadLogging()就ok。

如果还是达不到要求,就使用多个db文件吧。

另:

单位有一个三星 note2手机,上面所有的例子跑起来都啥问题也没有。。。。很好很强大。

最后,附上我的测试程序。

https://github.com/zebulon988/SqliteTest.git

独家之言,如有问题请回复我,谢谢!

android 多线程数据库读写分析与优化的更多相关文章

  1. Android中数据库Sqlite的性能优化

    1.索引简单的说,索引就像书本的目录,目录可以快速找到所在页数,数据库中索引可以帮助快速找到数据,而不用全表扫描,合适的索引可以大大提高数据库查询的效率.(1). 优点大大加快了数据库检索的速度,包括 ...

  2. 【转载】Android App应用启动分析与优化

    前言: 昨晚新版本终于发布了,但是还是记得有测试反馈app启动好长时间也没进入app主页,所以今天准备加个班总结一下App启动那些事! app的启动方式: 1.)冷启动  当启动应用时,后台没有该应用 ...

  3. 【阿里云产品公测】利用PTS服务优化网站数据库读写性能

    [阿里云产品公测]利用PTS服务优化网站数据库读写性能 作者:阿里云用户千鸟 写这个帖子主要也是因为在用PTS测试网站的时候,手动访问网站进入报错页面,主要原因是数据库连接对象存在问题,导致并发多的时 ...

  4. Android多线程分析之四:MessageQueue的实现

    Android多线程分析之四:MessageQueue的实现 罗朝辉 (http://www.cnblogs.com/kesalin/) CC 许可,转载请注明出处 在前面两篇文章<Androi ...

  5. Android多线程任务优化1:探讨AsyncTask的缺陷

     AsyncTask还有别的缺陷,在生成listview的时候,如果adapter里面的count动态改变的话,不能使用AsyncTask,只能使用Thread+Handler,否则会出现如下错误 j ...

  6. 一:MySQL数据库的性能的影响分析及其优化

    MySQL数据库的性能的影响分析及其优化 MySQL数据库的性能的影响 一. 服务器的硬件的限制 二. 服务器所使用的操作系统 三. 服务器的所配置的参数设置不同 四. 数据库存储引擎的选择 五. 数 ...

  7. Android多线程任务的优化1:AsyncTask的缺陷 (转至 http://www.linuxidc.com/Linux/2011-09/43150.htm)

    导语:在开发Android应用的过程中,我们需要时刻注意保障应用的稳定性和界面响应性,因为不稳定或者响应速度慢的应用将会给用户带来非常差的交互体验.在越来越讲究用户体验的大环境下,用户也许会因为应用的 ...

  8. 重新学习MySQL数据库5:根据MySQL索引原理进行分析与优化

    重新学习MySQL数据库5:根据MySQL索引原理进行分析与优化 一:Mysql原理与慢查询 MySQL凭借着出色的性能.低廉的成本.丰富的资源,已经成为绝大多数互联网公司的首选关系型数据库.虽然性能 ...

  9. Android多线程分析之五:使用AsyncTask异步下载图像

    Android多线程分析之五:使用AsyncTask异步下载图像 罗朝辉 (http://www.cnblogs.com/kesalin) CC 许可,转载请注明出处 在本系列文章的第一篇<An ...

随机推荐

  1. codeforces 334B - Eight Point Sets

    题意难懂,其实就是x1<x2<x3与y1<y2<y3两两组合成九个点,去掉(x2,y2),剩余八个.这样的八个点才是满足要求的. 忘去重了 #include<cstdio ...

  2. 完整cocos2d-x编译Andriod应用过程

    作者:何卫 转载请注明,原文链接:http://www.cnblogs.com/hewei2012/p/3366969.html 其他平台移植:http://cocos2d.cocoachina.co ...

  3. Windows Azure® 由世纪互联运营发布MySQL Database on Azure正式商用版

    我们很高兴宣布MySQL Database on Azure于2015年9月1日在中国地区正式商用.回望过去,从2014年12月对少量用户开放的预览试用,到2015年4月30日对中国用户全面开放的公共 ...

  4. DataGuard相同SID物理Standby搭建

    Oracle Data Guard 是针对企业数据库的最有效和最全面的数据可用性.数据保护和灾难恢复解决方案.它提供管理.监视和自动化软件基础架构来创建和维护一个或多个同步备用数据库,从而保护数据不受 ...

  5. 【转】iOS类似Android上toast效果

    原文网址:http://m.blog.csdn.net/article/details?id=50478737 做过Android开发的人都知道toast,它会在界面上显示一排黑色背景的文字,用于提示 ...

  6. C#判断用户是否使用微信浏览器,并据此来显示真实内容或二维码

    平时我们看一些网页的时候会发现这样的功能:有的页面只能在微信里访问,如果在电脑上访问就只显示当前地址的二维码.这个用C#怎么实现呢?我们结合代码来看看. 首先,我们需要先判断用户使用的是什么浏览器,这 ...

  7. Easy Climb

    题意: 有n块石头,给定他们的高度,现保持第一和最后一块高度不变,其他可增加和减少高度,求通过变换使所有相邻石头的高度差的绝对值不大于d,所变化高度总和的最小值. 分析: 状态还可以想出来,dp[i] ...

  8. codedorces 260 div2 A题

    水题,扫描一遍看是否出现价格低质量高的情况. #include<cstdio> #include<string> #include<vector> #include ...

  9. Tableau学习笔记之二

    2张图片解析下Tableau 9.0界面的功能 1.数据加载界面: 2.数据分析界面:

  10. 【转载】epoll的使用

    select,poll,epoll简介 select select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理.这样所带来的缺点是: 1 单个进程可监视的fd数量被限制 2 需要维 ...