Android sqlite数据库连接池连接异常分析

转:http://bbs.51cto.com/thread-1113117-1.html

 

Android sqlite数据库连接池连接异常分析

1 android开发过程中,突然碰到了这个错误,数据库连接分配不到,日志如下:
W/SQLiteConnectionPool( 3681): Theconnection pool for database '/data/user/0/com.android.providers.contacts/databases/contacts2.db'has been unable to grant a connection to thread 371 (ContactsProviderWorker)with flags 0x1 for 30.000002 seconds.
W/SQLiteConnectionPool( 3681): Connections:0 active, 1 idle, 0 available.


2 网上别人的总结
搜索了上面错误日志,找到别人的解决方法:

Android sqlite数据库连接池连接异常分析Android sqlite数据库连接池连接异常分析

Android sqlite数据库连接池连接异常分析1.png (62.72 KB)
2014-6-23 19:43


他说是在一个Transaction里面又执行一个execSql(sql)导致。但是经过自己的分析发现,完全不是这么回事。可见,网络上很多东西都是不可全信的。
为什么这么说呢,我们首先来分析下整个数据库连接的获取过程,一般进行下列操作时都会申请获取一个数据库连接。
a.
Query, insert, delete操作
b.
beginTranscation操作
从网上截了个图,让大家看的更清楚点

Android sqlite数据库连接池连接异常分析Android sqlite数据库连接池连接异常分析
Android sqlite数据库连接池连接异常分析2.png (91.23 KB)
2014-6-23 19:43




Ok... 那么我们就以insert操作为例,说明数据库连接获取的流程。


1.
01 SqliteDatabase的insert方法
02 public long insert(String table, StringnullColumnHack, ContentValues values) {
03  
04 try {
05  
06 return insertWithOnConflict(table, nullColumnHack, values,CONFLICT_NONE);
07  
08 } catch(SQLException e) {
09  
10 Log.e(TAG, "Error inserting "+ values, e);
11  
12 return -1;
13  
14 }
15 }
2.
Ok... 继续往下面分析insertWithOnConflict方法
01 public long insertWithOnConflict(Stringtable, String nullColumnHack,
02  
03 ContentValues initialValues, int conflictAlgorithm) {
04  
05 acquireReference();
06  
07 try {
08  
09 StringBuilder sql = newStringBuilder();
10  
11 sql.append("INSERT");
12  
13 sql.append(CONFLICT_VALUES[conflictAlgorithm]);
14  
15 sql.append(" INTO ");
16  
17 ...
18  
19 sql.append(')');
20  
21  
22  
23 SQLiteStatement statement = newSQLiteStatement(this, sql.toString(),bindArgs);
24  
25 try {
26  
27 returnstatement.executeInsert();
28  
29 } finally{
30  
31 statement.close();
32  
33 }
34  
35 } finally{
36  
37 releaseReference();
38  
39 }
40  
41 }
这个方法首先构造insert的sql语句,然后调用statement的executeInsert()方法
那继续跟下去
3.
frameworks\base\core\java\android\database\sqlite\SQLiteStatement.java
01 public long executeInsert() {
02  
03 ...
04  
05 return getSession().executeForLastInsertedRowId(
06  
07 getSql(), getBindArgs(), getConnectionFlags(),null);
08  
09 ...
10  
11 }
这个方法比较重要,分成两步来分析:
首先分析getSession()方法,
然后再去看看executeForLastInsertedRowId方法。


3a. getSession分析
01 frameworks\base\core\java\android\database\sqlite\SQLiteProgram.java
02 protected final SQLiteSession getSession(){
03  
04 return mDatabase.getThreadSession();
05 }
06 3b. 调用的是SqliteDatabase.getThreadSession()方法,继续分析
07 frameworks\base\core\java\android\database\sqlite\SQLiteDatabase.java
08 SQLiteSession getThreadSession() {
09  
10 return mThreadSession.get(); // initialValue() throws if database closed
11  
12 }
调用的是mThreadSession.get(),看看mThreadSession是什么东西
01 private final ThreadLocal<SQLiteSession>mThreadSession = new ThreadLocal<SQLiteSession>() {
02  
03 protected SQLiteSession initialValue() {
04  
05 return createSession();
06  
07 }
08 };
09  
10  
11 SQLiteSession createSession() {
12  
13 final SQLiteConnectionPool pool;
14  
15 synchronized (mLock) {
16  
17 pool = mConnectionPoolLocked;
18  
19 }
20  
21 return new SQLiteSession(pool);
22  
23 }
24  
25  
26 public SQLiteSession(SQLiteConnectionPoolconnectionPool) {
27  
28 if (connectionPool == null) {
29  
30 throw new IllegalArgumentException("connectionPool must not benull");
31  
32 }
33  
34 mConnectionPool = connectionPool;
35  
36 }
嗯,非常明白,这是一个ThreadLocal类型的变量,ThreadLocal,Java提供的一个用于多线程之间隔离数据的东西,避免多线程访问同一个变量导致冲突。
也就是ThreadLocal是为每个线程保存一份变量,这样就不会引起冲突了。这个ThreadLocal如果有不理解的,可以百度下ThreadLocal的使用。


