android 数据库锁的问题

最近在项目中遇到一个错误:

12-26 17:29:10.501 32592 32644 W SQLiteConnectionPool: The connection pool for database '***.db' has been unable to grant a connection to thread 1378 (thread-pool-0) with flags 0x2 for 60.038002 seconds.
12-26 17:29:10.501 32592 32644 W SQLiteConnectionPool: Connections: 0 active, 1 idle, 0 available.

 分析了一下,这个就是因为死锁造成的

报错的地方是:

SQLiteConnectionPool.java

    private SQLiteConnection waitForConnection(String sql, int connectionFlags,
            CancellationSignal cancellationSignal) {

        ......
                        logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags);
        ......

}

错误发生位置

    private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) {
        final Thread thread = Thread.currentThread();
        StringBuilder msg = new StringBuilder();
        msg.append("The connection pool for database '").append(mConfiguration.label);
        msg.append("' has been unable to grant a connection to thread ");
        msg.append(thread.getId()).append(" (").append(thread.getName()).append(") ");
        msg.append("with flags 0x").append(Integer.toHexString(connectionFlags));
        msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n");
         ......
}

发生上面错误的原因就是:

我们操作数据库时(先不考虑读取)一定要先获取连接,但是同一个connect在不特殊设置的时候,只能维持1个,当大于一个的时候,另一个就需要等待。

(可以通过设置ENABLE_WRITE_AHEAD_LOGGING的值,来提高这个数量)

所以:

当我们有一个连接正在插入数据到数据库,又来了一个插入数据库操作的时候,第二个就会等待,第一个完成,会通知第二个开始插入,这样也保证了数据库的线程安全。

这里虽然不是通过锁的机制实现的,但是和锁的目的是一样的。获取锁与释放锁的位置就是:

acquireConnection
releaseConnection

这两个方法调用的地方有两个:

1.修改数据库的地方,需要在修改之前,获取锁,修改之后,释放锁。

2.当我们获取事务的时候,获取锁,结束事务的时候,释放锁。

我们在操作数据库的时候,有时候,不熟悉相关机制,就有可能增加锁,这时就会导致又可能有两个锁互相锁住的情况,就会造成死锁发生,一旦发生死锁,每个30s就会打印一次上面的log

举一个发生死锁的问题代码:(这个是我当时犯的一个真实线上问题)

    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        if (mDb == null) {
            return null;
        }
        SQLiteDatabase db = mDb;
        Uri result;
        synchronized (sLock) {
            long ret = db.replace(FantasyDbConstant.TABLE_DATA, null, contentValues);
            result = Uri.parse("content://" + xxx + "/" + ret);
        }
        return result;
    }

其实我这个插入操作无论怎么调用都没有问题,因为我只是简单的在insert外面加锁,单个插入我也没有采用事务。

但万万没想到啊,在ContentProvider中有一部分代码是这样的,这个super.applyBatch实际上调用的是insert方法,这就导致了事务的锁中间又插入了一个对象锁,造成了死锁。

    public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations)
            throws OperationApplicationException {
        SQLiteDatabase db = fantasyDb.getSqlite();
        if (db == null) {
            return new ContentProviderResult[]{};
        }
        db.beginTransaction();
        try {
            ContentProviderResult[] results = super.applyBatch(operations);
            db.setTransactionSuccessful();
            return results;
        } finally {
            db.endTransaction();
        }
    }

所以理论上我们在使用数据库的时候,根本不需要自己加锁,因为数据库本身就是线程安全的,这样既不会增加效果,还会引发各种问题。

到此为止的所有逻辑都是java层的逻辑判断,还没有到数据库真正的核心逻辑。

 

到这里你以为就结束了?远远没有数据这里面还有很多的坑,可能有些复杂的业务场景还是需要加锁的。下面再介绍一个错误:

android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)

在介绍这个之前,我觉得有必要介绍一下真正的数据库的相关知识了:

数据库的锁机制:一共有五种状态,如下图:

 

了解了锁的机制之后,我们在回到上面的这个错误:

同一个进程多个线程开启多个connection,同时去写数据库:

