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 网上别人的总结
搜索了上面错误日志,找到别人的解决方法:

1.png (62.72 KB)
2014-6-23 19:43


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

2.png (91.23 KB)
2014-6-23 19:43




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


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


3a. getSession分析
01frameworks\base\core\java\android\database\sqlite\SQLiteProgram.java
02protected final SQLiteSession getSession(){
03 
04return mDatabase.getThreadSession();
05}
063b. 调用的是SqliteDatabase.getThreadSession()方法,继续分析
07frameworks\base\core\java\android\database\sqlite\SQLiteDatabase.java
08SQLiteSession getThreadSession() {
09 
10return mThreadSession.get(); // initialValue() throws if database closed
11 
12}
调用的是mThreadSession.get(),看看mThreadSession是什么东西
01private final ThreadLocal<SQLiteSession>mThreadSession = new ThreadLocal<SQLiteSession>() {
02 
03protected SQLiteSession initialValue() {
04 
05return createSession();
06 
07}
08};
09 
10 
11SQLiteSession createSession() {
12 
13final SQLiteConnectionPool pool;
14 
15synchronized (mLock) {
16 
17pool = mConnectionPoolLocked;
18 
19}
20 
21return new SQLiteSession(pool);
22 
23}
24 
25 
26public SQLiteSession(SQLiteConnectionPoolconnectionPool) {
27 
28if (connectionPool == null) {
29 
30throw new IllegalArgumentException("connectionPool must not benull");
31 
32}
33 
34mConnectionPool = 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
01public long executeForLastInsertedRowId(Stringsql, Object[] bindArgs,int connectionFlags,
02 
03CancellationSignal cancellationSignal) {
04 
05...
06 
07acquireConnection(sql, connectionFlags, cancellationSignal);// mightthrow
08 
09try {
10 
11return mConnection.executeForLastInsertedRowId(sql, bindArgs,
12 
13cancellationSignal); //might throw
14 
15} finally{
16 
17releaseConnection(); // might throw
18 
19}
20 
21}
这个方法主要分析两个操作
acquireConnection
releaseConnection
因为中间的mConnection.executeForLastInsertedRowId语句主要是通过Jni往底层调用,插入数据库数据,不是今天我们讨论的话题。

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


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


c.
如果获取到连接,那么返回这个连接
1if (connection != null) {
2 
3return 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
01finally {
02releaseConnection();// might throw
03}
04 
05 
06private void releaseConnection() {
07 
08 
09assert mConnection != null;
10 
11assert mConnectionUseCount > 0;
12 
13if (--mConnectionUseCount == 0) {
14 
15 
16try {
17 
18mConnectionPool.releaseConnection(mConnection);// might throw
19 
20} finally{
21 
22mConnection = 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
01public voidreleaseConnection(SQLiteConnection connection) {
02 
03 
04synchronized (mLock) {
05 
06....
07 
08if (!mIsOpen) {
09 
10closeConnectionAndLogExceptionsLocked(connection);
11 
12} elseif (connection.isPrimaryConnection()) {
13 
14if(recycleConnectionLocked(connection, status)) {
15 
16assert mAvailablePrimaryConnection== null;
17 
18mAvailablePrimaryConnection= connection;
19 
20}
21 
22wakeConnectionWaitersLocked();
23 
24} elseif (mAvailableNonPrimaryConnections.size() >=mMaxConnectionPoolSize -1) {
25 
26closeConnectionAndLogExceptionsLocked(connection);
27 
28} else{
29 
30if(recycleConnectionLocked(connection, status)) {
31 
32mAvailableNonPrimaryConnections.add(connection);
33 
34}
35 
36 
37wakeConnectionWaitersLocked();
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.csdn.net/efeics/article/details/18970483
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值