Ok..理解完ThreadLocal之后,继续看createSession方法
它是使用mConnectionPoolLocked这么个SQLiteConnectionPool变量来实例化一个SQLiteSession对象。而这个mConnectionPoolLocked对象是SqliteDatabase打开的时候实例化的,也就是说一个database只有一个这样连接。
那么,这里我们可以理清楚一个关系
a.
一个线程会对应一个SqliteSession对象,这个是用ThreadLocal对象来维持的。
b.
一个数据库对应唯一一个sqliteDabase对象,以及唯一一个SQLiteConnectionPool对象,然后各个线程之间共享这个SQLiteConnectionPool对象。
Ok..理清楚这几个关系之后,返回去分析executeForLastInsertedRowId方法


4.
frameworks\base\core\java\android\database\sqlite\SQLiteSession.java
01 public long executeForLastInsertedRowId(Stringsql, Object[] bindArgs,int connectionFlags,
02  
03 CancellationSignal cancellationSignal) {
04  
05 ...
06  
07 acquireConnection(sql, connectionFlags, cancellationSignal);// mightthrow
08  
09 try {
10  
11 return mConnection.executeForLastInsertedRowId(sql, bindArgs,
12  
13 cancellationSignal); //might throw
14  
15 } finally{
16  
17 releaseConnection(); // might throw
18  
19 }
20  
21 }
这个方法主要分析两个操作
acquireConnection
releaseConnection
因为中间的mConnection.executeForLastInsertedRowId语句主要是通过Jni往底层调用,插入数据库数据,不是今天我们讨论的话题。

Ok.. 那这两个操作,一个个来分析
4a. acquireConnection
frameworks\base\core\java\android\database\sqlite\SQLiteSession.java
01 private void acquireConnection(String sql,intconnectionFlags,
02  
03 CancellationSignal cancellationSignal) {
04  
05  
06 if (mConnection == null) {
07  
08 assert mConnectionUseCount == 0;
09  
10 mConnection = mConnectionPool.acquireConnection(sql, connectionFlags,
11  
12 cancellationSignal); // mightthrow
13  
14 mConnectionFlags = connectionFlags;
15  
16 }
17  
18 mConnectionUseCount += 1;
19  
20  
21 }
Ok..首先会去判断当前线程的Session里面的mConnection是否为null,如果不为null,就只是简单的把连接个数mConnectionUseCount加1
如果为null,也就是当前线程的Session没有连接数据库,那么就要去申请一个连接。



所以这里的逻辑特别重要,就是一个对于一个线程而已,它只会去获取一次数据库连接。即使你调用再多的beginTranscation以及query,第一次调用的时候会去获取连接,以后就是让mConnectionUseCount 加1;
当然,你使用beginTranscation需要手动调用endTranscation,不然不会去释放连接。
调用其他的,比如query,insert之类的,不需要手动释放。系统会帮你去调用释放连接。
Ok.. 接下来分析mConnectionPool.acquireConnection的流程.


4b. 调用mConnectionPool.acquireConnection,顾名思义,它是向连接池申请一个连接;根据之前的分析,这个连接池是唯一的,是多个线程之间共享的。