2018-12-28 17:31:12.039 2562-2580/com.redare.dbmultprocess E/AndroidRuntime: FATAL EXCEPTION: Thread-3
    Process: com.redare.dbmultprocess, PID: 2562
    android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
        at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method)
        at android.database.sqlite.SQLiteConnection.executeForChangedRowCount(SQLiteConnection.java:734)
        at android.database.sqlite.SQLiteSession.executeForChangedRowCount(SQLiteSession.java:754)
        at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:64)
        at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:1679)
        at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1608)
        at com.redare.dbmultprocess.db.SQLite_db.exeDO(SQLite_db.java:34)
        at com.redare.dbmultprocess.db.SQLite_db.insertNewTask(SQLite_db.java:48)
        at com.redare.dbmultprocess.MyService$1.run(MyService.java:46)
        at java.lang.Thread.run(Thread.java:764)

我们通过查看sqlite.c文件可以知道:database is locked 对应的是SQLITE_BUSY的错误信息,就是说有一个database connection已经获取写入的状态,并且还没有commit,这是如果有另一个database connection尝试写入,就会报这个错误。

PS:SQLITE_LOCKED是有冲突,例如一个线程使用database connection尝试drop数据库内容,另一个线程使用当前的database connection去读,就会发生这个错误(我没有复现这个场景)

SQLITE_PRIVATE const char *sqlite3ErrStr(int rc){
  static const char* const aMsg[] = {
    *********************************************
    /* SQLITE_BUSY        */ "database is locked",
    /* SQLITE_LOCKED      */ "database table is locked",
    *********************************************
  };

到这里你是不是有些懵逼,sqlite不是说是线程安全的吗?为什么会直接崩溃呢?

这里需要强调几个概念:

1.connection :每个SQLiteOpenHelper,都会对应一个database,而在android 的java层的数据库的锁机制,都是在对象锁的维度上,所以说两个SQLiteOpenHelper之间是不会造成线程之间的排斥的,所以两个,线程可以同时写入

2.对于sqlite c部分的逻辑,我们看到上面图中的锁,他是不允许两个线程同时写入的,如果有一个线程已经获取了RESOLVED,PENDING,EXCLUSIVE的锁的时候,如果有另一个线程尝试去获取该种类型的锁的时候,就会返回SQLITE_BUSY的错误信息,反应到上层就是上面这个错误

现在说一下上面的结论:

1.数据库是线程安全的,这个无论是在android java端,还是在sqlite的c端,都是做了相关的保护的,但java层的保护是不发生异常,而sqlite c这层保护是保证不发生脏数据。

2.android 端如果采用多个SQLiteOpenHelper的对象,那么这个在java这一层的保护就跳过了,不再提供线程安全的保护,这样就有可能发生崩溃,

3.如果你在创建和Drop数据库的时候,查询数据库,那也会发生崩溃(但这个没有复现,验证的时候,虽然是同时,但如果先查询,验证没问题,如果先drop,则查询的时候,找不到table)

 

到这里就要总结一下需要注意的点了:

1.数据库的connection尽量维持一个,这样更安全一些。

2.没有特殊需求,不要自作多情,在增删改查的时候,增加锁。

 

介绍完以上这个问题之后,我们再来讨论一个问题就是

数据库是否是进程安全的?

先来看一下官网的解答:(我们只关注android,Win95 / 98 / ME下,缺少对读取器/写入器锁的支持,情况不一样)

1.sqlite 采用的是读取器/写入器

2.sqlite 允许多个进程一次打开数据库文件

3.sqlite允许多个进程一次读取数据库文件

4.sqlite只允许一个进程写入文件,并且在写入时,锁住整个数据库文件(几毫秒),如果其他进程如果访问会返回SQLITE_BUSY的错误

看到这里我们可以得出与线程安全同样的结论:

1.sqlite可以保证不产生脏数据

2.sqlite多进程写入时会发生崩溃(而且多进程没办法公用一个connection,所以这个问题android端并没有给解决方案)

多进程同时写文件时发生的崩溃:

2018-12-28 16:00:03.381 27025-27041/com.redare.dbmultprocess E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.redare.dbmultprocess, PID: 27025
    android.database.sqlite.SQLiteException: Failed to change locale for db '/data/user/0/com.redare.dbmultprocess/databases/my_db' to 'en_US'.
        at android.database.sqlite.SQLiteConnection.setLocaleFromConfiguration(SQLiteConnection.java:393)
        at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:218)
        at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:193)
        at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:463)
        at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:185)
        at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:177)
        at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:808)
        at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:793)
        at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:696)
        at android.app.ContextImpl.openOrCreateDatabase(ContextImpl.java:723)
        at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:299)
        at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:254)
        at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:194)
        at com.redare.dbmultprocess.db.SQLite_db.exeDO(SQLite_db.java:34)
        at com.redare.dbmultprocess.db.SQLite_db.insertNewTask(SQLite_db.java:49)
        at com.redare.dbmultprocess.MainActivity$1.run(MainActivity.java:35)
        at java.lang.Thread.run(Thread.java:764)
     Caused by: android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
        at android.database.sqlite.SQLiteConnection.nativeExecute(Native Method)
        at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:555)
        at android.database.sqlite.SQLiteConnection.setLocaleFromConfiguration(SQLiteConnection.java:371)
        at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:218) 
        at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:193) 
        at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:463) 
        at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:185) 
        at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:177) 
        at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:808) 
        at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:793) 
        at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:696) 
        at android.app.ContextImpl.openOrCreateDatabase(ContextImpl.java:723) 
        at android.content.ContextWrapper.openOrCreateDatabase(ContextWrapper.java:299) 
        at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:254) 
        at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:194) 
        at com.redare.dbmultprocess.db.SQLite_db.exeDO(SQLite_db.java:34) 
        at com.redare.dbmultprocess.db.SQLite_db.insertNewTask(SQLite_db.java:49) 
        at com.redare.dbmultprocess.MainActivity$1.run(MainActivity.java:35) 
        at java.lang.Thread.run(Thread.java:764) 
