<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 9694字,需瀏覽 20分鐘

           ·

          2021-07-14 00:19

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

          來自CSDN的一篇文章,被很多號(hào)轉(zhuǎn)載過

          姑且不談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()方式獲取連接 :

          注:Runnable中故意不執(zhí)行connection.close

          結(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ā)吧。

          謝謝支持喲 (*^__^*)

          瀏覽 56
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  高清无码视频 播放免费 | 一区二区三区精品 | 91视频大香蕉 | a级视频在线观看 | 亚洲图片小说区 |