<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>

          面試官:如何實現(xiàn)一個連接池,我當(dāng)場懵了

          共 4475字,需瀏覽 9分鐘

           ·

          2021-03-27 18:28


            點擊上方“JavaEdge”,關(guān)注公眾號

          設(shè)為“星標(biāo)”,好文章不錯過!

          什么是連接池?





          結(jié)構(gòu)


          連接池對外提供如下接口:

          • 獲得連接

          • 歸還連接

          暴露最小空閑連接數(shù)、最大連接數(shù)等客戶端可配置的參數(shù)

          對內(nèi)則實現(xiàn)如下功能:

          • 連接建立

          • 連接心跳保持

          • 連接管理

          • 空閑連接回收

          • 連接可用性檢測

          連接池結(jié)構(gòu)示意圖


          若客戶端SDK沒有使用連接池,而直接是TCP連接,就需要考慮每次建立TCP連接的開銷。因為TCP基于字節(jié)流,多線程下對同一連接操作有線程安全隱患。

          TCP連接的客戶端SDK,對外提供API方式




          連接池和連接分離式


          有一個XXXPool類負(fù)責(zé)連接池實現(xiàn):

          1. 先從其獲得連接XXXConnection

          2. 再用所獲連接請求服務(wù)端

          3. 完成后歸還連接

          XXXPool必須是線程安全的,可并發(fā)獲取和歸還連接,而XXXConnection是非線程安全的。
          對應(yīng)到連接池結(jié)構(gòu)示意圖,XXXPool就是右邊連接池那個框,左邊客戶端是我們自己的代碼。

          最佳實踐


          連接池本身一般是線程安全的,可復(fù)用。每次使用需要從連接池獲取連接,使用后歸還,歸還的工作由使用者負(fù)責(zé)。



          內(nèi)置連接池


          對外提供一個XXXClient類,通過該類可直接請求服務(wù)端。該類內(nèi)部維護了連接池,SDK使用者無需考慮連接的獲取和歸還問題。
          XXXClient是線程安全的。對應(yīng)到連接池結(jié)構(gòu)示意圖中,整個API就是藍框。

          最佳實踐


          大多數(shù)中間件、數(shù)據(jù)庫的客戶端SDK都會支持連接池,SDK負(fù)責(zé)連接的獲取和歸還,使用的時候直接復(fù)用客戶端。



          非連接池


          一般命名為XXXConnection,以區(qū)分其是基于連接池or單連接,而不建議命名為XXXClient。直接連接方式的API基于單一連接,每次使用都需要創(chuàng)建和斷開連接,性能一般,且通常不是線程安全的。對應(yīng)到連接池的結(jié)構(gòu)示意圖中,這種形式相當(dāng)于沒有右邊連接池那個框,客戶端直接連接服務(wù)端創(chuàng)建連接。

          最佳實踐


          那通常不是線程安全的,而且短連接的方式性能不會很高,使用的時候需要考慮是否自己封裝一個連接池。

          Jedis屬于哪種呢?



          多線程環(huán)境下復(fù)用一個連接會發(fā)生什么?

          首先,向Redis初始化2組數(shù)據(jù),Key=a、Value=1,Key=b、Value=2:

          @PostConstructpublic void init() {    try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {        Assert.isTrue("OK".equals(jedis.set("a", "1")), "set a = 1 return OK");        Assert.isTrue("OK".equals(jedis.set("b", "2")), "set b = 2 return OK");    }}

          然后,啟動兩個線程,共享操作同一個Jedis實例,每一個線程循環(huán)1000次,分別讀取Key為a和b的Value,判斷是否分別為1和2:

          Jedis jedis = new Jedis("127.0.0.1", 6379);new Thread(() -> {    for (int i = 0; i < 1000; i++) {        String result = jedis.get("a");        if (!result.equals("1")) {            log.warn("Expect a to be 1 but found {}", result);            return;        }    }}).start();new Thread(() -> {    for (int i = 0; i < 1000; i++) {        String result = jedis.get("b");        if (!result.equals("2")) {            log.warn("Expect b to be 2 but found {}", result);            return;        }    }}).start();TimeUnit.SECONDS.sleep(5);

          執(zhí)行多次,發(fā)現(xiàn)日志各種異常都有:

          //錯誤1[14:56:19.069] [Thread-28] [WARN ] [.t.c.c.redis.JedisMisreuseController:45  ] - Expect b to be 2 but found 1//錯誤2redis.clients.jedis.exceptions.JedisConnectionException: Unexpected end of stream.  at redis.clients.jedis.util.RedisInputStream.ensureFill(RedisInputStream.java:202)  at redis.clients.jedis.util.RedisInputStream.readLine(RedisInputStream.java:50)  at redis.clients.jedis.Protocol.processError(Protocol.java:114)  at redis.clients.jedis.Protocol.process(Protocol.java:166)  at redis.clients.jedis.Protocol.read(Protocol.java:220)  at redis.clients.jedis.Connection.readProtocolWithCheckingBroken(Connection.java:318)  at redis.clients.jedis.Connection.getBinaryBulkReply(Connection.java:255)  at redis.clients.jedis.Connection.getBulkReply(Connection.java:245)  at redis.clients.jedis.Jedis.get(Jedis.java:181)  at org.geekbang.time.commonmistakes.connectionpool.redis.JedisMisreuseController.lambda$wrong$1(JedisMisreuseController.java:43)  at java.lang.Thread.run(Thread.java:748)//錯誤3java.io.IOException: Socket Closed  at java.net.AbstractPlainSocketImpl.getOutputStream(AbstractPlainSocketImpl.java:440)  at java.net.Socket$3.run(Socket.java:954)  at java.net.Socket$3.run(Socket.java:952)  at java.security.AccessController.doPrivileged(Native Method)  at java.net.Socket.getOutputStream(Socket.java:951)  at redis.clients.jedis.Connection.connect(Connection.java:200)  ... 7 more

          讓我們分析一下Jedis類的源碼,搞清楚其中緣由吧。





          BinaryClient封裝了各種Redis命令

          都是調(diào)用其父類Connection方法,使用Protocol類發(fā)送命令

          Protocol類的sendCommand方法

          發(fā)送命令時是直接操作RedisOutputStream寫字節(jié)。

          在多線程環(huán)境下復(fù)用Jedis對象,其實就是在復(fù)用RedisOutputStream。如果多個線程在執(zhí)行操作,那么既無法確保整條命令以一個原子操作寫入Socket,也無法確保寫入后、讀取前沒有其他數(shù)據(jù)寫到遠端。
          這就能解釋了為何多線程下使用Jedis對象操作Redis會出現(xiàn)各種問題:

          • 寫操作互相干擾,多條命令交織,必然是非法的Redis命令,則Redis會關(guān)閉客戶端連接,導(dǎo)致連接斷開

          • 線程1和2先后寫入get a和get b請求,Redis也返回了值1和2,但是線程2先讀取了數(shù)據(jù)1就會出現(xiàn)數(shù)據(jù)錯亂的問題。




          修復(fù)


          使用Jedis提供的線程安全的類JedisPool來獲得Jedis的實例。JedisPool作為連接池,可以聲明為static 被多線程共享。注意使用try-with-resources模式。
          這樣使用后代碼不再有線程安全問題。最好再通過shutdownhook,在程序退出之前關(guān)閉JedisPool:

          @PostConstructpublic void init() {    Runtime.getRuntime().addShutdownHook(new Thread(() -> {        jedisPool.close();    }));}


          Jedis#close


          若Jedis是從連接池獲取的話,則close方法會調(diào)用連接池的return方法歸還連接:

          如果不是,則直接關(guān)閉連接,其最終調(diào)用Connection類的disconnect方法來關(guān)閉TCP連接:

          可見Jedis可獨立使用,也可配合連接池(JedisPool)

          JedisPool





          JedisPool繼承JedisPoolAbstract又繼承抽象類Pool,Pool內(nèi)部持有Apache Common的GenericObjectPool。


          所以JedisPool的連接池其實就是直接復(fù)用的GenericObjectPool,并沒有自己實現(xiàn)一套池子。

          綜上,Jedis API屬于連接池和連接分離的API,JedisPool是線程安全的連接池,Jedis是非線程安全的單一連接。


          往期推薦


          由于不知線程池的bug,某Java程序員叕被祭天

          程序員因重復(fù)記錄日志撐爆ELK被辭退!

          擁抱Kubernetes,再見了Spring Cloud

          JDK為何自己先破壞雙親委派模型?




          目前交流群已有 800+人,旨在促進技術(shù)交流,可關(guān)注公眾號添加筆者微信邀請進群


          喜歡文章,點個“在看、點贊、分享”素質(zhì)三連支持一下~

          瀏覽 56
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  中文字幕免费视频在线观看 | 俺去了官网 | 免费做爱视频网站 | 伊人影院大香蕉 | 在线观看区一 |