最近在项目中遇到一个错误:
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