frameworks\base\core\java\android\database\sqlite\SQLiteConnectionPool.java
001 public SQLiteConnectionacquireConnection(String sql, intconnectionFlags,
002  
003 CancellationSignal cancellationSignal) {
004  
005 return waitForConnection(sql, connectionFlags, cancellationSignal);
006  
007 }
008  
009  
010  
011  
012 private SQLiteConnectionwaitForConnection(String sql, intconnectionFlags,
013  
014 CancellationSignal cancellationSignal) {
015  
016 final boolean wantPrimaryConnection =
017  
018 (connectionFlags &CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) !=0;
019  
020  
021  
022 final ConnectionWaiter waiter;
023  
024 final int nonce;
025  
026 synchronized (mLock) {
027  
028 throwIfClosedLocked();
029  
030  
031  
032 // Abort if canceled.
033  
034 if (cancellationSignal != null) {
035  
036 cancellationSignal.throwIfCanceled();
037  
038 }
039  
040  
041  
042 // Try to acquire a connection.
043  
044 SQLiteConnection connection = null;
045  
046 if (!wantPrimaryConnection) {
047 //尝试获取非主连接
048  
049 connection =tryAcquireNonPrimaryConnectionLocked(
050  
051 sql, connectionFlags);// might throw
052  
053 }
054  
055 if (connection == null) {
056 //尝试去获取主连接
057  
058 connection =tryAcquirePrimaryConnectionLocked(connectionFlags);// might throw
059  
060 }
061  
062 if (connection != null) {
063 //获取到连接后,返回连接
064  
065 return connection;
066  
067 }
068  
069 //没有获取到连接,新建一个waiter对象,加入等待队列
070  
071  
072 // No connections available.
073 Enqueue a waiter in priority order.
074  
075 final int priority = getPriority(connectionFlags);
076  
077 final long startTime = SystemClock.uptimeMillis();
078  
079 waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime,
080  
081 priority,wantPrimaryConnection, sql, connectionFlags);
082  
083 ConnectionWaiter predecessor = null;
084  
085 ConnectionWaiter successor = mConnectionWaiterQueue;
086  
087 while (successor != null) {
088  
089  
090 if (priority >successor.mPriority) {
091  
092 waiter.mNext = successor;
093  
094 break;
095  
096 }
097  
098 predecessor = successor;
099  
100 successor = successor.mNext;
101  
102 }
103  
104 if (predecessor != null) {
105  
106 predecessor.mNext = waiter;
107  
108 } else{
109  
110 mConnectionWaiterQueue =waiter;
111  
112 }
113  
114  
115  
116 nonce = waiter.mNonce;
117  
118 }
119  
120  
121  
122 ....
123  
124  
125  
126 //把当前线程睡眠30s,然后尝试去获取连接,如果没有获取到连接,则打印当前线程的等待时间。
127  
128 try {
129  
130 // Park the thread until a connection is assigned or the pool is closed.
131  
132 // Rethrow an exception from the wait, if we got one.
133  
134 long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS;
135 //30s
136  
137 long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis;
138  
139 for (;;) {
140  
141 // Detect and recover fromconnection leaks.
142  
143 if(mConnectionLeaked.compareAndSet(true,false)) {
144  
145  
146 synchronized (mLock) {
147  
148 wakeConnectionWaitersLocked();
149 }
150  
151 }
152  
153  
154  
155 // Wait to be unparked (mayalready have happened), a timeout, or interruption.
156  
157 LockSupport.parkNanos(this,busyTimeoutMillis * 1000000L);
158 //当前线程停止30s
159  
160  
161  
162 // Clear the interrupted flag,just in case.
163  
164 Thread.interrupted();
165  
166  
167  
168 // Check whether we are donewaiting yet.
169  
170 synchronized (mLock) {
171  
172 throwIfClosedLocked();
173  
174 final SQLiteConnectionconnection = waiter.mAssignedConnection;
175  
176 final RuntimeException ex =waiter.mException;
177  
178 if (connection != null ||ex != null) {
179  
180 recycleConnectionWaiterLocked(waiter);
181  
182 if (connection != null){
183  
184 return connection;
185  
186 }
187  
188 throw ex; // rethrow!
189  
190 }
191  
192  
193  
194  
195 final long now =SystemClock.uptimeMillis();
196  
197 if (now <nextBusyTimeoutTime) {
198  
199 busyTimeoutMillis = now- nextBusyTimeoutTime;
200  
201 } else{
202  
203 logConnectionPoolBusyLocked(now- waiter.mStartTime, connectionFlags);
204 //打印出当前线程等待的时间
205  
206 busyTimeoutMillis =CONNECTION_POOL_BUSY_MILLIS;
207  
208 nextBusyTimeoutTime =now + busyTimeoutMillis;
209  
210 }
211  
212 }
213  
214 }
215  
216 } finally{
217  
218 ...
219  
220 }
221  
222 }
这个方法比较多,我在里面做了一点注释,大概包括下面几个步骤。同时要明确一个概念,主连接和非主连接。
其实他们没有本质的区别,主连接是一定有的,在初始化的时候就实例化完毕;而非主连接是一个连接集合,也就是说非主连接可以有很多个。不过一般有个最大大小,我们可以配置的。
Ok...明白这个概念之后,讲讲步骤
a.
如果没有指定要获取主连接的话,首先尝试获取非主连接
01 private SQLiteConnectiontryAcquireNonPrimaryConnectionLocked(
02  
03 String sql, intconnectionFlags) {
04  
05 // Try to acquire the next connection in the queue.
06  
07 SQLiteConnection connection;
08  
09 final int availableCount = mAvailableNonPrimaryConnections.size();
10  
11 if (availableCount > 1 && sql != null) {
12  
13 // If we have a choice, then prefer a connection that has the
14  
15 // prepared statement in its cache.
16  
17  
18 for (int i = 0; i <availableCount; i++) {
19  
20 connection =mAvailableNonPrimaryConnections.get(i);
21  
22 if(connection.isPreparedStatementInCache(sql)) {
23  
24 mAvailableNonPrimaryConnections.remove(i);
25  
26  
27 finishAcquireConnectionLocked(connection,connectionFlags);// might throw
28  
29 return connection;
30  
31 }
32  
33 }
34  
35 }
36  
37 if (availableCount > 0) {
38  
39 // Otherwise, just grab the next one.
40  
41  
42 connection =mAvailableNonPrimaryConnections.remove(availableCount -1);
43  
44 finishAcquireConnectionLocked(connection, connectionFlags);// mightthrow
45  
46 return connection;
47  
48 }
49  
50  
51  
52 // Expand the pool if needed.
53  
54 int openConnections = mAcquiredConnections.size();
55  
56 if (mAvailablePrimaryConnection != null) {
57  
58 openConnections += 1;
59  
60  
61 }
62  
63  
64  
65 if (openConnections >= mMaxConnectionPoolSize) {
66  
67 return null;
68  
69 }
70  
71 connection = openConnectionLocked(mConfiguration,
72  
73 false /*primaryConnection*/);// might throw
74  
75 finishAcquireConnectionLocked(connection, connectionFlags);// mightthrow
76  
77 return connection;
78  
79 }
获取非主连接的时候,首先会判断已有的连接中有没有相同的sql,如果有的话,就直接返回这个连接。
然后如果第一步没有成功的话,比如传入的sql是null,那么就会尝试获取队列里面的最后一个连接,然后返回。
如果第二步也没有成功,那么它会尝试去扩充非主连接集合
但是,它会去判断是否超过了最大的连接数,是已经到达最大的连接数,那么就返回null
1 if (openConnections >=mMaxConnectionPoolSize) {
2  
3 return null;
4  
5 }
如果还没有到达最大连接数,那么就把连接放入非主连接集合,然后返回这个扩容的连接。


