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

          面試官:Java池化技術(shù)你了解多少?

          共 11055字,需瀏覽 23分鐘

           ·

          2022-08-03 10:20

          點(diǎn)擊關(guān)注公眾號(hào):互聯(lián)網(wǎng)架構(gòu)師,后臺(tái)回復(fù) 2T獲取2TB學(xué)習(xí)資源!

          上一篇:Alibaba開源內(nèi)網(wǎng)高并發(fā)編程手冊.pdf

          在我們平常的編碼中,通常會(huì)將一些對象保存起來,這主要考慮的是對象的創(chuàng)建成本。比如像線程資源、數(shù)據(jù)庫連接資源或者 TCP 連接等,這類對象的初始化通常要花費(fèi)比較長的時(shí)間,如果頻繁地申請和銷毀,就會(huì)耗費(fèi)大量的系統(tǒng)資源,造成不必要的性能損失。

          并且這些對象都有一個(gè)顯著的特征,就是通過輕量級(jí)的重置工作,可以循環(huán)、重復(fù)地使用。這個(gè)時(shí)候,我們就可以使用一個(gè)虛擬的池子,將這些資源保存起來,當(dāng)使用的時(shí)候,我們就從池子里快速獲取一個(gè)即可。

          在 Java 中,池化技術(shù)應(yīng)用非常廣泛,常見的就有數(shù)據(jù)庫連接池、線程池等,本文主講連接池,線程池我們將在后續(xù)的博客中進(jìn)行介紹。

          公用池化包 Commons Pool 2

          簡介

          我們首先來看一下 Java 中公用的池化包 Commons Pool 2,來了解一下對象池的一般結(jié)構(gòu)。根據(jù)我們的業(yè)務(wù)需求,使用這套 API 能夠很容易實(shí)現(xiàn)對象的池化管理。

          <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
          <dependency>
              <groupId>org.apache.commons</groupId>
              <artifactId>commons-pool2</artifactId>
              <version>2.11.1</version>
          </dependency>

          GenericObjectPool 是對象池的核心類,通過傳入一個(gè)對象池的配置和一個(gè)對象的工廠,即可快速創(chuàng)建對象池。

          public GenericObjectPool
                      final PooledObjectFactory<T> factory, 
                      final GenericObjectPoolConfig<T> config)

          案例

          Redis 的常用客戶端 Jedis,就是使用 Commons Pool 管理連接池的,可以說是一個(gè)最佳實(shí)踐。下圖是 Jedis 使用工廠創(chuàng)建對象的主要代碼塊。對象工廠類最主要的方法就是makeObject,它的返回值是 PooledObject 類型,可以將對象使用 new DefaultPooledObject<>(obj) 進(jìn)行簡單包裝返回。

          redis.clients.jedis.JedisFactory,使用工廠創(chuàng)建對象。

          @Override
          public PooledObject<Jedis> makeObject() throws Exception {
            Jedis jedis = null;
            try {
              jedis = new Jedis(jedisSocketFactory, clientConfig);
              //主要的耗時(shí)操作
              jedis.connect();
              //返回包裝對象
              return new DefaultPooledObject<>(jedis);
            } catch (JedisException je) {
              if (jedis != null) {
                try {
                  jedis.quit();
                } catch (RuntimeException e) {
                  logger.warn("Error while QUIT", e);
                }
                try {
                  jedis.close();
                } catch (RuntimeException e) {
                  logger.warn("Error while close", e);
                }
              }
              throw je;
            }
          }

          我們再來介紹一下對象的生成過程,如下圖,對象在進(jìn)行獲取時(shí),將首先嘗試從對象池里拿出一個(gè),如果對象池中沒有空閑的對象,就使用工廠類提供的方法,生成一個(gè)新的。

          public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception {
              //此處省略若干行
              while (p == null) {
                  create = false;
                  //首先嘗試從池子中獲取。
                  p = idleObjects.pollFirst();
                  // 池子里獲取不到,才調(diào)用工廠內(nèi)生成新實(shí)例
                  if (p == null) {
                      p = create();
                      if (p != null) {
                          create = true;
                      }
                  }
                  //此處省略若干行
              }
              //此處省略若干行
          }

          那對象是存在什么地方的呢?這個(gè)存儲(chǔ)的職責(zé),就是由一個(gè)叫作 LinkedBlockingDeque的結(jié)構(gòu)來承擔(dān)的,它是一個(gè)雙向的隊(duì)列。

          接下來看一下 GenericObjectPoolConfig 的主要屬性:

          // GenericObjectPoolConfig本身的屬性
          private int maxTotal = DEFAULT_MAX_TOTAL;
          private int maxIdle = DEFAULT_MAX_IDLE;
          private int minIdle = DEFAULT_MIN_IDLE;
          // 其父類BaseObjectPoolConfig的屬性
          private boolean lifo = DEFAULT_LIFO;
          private boolean fairness = DEFAULT_FAIRNESS;
          private long maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;
          private long minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
          private long evictorShutdownTimeoutMillis = DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;
          private long softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
          private int numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
          private EvictionPolicy<T> evictionPolicy = null
          // Only 2.6.0 applications set this 
          private String evictionPolicyClassName = DEFAULT_EVICTION_POLICY_CLASS_NAME;
          private boolean testOnCreate = DEFAULT_TEST_ON_CREATE;
          private boolean testOnBorrow = DEFAULT_TEST_ON_BORROW;
          private boolean testOnReturn = DEFAULT_TEST_ON_RETURN;
          private boolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
          private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
          private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;

          參數(shù)很多,要想了解參數(shù)的意義,我們首先來看一下一個(gè)池化對象在整個(gè)池子中的生命周期。如下圖所示,池子的操作主要有兩個(gè):一個(gè)是業(yè)務(wù)線程,一個(gè)是檢測線程。

          對象池在進(jìn)行初始化時(shí),要指定三個(gè)主要的參數(shù):

          • maxTotal 對象池中管理的對象上限
          • maxIdle 最大空閑數(shù)
          • minIdle 最小空閑數(shù)

          其中 maxTotal 和業(yè)務(wù)線程有關(guān),當(dāng)業(yè)務(wù)線程想要獲取對象時(shí),會(huì)首先檢測是否有空閑的對象。如果有,則返回一個(gè);否則進(jìn)入創(chuàng)建邏輯。此時(shí),如果池中個(gè)數(shù)已經(jīng)達(dá)到了最大值,就會(huì)創(chuàng)建失敗,返回空對象。

          對象在獲取的時(shí)候,有一個(gè)非常重要的參數(shù),那就是最大等待時(shí)間(maxWaitMillis),這個(gè)參數(shù)對應(yīng)用方的性能影響是比較大的。該參數(shù)默認(rèn)為 -1,表示永不超時(shí),直到有對象空閑。

          如下圖,如果對象創(chuàng)建非常緩慢或者使用非常繁忙,業(yè)務(wù)線程會(huì)持續(xù)阻塞 (blockWhenExhausted 默認(rèn)為 true),進(jìn)而導(dǎo)致正常服務(wù)也不能運(yùn)行。

          面試題

          一般面試官會(huì)問:你會(huì)把超時(shí)參數(shù)設(shè)置成多大呢?

          我一般都會(huì)把最大等待時(shí)間,設(shè)置成接口可以忍受的最大延遲。比如,一個(gè)正常服務(wù)響應(yīng)時(shí)間 10ms 左右,達(dá)到 1 秒鐘就會(huì)感覺到卡頓,那么這個(gè)參數(shù)設(shè)置成 500~1000ms 都是可以的。超時(shí)之后,會(huì)拋出 NoSuchElementException 異常,請求會(huì)快速失敗,不會(huì)影響其他業(yè)務(wù)線程,這種 Fail Fast 的思想,在互聯(lián)網(wǎng)應(yīng)用非常廣泛。

          帶有evcit 字樣的參數(shù),主要是處理對象逐出的。池化對象除了初始化和銷毀的時(shí)候比較昂貴,在運(yùn)行時(shí)也會(huì)占用系統(tǒng)資源。比如,連接池會(huì)占用多條連接,線程池會(huì)增加調(diào)度開銷等。業(yè)務(wù)在突發(fā)流量下,會(huì)申請到超出正常情況的對象資源,放在池子中。等這些對象不再被使用,我們就需要把它清理掉。

          超出 minEvictableIdleTimeMillis 參數(shù)指定值的對象,就會(huì)被強(qiáng)制回收掉,這個(gè)值默認(rèn)是 30 分鐘;softMinEvictableIdleTimeMillis 參數(shù)類似,但它只有在當(dāng)前對象數(shù)量大于 minIdle 的時(shí)候才會(huì)執(zhí)行移除,所以前者的動(dòng)作要更暴力一些。

          還有 4 個(gè) test 參數(shù):testOnCreate、testOnBorrowtestOnReturn、testWhileIdle,分別指定了在創(chuàng)建、獲取、歸還、空閑檢測的時(shí)候,是否對池化對象進(jìn)行有效性檢測。

          開啟這些檢測,能保證資源的有效性,但它會(huì)耗費(fèi)性能,所以默認(rèn)為 false。生產(chǎn)環(huán)境上,建議只將 testWhileIdle 設(shè)置為 true,并通過調(diào)整空閑檢測時(shí)間間隔(timeBetweenEvictionRunsMillis),比如 1 分鐘,來保證資源的可用性,同時(shí)也保證效率。

          JMH 測試

          使用連接池和不使用連接池,它們之間的性能差距到底有多大呢?下面是一個(gè)簡單的 JMH 測試?yán)樱ㄒ妭}庫),進(jìn)行一個(gè)簡單的 set 操作,為 redis 的 key 設(shè)置一個(gè)隨機(jī)值。

          @Fork(2
          @State(Scope.Benchmark) 
          @Warmup(iterations = 5, time = 1
          @Measurement(iterations = 5, time = 1
          @BenchmarkMode(Mode.Throughput) 
          public class JedisPoolVSJedisBenchmark 
             JedisPool pool = new JedisPool("localhost"6379); 

             @Benchmark 
             public void testPool() 
                 Jedis jedis = pool.getResource(); 
                 jedis.set("a", UUID.randomUUID().toString()); 
                 jedis.close(); 
             } 

             @Benchmark 
             public void testJedis() 
                 Jedis jedis = new Jedis("localhost"6379); 
                 jedis.set("a", UUID.randomUUID().toString()); 
                 jedis.close(); 
             } 
             //此處省略若干行
          }

          將測試結(jié)果使用 meta-chart 作圖,展示結(jié)果如下圖所示,可以看到使用了連接池的方式,它的吞吐量是未使用連接池方式的 5 倍!

          數(shù)據(jù)庫連接池 HikariCP

          HikariCP 源于日語“光る”,光的意思,寓意軟件工作速度和光速一樣快,它是 SpringBoot 中默認(rèn)的數(shù)據(jù)庫連接池。數(shù)據(jù)庫是我們工作中經(jīng)常使用到的組件,針對數(shù)據(jù)庫設(shè)計(jì)的客戶端連接池是非常多的,它的設(shè)計(jì)原理與我們在本文開頭提到的基本一致,可以有效地減少數(shù)據(jù)庫連接創(chuàng)建、銷毀的資源消耗。

          同是連接池,它們的性能也是有差別的,下圖是 HikariCP 官方的一張測試圖,可以看到它優(yōu)異的性能,官方的 JMH 測試代碼見 Github。

          一般面試題是這么問的:HikariCP 為什么快呢?主要有三個(gè)方面:

          • 它使用 FastList 替代 ArrayList,通過初始化的默認(rèn)值,減少了越界檢查的操作;

          • 優(yōu)化并精簡了字節(jié)碼,通過使用 Javassist,減少了動(dòng)態(tài)代理的性能損耗,比如使用 invokestatic 指令代替 invokevirtual 指令;

          • 實(shí)現(xiàn)了無鎖的 ConcurrentBag,減少了并發(fā)場景下的鎖競爭。

          HikariCP 對性能的一些優(yōu)化操作,是非常值得我們借鑒的,在之后的博客中,我們將詳細(xì)分析幾個(gè)優(yōu)化場景。

          數(shù)據(jù)庫連接池同樣面臨一個(gè)最大值(maximumPoolSize)和最小值(minimumIdle)的問題。這里同樣有一個(gè)非常高頻的面試題:你平常會(huì)把連接池設(shè)置成多大呢?

          很多同學(xué)認(rèn)為,連接池的大小設(shè)置得越大越好,有的同學(xué)甚至把這個(gè)值設(shè)置成 1000 以上,這是一種誤解。根據(jù)經(jīng)驗(yàn),數(shù)據(jù)庫連接,只需要 20~50 個(gè)就夠用了。具體的大小,要根據(jù)業(yè)務(wù)屬性進(jìn)行調(diào)整,但大得離譜肯定是不合適的。

          HikariCP 官方是不推薦設(shè)置 minimumIdle 這個(gè)值的,它將被默認(rèn)設(shè)置成和 maximumPoolSize 一樣的大小。如果你的數(shù)據(jù)庫Server端連接資源空閑較大,不妨也可以去掉連接池的動(dòng)態(tài)調(diào)整功能。

          另外,根據(jù)數(shù)據(jù)庫查詢和事務(wù)類型,一個(gè)應(yīng)用中是可以配置多個(gè)數(shù)據(jù)庫連接池的,這個(gè)優(yōu)化技巧很少有人知道,在此簡要描述一下。

          業(yè)務(wù)類型通常有兩種:一種需要快速的響應(yīng)時(shí)間,把數(shù)據(jù)盡快返回給用戶;另外一種是可以在后臺(tái)慢慢執(zhí)行,耗時(shí)比較長,對時(shí)效性要求不高。如果這兩種業(yè)務(wù)類型,共用一個(gè)數(shù)據(jù)庫連接池,就容易發(fā)生資源爭搶,進(jìn)而影響接口響應(yīng)速度。雖然微服務(wù)能夠解決這種情況,但大多數(shù)服務(wù)是沒有這種條件的,這時(shí)就可以對連接池進(jìn)行拆分。

          如圖,在同一個(gè)業(yè)務(wù)中,根據(jù)業(yè)務(wù)的屬性,我們分了兩個(gè)連接池,就是來處理這種情況的。

          HikariCP 還提到了另外一個(gè)知識(shí)點(diǎn),在 JDBC4 的協(xié)議中,通過 Connection.isValid() 就可以檢測連接的有效性。這樣,我們就不用設(shè)置一大堆的 test 參數(shù)了,HikariCP 也沒有提供這樣的參數(shù)。

          結(jié)果緩存池

          到了這里你可能會(huì)發(fā)現(xiàn)池(Pool)與緩存(Cache)有許多相似之處。

          它們之間的一個(gè)共同點(diǎn),就是將對象加工后,存儲(chǔ)在相對高速的區(qū)域。我習(xí)慣性將緩存看作是數(shù)據(jù)對象,而把池中的對象看作是執(zhí)行對象。緩存中的數(shù)據(jù)有一個(gè)命中率問題,而池中的對象一般都是對等的。

          考慮下面一個(gè)場景,jsp 提供了網(wǎng)頁的動(dòng)態(tài)功能,它可以在執(zhí)行后,編譯成 class 文件,加快執(zhí)行速度;再或者,一些媒體平臺(tái),會(huì)將熱門文章,定時(shí)轉(zhuǎn)化成靜態(tài)的 html 頁面,僅靠 nginx 的負(fù)載均衡即可應(yīng)對高并發(fā)請求(動(dòng)靜分離)。

          這些時(shí)候,你很難說清楚,這是針對緩存的優(yōu)化,還是針對對象進(jìn)行了池化,它們在本質(zhì)上只是保存了某個(gè)執(zhí)行步驟的結(jié)果,使得下次訪問時(shí)不需要從頭再來。我通常把這種技術(shù)叫作結(jié)果緩存池(Result Cache Pool),屬于多種優(yōu)化手段的綜合。

          小結(jié)

          下面我來簡單總結(jié)一下本文的內(nèi)容重點(diǎn):

          我們從 Java 中最通用的公用池化包 Commons Pool 2 說起,介紹了它的一些實(shí)現(xiàn)細(xì)節(jié),并對一些重要參數(shù)的應(yīng)用做了講解;Jedis 就是在 Commons Pool 2 的基礎(chǔ)上封裝的,通過 JMH 測試,我們發(fā)現(xiàn)對象池化之后,有了接近 5 倍的性能提升;接下來介紹了數(shù)據(jù)庫連接池中速度速快的 HikariCP ,它在池化技術(shù)之上,又通過編碼技巧進(jìn)行了進(jìn)一步的性能提升,HikariCP 是我重點(diǎn)研究的類庫之一,我也建議你加入自己的任務(wù)清單中。

          總體來說,當(dāng)你遇到下面的場景,就可以考慮使用池化來增加系統(tǒng)性能:

          • 對象的創(chuàng)建或者銷毀,需要耗費(fèi)較多的系統(tǒng)資源;
          • 對象的創(chuàng)建或者銷毀,耗時(shí)長,需要繁雜的操作和較長時(shí)間的等待;
          • 對象創(chuàng)建后,通過一些狀態(tài)重置,可被反復(fù)使用。

          將對象池化之后,只是開啟了第一步優(yōu)化。要想達(dá)到最優(yōu)性能,就不得不調(diào)整池的一些關(guān)鍵參數(shù),合理的池大小加上合理的超時(shí)時(shí)間,就可以讓池發(fā)揮更大的價(jià)值。和緩存的命中率類似,對池的監(jiān)控也是非常重要的。

          如下圖,可以看到數(shù)據(jù)庫連接池連接數(shù)長時(shí)間保持在高位不釋放,同時(shí)等待的線程數(shù)急劇增加,這就能幫我們快速定位到數(shù)據(jù)庫的事務(wù)問題。

          平常的編碼中,有很多類似的場景。比如 Http 連接池,Okhttp 和 Httpclient 就都提供了連接池的概念,你可以類比著去分析一下,關(guān)注點(diǎn)也是在連接大小和超時(shí)時(shí)間上;在底層的中間件,比如 RPC,也通常使用連接池技術(shù)加速資源獲取,比如 Dubbo 連接池、 Feign 切換成 httppclient 的實(shí)現(xiàn)等技術(shù)。

          你會(huì)發(fā)現(xiàn),在不同資源層面的池化設(shè)計(jì)也是類似的。比如線程池,通過隊(duì)列對任務(wù)進(jìn)行了二層緩沖,提供了多樣的拒絕策略等,線程池我們將在后續(xù)的文章中進(jìn)行介紹。

          線程池的這些特性,你同樣可以借鑒到連接池技術(shù)中,用來緩解請求溢出,創(chuàng)建一些溢出策略?,F(xiàn)實(shí)情況中,我們也會(huì)這么做。那么具體怎么做?有哪些做法?這部分內(nèi)容就留給大家思考了。

          來源:tomcat.blog.csdn.net/article/details/123867269

          -End-

          最后,關(guān)注公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師,在后臺(tái)回復(fù):2T,可以獲取我整理的 Java 系列面試題和答案,非常齊全


          正文結(jié)束


          推薦閱讀 ↓↓↓

          1.全新 IDEA 2022.2 正式發(fā)布,新特性真香!

          2.從零開始搭建創(chuàng)業(yè)公司后臺(tái)技術(shù)棧

          3.程序員一般可以從什么平臺(tái)接私活?

          4.Spring中毒太深,離開Spring我連最基本的CRUD都不會(huì)寫了...

          5.為什么國內(nèi) 996 干不過國外的 955呢?

          6.中國的鐵路訂票系統(tǒng)在世界上屬于什么水平?                        

          7.15張圖看懂瞎忙和高效的區(qū)別!


          瀏覽 32
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  一级a一级a爰片免费啪啪女女 | 黄色免费网站在线看 | 日韩99| 日本东京热视频在线播放 | 精品久久中文娱乐网 |