高并發(fā)服務(wù)優(yōu)化篇:淺談數(shù)據(jù)庫連接池

被N多大號(hào)轉(zhuǎn)載的一篇CSDN博客,引起了我的注意,說的是數(shù)據(jù)庫連接池使用threadlocal的原因,文中結(jié)論如下圖所示。

姑且不談threadlocal的作用和工作原理,單說數(shù)據(jù)庫連接池這個(gè)知識(shí)點(diǎn),猛地一看挺有理;仔細(xì)一看,怎么感覺不太對(duì)啊,同學(xué),這是什么虎狼之詞。
$ 實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)
個(gè)人理解,連接池提供的獲取連接的能力,需要對(duì)"任務(wù)"唯一,即,只有當(dāng)某一線程完成了本次數(shù)據(jù)操作,將連接放回到連接池之后,其他線程才能夠再次獲取并使用。原因我們后面細(xì)說,先來親自測(cè)試一下。
連接池選一個(gè)druid,設(shè)置連接池中只有一個(gè)connection,方便驗(yàn)證多線程應(yīng)對(duì)同一個(gè)connection的場景。
首先,將datasource共享資源傳入線程,采用datasource.getConnection()方式獲取連接 :

結(jié)果如上圖:只有一個(gè)線程可以正常執(zhí)行,由于沒有被關(guān)閉,其他線程都獲取連接失敗了。說明,數(shù)據(jù)庫連接池的作用方式是某個(gè)線程任務(wù)"獨(dú)占"的。
$ 退一步來講
假設(shè)如同開頭文章中描述的,用了一個(gè)功能不完備的連接池,讓多個(gè)線程拿到了同一個(gè)connection,那么,用threadlocal真的可以起到互不影響的作用么?
//驗(yàn)證思路參考自:https://blog.csdn.net/sunbo94/article/details/79409298
//Connection設(shè)置 autoCommit=false
private static final ThreadLocal<Connection> connectionThreadLocal=new ThreadLocal<>();
private static class InnerRunner implements Runnable{
@Override
public void run() {
//其他代碼省略...
String insertSql="insert into user(id,name) value("+RunnerIndex+","+RunnerIndex+")";
statement=connectionThreadLocal.get().createStatement();
statement.executeUpdate(insertSql);
System.out.println(RunnerIndex+" is running");
//讓特定的線程執(zhí)行回滾,用來驗(yàn)證事務(wù)之間的影響
if (RunnerIndex==3){
//模擬異常時(shí)耗時(shí)增加
Thread.sleep(100);
//從threadlocal里拿連接對(duì)象
connectionThreadLocal.get().rollback();
System.out.println("3 rollback");
}else{
//從threadlocal里拿連接對(duì)象
connectionThreadLocal.get().commit();
System.out.println(RunnerIndex +" commit");
}
}
}
結(jié)果如下:

