Android Sqlite并发问题

背景

  • 我们的项目中使用的是ormlite的加密框架sqlcipher来进行数据库操作的

多进程操作同一个数据库文件出现了问题

net.sqlcipher.database.SQLiteException: error code 5: database is locked
                                                           at net.sqlcipher.database.SQLiteStatement.native_execute(Native Method)
                                                           at net.sqlcipher.database.SQLiteStatement.executeInsert(SQLiteStatement.java:84)
                                                           at com.j256.ormlite.sqlcipher.android.AndroidDatabaseConnection.insert(AndroidDatabaseConnection.java:158)
                                                           at com.j256.ormlite.stmt.mapped.MappedCreate.insert(MappedCreate.java:91)
                                                           at com.j256.ormlite.stmt.StatementExecutor.create(StatementExecutor.java:450)
                                                           at com.j256.ormlite.dao.BaseDaoImpl.create(BaseDaoImpl.java:310)
                                                           at com.xtc.database.encrypt.OrmLiteDao.insert(OrmLiteDao.java:99)
                                                           at com.xtc.bigdata.report.db.ReportDao.insert(ReportDao.java:46)
                                                           at com.xtc.bigdata.report.db.UserBehaviorProvider.insert(UserBehaviorProvider.java:113)
                                                           at android.content.ContentProvider$Transport.insert(ContentProvider.java:264)
                                                           at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:163)
                                                           at android.os.Binder.execTransact(Binder.java:565)

如上异常堆栈中的错误信息error code 5: database is locked,经过查找发现code为5代表sqlite中的SQLITE_BUSY异常,详见:https://www.sqlite.org/rescode.html#busy,这里面说,SQLITE_BUSY(5)异常是一个数据库文件在被其他不同的数据库连接进行并发操作的时候写操作将补发继续,通常是多个进程的不同数据库连接对同一个数据库进行并发操作,例如进程A在进行耗时的数据库事务,而于此同时进程B也要进行一个数据库事务,这时候进程B就会直接返回SQLITE_BUSY的错误码,因为sqlite只能支持同一个时刻只能有一个写操作,所以解决这个问题的方法就是避免不同进程分别对同一个数据库各自开启一个database connection,并且对相同的数据库进行并发操作,如果有这种需求,那么应该全部都交给一个进程来对数据库进行操作,其他的进程想操作这个数据库就通过contentprovider的方式来实现数据共享,使用contentprovider的方式是最安全的,如果是通过shareUserId的方式来实现数据库共享也是不安全的,因为:

Context thdContext = null;  
try {  
    thdContext = createPackageContext(  
            "com.example.testdatabase",  
            Context.CONTEXT_IGNORE_SECURITY);  
    String dbPath = thdContext.getDatabasePath("BookStore.db")  
            .getAbsolutePath();  
    SQLiteDatabase db = SQLiteDatabase.openDatabase(dbPath,  
            null, SQLiteDatabase.OPEN_READWRITE);  
    Cursor cursor = db.query("Book", null, null, null, null,  
            null, null);  
    if (cursor.moveToFirst()) {  
        do {  
            String name = cursor.getString(cursor  
                    .getColumnIndex("name"));  
            Log.d(TAG, "name: " + name);  
        } while (cursor.moveToNext());  
    }  
} catch (NameNotFoundException e) {  
    e.printStackTrace();  
}  

SQLiteDatabase.openDatabase会创建一个数据库实例SQLiteDatabase,如果在不同的进程如果通过shareuserid来实现数据库共享,那么会造成每一个进程都有SQLiteDatabase对象,在并发操作的时候也有可能会出现如上问题,所以还是推荐使用contentprovider的方式来实现数据库共享,必须注意contentprovider必须只有宿主app进程来维护,其他的进程就通过调用宿主app进程的contentprovider暴露出去的接口来实现对宿主app进程的数据库的操作,实际上这时候的数据库操作就都是由宿主app进程来操作的了,就不会出现如上的异常

拓展

  • 上面提及的数据库操作异常的code是5,对应的是SQLITE_BUSY,这里还有一个相似的数据库操作异常,code为6,对应的是SQLITE_LOCKED,详见:https://www.sqlite.org/rescode.html#busy,具体意思就是说,SQLITE_LOCKED错误码是在同一个数据库连接存在冲突,或者不同的数据库连接共享相同的数据库缓存存在冲突的时候,写操作将无法继续,这里的冲突是什么意思呢?比如,有一个删除表的操作发生在其他的线程在对这个表进行读操作的过程中,那么就会报SQLITE_LOCKED异常,也就是说一个线程的删除表操作和另一个线程对相同表的读取操作存在冲突,前提是这两个操作都是使用同一个数据库连接

  • java.lang.IllegalStateException: get field slot from row 0 col 0 failed异常,这个异常是数据库在执行查询操作的时候,如果数据库中的一条记录所占用的内存大于1MB的话,这时候查询操作就会报错,解决方法就是让每一条的数据库记录的大小都不要超过1MB,这里是单条记录的大小不能超过1MB,如果是每条数据库记录大小都不超过1MB,但是10条加起来超过1MB,那这是没有问题的,此问题在旧版的sqlcipher会出现,但是在新版的sqlcipher貌似已经修复了这个bug,但是只是提高了1MB的阀值,至于怎么提高的?看下面这个issue:

We are glad to hear 3.5.7 is working well for you. We've adjusted the library to allow it to dynamically resize the backing memory for the cursor, so you would be limited by the device.

详见:https://github.com/sqlcipher/android-database-sqlcipher/issues/341#issuecomment-310289295,现在是改成动态来分配大小的,所以限制的上限就会由机器来决定,也就是说,仍然存在这个问题,如果存入数据库的记录太大,还是有可能发生此异常,我们不建议让sqlite数据库中去存储blog这种大的数据记录,应该大的数据记录存成文件,然后把文件路径存到数据库中会更加合适

    原文作者:大大大大大先生
    原文地址: https://www.jianshu.com/p/bc50783b902a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