2018-12-28 16:02:55.331 27149-27167/com.redare.dbmultprocess:myservice E/AndroidRuntime: FATAL EXCEPTION: Thread-2
    Process: com.redare.dbmultprocess:myservice, PID: 27149
    android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
        at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount(Native Method)
        at android.database.sqlite.SQLiteConnection.executeForChangedRowCount(SQLiteConnection.java:734)
        at android.database.sqlite.SQLiteSession.executeForChangedRowCount(SQLiteSession.java:754)
        at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:64)
        at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:1679)
        at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1608)
        at com.redare.dbmultprocess.db.SQLite_db.exeDO(SQLite_db.java:35)
        at com.redare.dbmultprocess.db.SQLite_db.insertNewTask(SQLite_db.java:49)
        at com.redare.dbmultprocess.MyService$1.run(MyService.java:46)
        at java.lang.Thread.run(Thread.java:764)

最后总结一下数据库的知识:

1.sqlite尽量保持一个connection

2.多进程操作时,尽量保证只有一个进程写入,可以有多个进程读取

3.不要私自加锁

 

但是你以为这样就完美了?实际上还有坑:

当我们使用数据库连接的时候,我们涉及到关闭连接的问题,如果多个连接使用同一个connection,一旦有一个先close了,那么后面正在使用的就会崩溃(这里android在关闭的时候,会有一个计数器,但没搞明白这里实现的乱七八糟的,明显存在问题,一直没有修复)

同一个SqliteHelper对象进行读写操作:

2018-12-28 18:08:13.739 7603-7932/com.redare.dbmultprocess E/AndroidRuntime: FATAL EXCEPTION: Thread-313
    Process: com.redare.dbmultprocess, PID: 7603
    java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
        at android.database.sqlite.SQLiteConnectionPool.throwIfClosedLocked(SQLiteConnectionPool.java:962)
        at android.database.sqlite.SQLiteConnectionPool.waitForConnection(SQLiteConnectionPool.java:677)
        at android.database.sqlite.SQLiteConnectionPool.acquireConnection(SQLiteConnectionPool.java:348)
        at android.database.sqlite.SQLiteSession.acquireConnection(SQLiteSession.java:894)
        at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:834)
        at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
        at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:143)
        at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:132)
        at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:219)
        at android.database.AbstractCursor.moveToFirst(AbstractCursor.java:258)
        at com.redare.dbmultprocess.db.SQLite_db.query(SQLite_db.java:64)
        at com.redare.dbmultprocess.MainActivity$2.run(MainActivity.java:48)
        at java.lang.Thread.run(Thread.java:764)

我在github上复现了所有的崩溃,感兴趣的可以自己查看。

https://github.com/xiepengchong/DbException

参考文章:

https://www.sqlite.org/lockingv3.html

https://www.sqlite.org/faq.html#q5

https://www.sqlite.org/rescode.html

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值