只要是線程3的statement.executeUpdate 語句運(yùn)行在前,而事務(wù)回滾語句執(zhí)行在某個(gè)commit之后,就會(huì)出現(xiàn)問題,即需要回滾的數(shù)據(jù)被提交的情況。
如下圖,3的insert結(jié)果確實(shí)沒有被回滾,而是出現(xiàn)在了表中:
所以,對(duì)于知識(shí),大家不能盲目的接收,建議抱些懷疑的態(tài)度,還是有必要的。
$ 話說回來,為什么threadlocal對(duì)同一個(gè)數(shù)據(jù)庫連接不起作用呢?
Connection是什么?
connection可以當(dāng)成是服務(wù)器和數(shù)據(jù)庫的一個(gè)會(huì)話,而statemant用來在會(huì)話的上下文中執(zhí)行sql以及返回結(jié)果。一個(gè)connection可以包含多個(gè)statement;然而在兩者中間,還有一個(gè)事務(wù)(Translation)的概念,事務(wù)用來保證其內(nèi)部的語句,要么都執(zhí)行,要么都不執(zhí)行,如果autoCommit被開啟,則默認(rèn)是一個(gè)語句一個(gè)事務(wù)。
往簡單點(diǎn)說,connection是一種共享資源,更簡單一點(diǎn),它是一個(gè)共享變量,在被連接池創(chuàng)建之后,在內(nèi)存中的地址是唯一的一個(gè)變量。
ThreadLocal能存共享變量么?
存肯定能存,但不建議,因?yàn)閷onnection set進(jìn)ThreadLocalMap,也其實(shí)是保存一個(gè)內(nèi)存對(duì)象的地址引用而已,真正使用的時(shí)候,還是唯一的那個(gè)對(duì)象在起作用。
ThreadLocal最常用的功能,是為了避免層層傳遞而提供了對(duì)象保存和獲取方法。
高中學(xué)數(shù)學(xué)的時(shí)候曾經(jīng)有過一個(gè)技巧,叫證難則反,在這里也適用。我們反過來想,如果用threadlocal的副本拷貝能實(shí)現(xiàn)connection的隔離,那豈不是只要一個(gè)connection就可以了?實(shí)時(shí)上呢,數(shù)據(jù)庫連接常常會(huì)出現(xiàn)不夠用的情況,結(jié)論就顯而易見了~
$ 話又說回來,threadLocal想要完成數(shù)據(jù)庫連接隔離的功能,需要怎么做呢?
如果非要用ThreadLocal實(shí)現(xiàn)這個(gè)連接隔離的功能,那么,只能是為每個(gè)線程創(chuàng)建新的連接,然后保存在Threadlocal中,這樣,每個(gè)線程在自己的生命周期范圍內(nèi)只會(huì)使用這個(gè)連接,即可實(shí)現(xiàn)線程隔離。
$ 話又又說回來,druid、zadl等一眾數(shù)據(jù)庫連接池是怎么進(jìn)行連接的管理工作的呢?
最大連接數(shù)為1的druid連接池原理概覽:

