高并發(fā)服務(wù)優(yōu)化篇:淺談數(shù)據(jù)庫(kù)連接池
本文轉(zhuǎn)載自微信公眾號(hào)「Coder的技術(shù)之路」,作者Coder的技術(shù)之路。轉(zhuǎn)載本文請(qǐng)聯(lián)系Coder的技術(shù)之路公眾號(hào)。
被N多大號(hào)轉(zhuǎn)載的一篇博客,引起了我的注意,說(shuō)的是數(shù)據(jù)庫(kù)連接池使用threadlocal的原因,文中結(jié)論如下圖所示。
姑且不談threadlocal的作用和工作原理,單說(shuō)數(shù)據(jù)庫(kù)連接池這個(gè)知識(shí)點(diǎn),猛地一看挺有理;仔細(xì)一看,怎么感覺(jué)不太對(duì)啊,同學(xué),這是什么虎狼之詞。
$ 實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)
個(gè)人理解,連接池提供的獲取連接的能力,需要對(duì)"任務(wù)"唯一,即,只有當(dāng)某一線程完成了本次數(shù)據(jù)操作,將連接放回到連接池之后,其他線程才能夠再次獲取并使用。原因我們后面細(xì)說(shuō),先來(lái)親自測(cè)試一下。
連接池選一個(gè)druid,設(shè)置連接池中只有一個(gè)connection,方便驗(yàn)證多線程應(yīng)對(duì)同一個(gè)connection的場(chǎng)景。
首先,將datasource共享資源傳入線程,采用datasource.getConnection()方式獲取連接 :
注:Runnable中故意不執(zhí)行connection.close
結(jié)果如上圖:只有一個(gè)線程可以正常執(zhí)行,由于沒(méi)有被關(guān)閉,其他線程都獲取連接失敗了。說(shuō)明,數(shù)據(jù)庫(kù)連接池的作用方式是某個(gè)線程任務(wù)"獨(dú)占"的。
$ 退一步來(lái)講
假設(shè)如同開(kāi)頭文章中描述的,用了一個(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í)行回滾,用來(lái)驗(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ǔ)句運(yùn)行在前,而事務(wù)回滾語(yǔ)句執(zhí)行在某個(gè)commit之后,就會(huì)出現(xiàn)問(wèn)題,即需要回滾的數(shù)據(jù)被提交的情況。
如下圖,3的insert結(jié)果確實(shí)沒(méi)有被回滾,而是出現(xiàn)在了表中:
所以,對(duì)于知識(shí),大家不能盲目的接收,建議抱些懷疑的態(tài)度,還是有必要的。
$ 話說(shuō)回來(lái),為什么threadlocal對(duì)同一個(gè)數(shù)據(jù)庫(kù)連接不起作用呢?
Connection是什么?
connection可以當(dāng)成是服務(wù)器和數(shù)據(jù)庫(kù)的一個(gè)會(huì)話,而statemant用來(lái)在會(huì)話的上下文中執(zhí)行sql以及返回結(jié)果。一個(gè)connection可以包含多個(gè)statement;然而在兩者中間,還有一個(gè)事務(wù)(Translation)的概念,事務(wù)用來(lái)保證其內(nèi)部的語(yǔ)句,要么都執(zhí)行,要么都不執(zhí)行,如果autoCommit被開(kāi)啟,則默認(rèn)是一個(gè)語(yǔ)句一個(gè)事務(wù)。
往簡(jiǎn)單點(diǎn)說(shuō),connection是一種共享資源,更簡(jiǎn)單一點(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)有過(guò)一個(gè)技巧,叫證難則反,在這里也適用。我們反過(guò)來(lái)想,如果用threadlocal的副本拷貝能實(shí)現(xiàn)connection的隔離,那豈不是只要一個(gè)connection就可以了?實(shí)時(shí)上呢,數(shù)據(jù)庫(kù)連接常常會(huì)出現(xiàn)不夠用的情況,結(jié)論就顯而易見(jiàn)了~
$ 話又說(shuō)回來(lái),threadLocal想要完成數(shù)據(jù)庫(kù)連接隔離的功能,需要怎么做呢?
如果非要用ThreadLocal實(shí)現(xiàn)這個(gè)連接隔離的功能,那么,只能是為每個(gè)線程創(chuàng)建新的連接,然后保存在Threadlocal中,這樣,每個(gè)線程在自己的生命周期范圍內(nèi)只會(huì)使用這個(gè)連接,即可實(shí)現(xiàn)線程隔離。
$ 話又又說(shuō)回來(lái),druid、zadl等一眾數(shù)據(jù)庫(kù)連接池是怎么進(jìn)行連接的管理工作的呢?
最大連接數(shù)為1的druid連接池原理概覽:
- druid維護(hù)一個(gè)數(shù)組來(lái)存放連接
- 同時(shí)維護(hù)了多個(gè)變量來(lái)檢測(cè)連接池的狀態(tài),其中poolingCount用來(lái)表示池中連接的數(shù)量
- 當(dāng)有線程來(lái)獲取連接時(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連接池的極簡(jiǎn)版流程敘述,實(shí)際上,還有連接池空等待、滿通知、活躍數(shù)、異常數(shù)等的復(fù)雜判斷。*有興趣的同學(xué)可以看下源碼。
zdal的連接池管理源碼一覽:
- public class InternalManagedConnectionPool{
- //最大連接數(shù)
- private final int maxSize;
- //用來(lái)存放連接的鏈表
- private final ArrayList connectionListeners;
- //內(nèi)部的信號(hào)量,用來(lái)控制允許獲取資源的線程總數(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)沒(méi)有新的連接釋放,poolName = "
- + poolName
- + " blocking timeout="
- + poolParams.blockingTimeout +
- "(ms)");
- }
- }
這里把內(nèi)部連接池的管理類的關(guān)鍵屬性和連接獲取方法流量進(jìn)行了簡(jiǎn)化,連接歸還就不弄了,大同小異,仔細(xì)看,我們看到了什么
- volatile 標(biāo)識(shí)的maxUsedConnections用來(lái)完成線程間數(shù)據(jù)可見(jiàn)
- 隸屬于AQS系列的Semaphone,用來(lái)控制共享資源并發(fā)訪問(wèn)量。
都是些常見(jiàn)的八股文,不過(guò)組合起來(lái)可就了不得~
$ 話又又又說(shuō)回來(lái),在druid、zdal中,threadlocal的作用體現(xiàn)在哪里呢?
我們知道,誠(chéng)如druid、zdal等優(yōu)秀的中間件,可不止是數(shù)據(jù)庫(kù)連接池這一個(gè)作用,阿里數(shù)據(jù)庫(kù)中間件zdal源碼解析 文中也有提及。
那么,ThreadLocal能在這里扮演什么角色呢?
就以zdal為例,因?yàn)榘⒗锏臄?shù)據(jù)庫(kù)規(guī)?;径挤浅4?,但又有一套完備的數(shù)據(jù)庫(kù)庫(kù)表拆分規(guī)范,因此,分庫(kù)鍵、分表鍵、主鍵、虛擬表名等在設(shè)計(jì)和存儲(chǔ)時(shí)需要遵循規(guī)范,而zdal中的解析操作,也需要與之相匹配。
這個(gè)解析工作是相對(duì)復(fù)雜且繁重的,然而,針對(duì)同一用戶的操作,通常庫(kù)表的路由是相對(duì)固定的,因此,當(dāng)我們解析過(guò)一次sql,通過(guò)各個(gè)字段和配置規(guī)則,計(jì)算出了庫(kù)表路由,那么,可以直接put進(jìn)線程上下文,供本次請(qǐng)求的后續(xù)數(shù)據(jù)庫(kù)操作使用。
- 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),決定庫(kù)表目的地
- metaData = sqlDispatcher.getDBAndTables(rc);
- } else {
- // 通過(guò)解析SQL來(lái)分庫(kù)分表
- try {
- metaData = sqlDispatcher.getDBAndTables(originalSql, parameters);
- } catch (ZdalCheckedExcption e) {
- throw new SQLException(e.getMessage());
- }
- }
- }
這個(gè)也正好是對(duì)前面ThreadLocal正確使用方法的補(bǔ)充。
起因是對(duì)一篇文章敘述產(chǎn)生疑問(wèn),通過(guò)簡(jiǎn)單的驗(yàn)證,證實(shí)了自己的想法,然后又從幾個(gè)方面對(duì)數(shù)據(jù)庫(kù)連接和threadlocal進(jìn)行了擴(kuò)展,以上,大家如果發(fā)現(xiàn)有任何問(wèn)題,歡迎留言幫忙指正和補(bǔ)充。