• 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里对多线程数据库读写的描述。

    SQLiteSession.java


    结论

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

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


    另:

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


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

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


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



  • 相关阅读:
    《大话设计模式》ruby版代码:工厂方法模式
    《大话设计模式》ruby版代码:代理模式
    redis ruby客户端学习(一)
    《大话设计模式》ruby版代码:装饰模式
    打卡2
    打卡1
    分块题集
    2018 Multi-University Training Contest 7
    2018 Multi-University Training Contest 6
    2018 Multi-University Training Contest 4
  • 原文地址:https://www.cnblogs.com/javawebsoa/p/3237018.html
Copyright © 2020-2023  润新知