b.
如果没有获取到非主连接,或者指定要获取主连接,那么就要去尝试获取主连接。
01 private SQLiteConnectiontryAcquirePrimaryConnectionLocked(intconnectionFlags) {
02  
03 // If the primary connection is available, acquire it now.
04  
05 SQLiteConnection connection = mAvailablePrimaryConnection;
06  
07 if (connection != null) {
08  
09 mAvailablePrimaryConnection = null;
10  
11 finishAcquireConnectionLocked(connection, connectionFlags);// mightthrow
12  
13 return connection;
14  
15 }
16  
17  
18  
19 // Make sure that the primary connection actually exists and has justbeen acquired.
20  
21 for (SQLiteConnection acquiredConnection :mAcquiredConnections.keySet()) {
22  
23 if (acquiredConnection.isPrimaryConnection()) {
24  
25 return null;
26  
27 }
28  
29  
30 }
31  
32  
33  
34 // Uhoh.
35 No primaryconnection!
36 Either thisis the firsttime we asked
37  
38 // for it, or maybe it leaked?
39  
40 connection = openConnectionLocked(mConfiguration,
41  
42 true /*primaryConnection*/); //might throw
43  
44 finishAcquireConnectionLocked(connection, connectionFlags);// mightthrow
45  
46 return connection;
47  
48 }
由于主连接只有一个,所以它用一个变量来表示 --- mAvailablePrimaryConnection
如果主连接为null就返回主连接,并把主连接设置成null,也就是主连接被占用。
1 if (connection != null) {
2  
3 mAvailablePrimaryConnection = null;
4  
5 finishAcquireConnectionLocked(connection, connectionFlags);// mightthrow
6  
7 return connection;
8  
9 }
如果主连接为null,那么它会去查看是否已经有人获取了主连接,如果是,那么返回null;这样做就是确保主连接是存在的。
如果经过上面一步确认,还没有返回,那么说明主连接没有创建;这个一般是不可能的,因为主连接时数据库初始化的时候创建的。这个一般是第一次访问,或者出现了程序异常。那么就新建一个主连接,然后返回。