druid維護(hù)一個(gè)數(shù)組來存放連接 同時(shí)維護(hù)了多個(gè)變量來檢測(cè)連接池的狀態(tài),其中poolingCount用來表示池中連接的數(shù)量 當(dāng)有線程來獲取連接時(shí),需要先加鎖,對(duì)數(shù)量進(jìn)行減一操作。 當(dāng)獲取連接時(shí)發(fā)現(xiàn)數(shù)量為0 ,則返回為空 當(dāng)連接關(guān)閉時(shí),會(huì)將連接資源放回?cái)?shù)組,并對(duì)數(shù)量做加一操作。
*上述只是druid連接池的極簡版流程敘述,實(shí)際上,還有連接池空等待、滿通知、活躍數(shù)、異常數(shù)等的復(fù)雜判斷。*有興趣的同學(xué)可以看下源碼。
zdal的連接池管理源碼一覽::
public class InternalManagedConnectionPool{
//最大連接數(shù)
private final int maxSize;
//用來存放連接的鏈表
private final ArrayList connectionListeners;
//內(nèi)部的信號(hào)量,用來控制允許獲取資源的線程總數(shù)
private final InternalSemaphore permits;
//正在使用的連接數(shù)
private volatile int maxUsedConnections = 0;
protected InternalManagedConnectionPool(...){
//構(gòu)造函數(shù)中,初始化了連接池大小和信號(hào)量大小
connectionListeners = new ArrayList(this.maxSize);
permits = new InternalSemaphore(this.maxSize);
}
getConnection()方法:
//獲取連接
public ConnectionListener getConnection(){
//信號(hào)量嘗試獲取許可
if (permits.tryAcquire(poolParams.blockingTimeout, TimeUnit.MILLISECONDS)) {
ConnectionListener cl = null;
do {
//加鎖資源池
synchronized (connectionListeners) {
if (connectionListeners.size() > 0) {
//獲取list的最后一個(gè)
cl = (ConnectionListener) connectionListeners.remove(connectionListeners.size() - 1);
//最大連接數(shù) 減去 正在工作的信號(hào)量
int size = (maxSize - permits.availablePermits());
if (size > maxUsedConnections){
maxUsedConnections = size;
}
}
}
if (cl != null) {
return cl;
}
}while(connectionListeners.size() > 0);
//OK, 在連接池中找不到正在工作的連接了. 那就創(chuàng)建個(gè)新的
createNewConnection(){...}
}else{
if (this.maxSize == this.maxUsedConnections) {
throw new ResourceException(
"數(shù)據(jù)源最大連接數(shù)已滿,并且在超時(shí)時(shí)間范圍內(nèi)沒有新的連接釋放,poolName = "
+ poolName
+ " blocking timeout="
+ poolParams.blockingTimeout +
"(ms)");
}
}
這里把內(nèi)部連接池的管理類的關(guān)鍵屬性和連接獲取方法流量進(jìn)行了簡化,連接歸還就不弄了,大同小異,仔細(xì)看,我們看到了什么
volatile 標(biāo)識(shí)的maxUsedConnections用來完成線程間數(shù)據(jù)可見 隸屬于AQS系列的Semaphone,用來控制共享資源并發(fā)訪問量。
都是些常見的八股文,不過組合起來可就了不得~
$ 話又又又說回來,在druid、zdal中,threadlocal的作用體現(xiàn)在哪里呢?
我們知道,誠如druid、zdal等優(yōu)秀的中間件,可不止是數(shù)據(jù)庫連接池這一個(gè)作用,阿里數(shù)據(jù)庫中間件zdal源碼解析 文中也有提及。
那么,ThreadLocal能在這里扮演什么角色呢?
就以zdal為例,因?yàn)榘⒗锏臄?shù)據(jù)庫規(guī)?;径挤浅4螅钟幸惶淄陚涞臄?shù)據(jù)庫庫表拆分規(guī)范,因此,分庫鍵、分表鍵、主鍵、虛擬表名等在設(shè)計(jì)和存儲(chǔ)時(shí)需要遵循規(guī)范,而zdal中的解析操作,也需要與之相匹配。
這個(gè)解析工作是相對(duì)復(fù)雜且繁重的,然而,針對(duì)同一用戶的操作,通常庫表的路由是相對(duì)固定的,因此,當(dāng)我們解析過一次sql,通過各個(gè)字段和配置規(guī)則,計(jì)算出了庫表路由,那么,可以直接put進(jìn)線程上下文,供本次請(qǐng)求的后續(xù)數(shù)據(jù)庫操作使用。
public Object parse(...){
SimpleCondition simpleCondition = new SimpleCondition();
simpleCondition.setVirtualTableName("user");
simpleCondition.put("age", 10);
ThreadLocalMap.put(ThreadLocalString.ROUTE_CONDITION, simpleCondition);
}
public void 后續(xù)操作(){
RouteCondition rc = (RouteCondition) ThreadLocalMap.get(ThreadLocalString.ROUTE_CONDITION);
if (rc != null) {
//不走解析SQL,由ThreadLocal傳入的指定對(duì)象(RouteCondition),決定庫表目的地
metaData = sqlDispatcher.getDBAndTables(rc);
} else {
// 通過解析SQL來分庫分表
try {
metaData = sqlDispatcher.getDBAndTables(originalSql, parameters);
} catch (ZdalCheckedExcption e) {
throw new SQLException(e.getMessage());
}
}
}
這個(gè)也正好是對(duì)前面ThreadLocal正確使用方法的補(bǔ)充。
起因是對(duì)一篇文章敘述產(chǎn)生疑問,通過簡單的驗(yàn)證,證實(shí)了自己的想法,然后又從幾個(gè)方面對(duì)數(shù)據(jù)庫連接和threadlocal進(jìn)行了擴(kuò)展,以上,大家如果發(fā)現(xiàn)有任何問題,歡迎留言幫忙指正和補(bǔ)充。
— 【 THE END 】— 本公眾號(hào)全部博文已整理成一個(gè)目錄,請(qǐng)?jiān)诠娞?hào)里回復(fù)「m」獲??! 最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) PDF 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)