c.
如果获取到连接,那么返回这个连接
1 if (connection != null) {
2  
3 return connection;
4 }
d.
如果没有获取到连接,那么新建一个waiter对象,并进入等待队列。
e.
线程进入死循环,不断休眠(30s),然后重新获取连接。直到获取到连接后返回。否则,记录下当前线程等待的时间。不过这里要注意,这里打印的等待时间并不包括系统睡眠的时间。比如,下午1点进入等待,2点-4点手机睡眠,那么到5点的时候打印,只能算两个小时。
也就是上面我们看到的异常日志:
04-2314:25:48.522 W/SQLiteConnectionPool( 3681): The connection pool for database'/data/user/0/com.android.providers.contacts/databases/contacts2.db' has beenunable to grant a connection to thread 371 (ContactsProviderWorker) with flags0x1 for 30.000002 seconds.
04-23 14:25:48.522 W/SQLiteConnectionPool( 3681):Connections: 0 active, 1 idle, 0 available.


Ok...至此,整个申请数据库连接过程分析基本完毕,下面分析释放的过程



5.释放连接
frameworks\base\core\java\android\database\sqlite\SQLiteSession.java
01 finally {
02 releaseConnection();// might throw
03 }
04  
05  
06 private void releaseConnection() {
07  
08  
09 assert mConnection != null;
10  
11 assert mConnectionUseCount > 0;
12  
13 if (--mConnectionUseCount == 0) {
14  
15  
16 try {
17  
18 mConnectionPool.releaseConnection(mConnection);// might throw
19  
20 } finally{
21  
22 mConnection = null;
23  
24 }
25  
26 }
27  
28 }
首先会去判断当前使用数是否为大于0,还记得前面我们说过如果线程第一次申请连接,那么就去申请,然后使用数+1;但是如果线程已经拥有了连接,那么我们只是简单的把连接数+1。
所以,这里要判断这个连接数是否>0
然后把连接数减去1,看是否等于0;这是什么意思呢?就是说当前线程已经没有使用数据库的操作了。

如果减去1之后>0,那么说明当前线程还要使用这个连接操作数据库,还不能释放。
这里也可以看出,申请连接和释放连接一定要是一一对应的。申请了,一定要释放。当然,对于应用程序来说,只有beginTranscation需要手动去释放,也就是调用endTranscation,而且必须要调用(一般写在finally里面)。
Ok...一切都ok的话,正式去释放连接
mConnectionPool.releaseConnection(mConnection);// might throw


5a. 正式释放连接

frameworks\base\core\java\android\database\sqlite\SQLiteConnectionPool.java
01 public voidreleaseConnection(SQLiteConnection connection) {
02  
03  
04 synchronized (mLock) {
05  
06 ....
07  
08 if (!mIsOpen) {
09  
10 closeConnectionAndLogExceptionsLocked(connection);
11  
12 } elseif (connection.isPrimaryConnection()) {
13  
14 if(recycleConnectionLocked(connection, status)) {
15  
16 assert mAvailablePrimaryConnection== null;
17  
18 mAvailablePrimaryConnection= connection;
19  
20 }
21  
22 wakeConnectionWaitersLocked();
23  
24 } elseif (mAvailableNonPrimaryConnections.size() >=mMaxConnectionPoolSize -1) {
25  
26 closeConnectionAndLogExceptionsLocked(connection);
27  
28 } else{
29  
30 if(recycleConnectionLocked(connection, status)) {
31  
32 mAvailableNonPrimaryConnections.add(connection);
33  
34 }
35  
36  
37 wakeConnectionWaitersLocked();
38  
39 }
40  
41 }
42  
43 }
这里会去根据当前连接是否是主连接而选择释放方法,然后通知那些正在等待的线程(waiter)去获取连接。


6. 总结
1. 如文章开头所说,这个bug不是由于在beginTranscation里面执行一个execuSql(sql)所导致的,因为在同一个线程里面,即使申请两次数据库连接,那也只是让使用数+1而已。它只会去申请一个连接。除非beginTranscation和execuSql(sql) 不在一个线程,另外开线程去execuSql(sql).
2. 那么怎么会导致这个bug呢?

a. 时间过长的操作,某个操作持续很长时间,那么其他线程等待的时候就会打印这个信息。不过,一般不会等待太久。因为一般不会有这么长时间的数据库操作。
b. 忘记调用endTranscation,如前面所说,只有beginTranscation需要程序员手动去调用endTransction来释放连接,其他操作不需要。
那么,有个时候会疏忽忘记调用endTransction了。
或者endTranscation没有放在finally里面,导致出现异常而没有调用endTransction
c.
死锁,这个比较复杂,需要具体问题具体分析。
如果出现死锁,线程互相等待,这样也没有去释放连接;那么后面的线程自然也拿不到数据库连接了。




参考资料
http://blog.****.net/efeics/article/details/18970483