天天看點

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

閱讀本文大約需要 30 分鐘。

Redis 作為優秀的記憶體資料庫,其擁有非常高的性能,單個執行個體的 OPS 能夠達到 10W 左右。但也正是以如此,當我們在使用 Redis 時,如果發現操作延遲變大的情況,就會與我們的預期不符。

你也許或多或少地,也遇到過以下這些場景:

  • 在 Redis 上執行同樣的指令,為什麼有時響應很快,有時卻很慢?
  • 為什麼 Redis 執行 SET、DEL 指令耗時也很久?
  • 為什麼我的 Redis 突然慢了一波,之後又恢複正常了?
  • 為什麼我的 Redis 穩定運作了很久,突然從某個時間點開始變慢了?
  • ...

如果你并不清楚 Redis 内部的實作原理,那麼在排查這種延遲問題時就會一頭霧水。

如果你也遇到了以上情況,那麼,這篇文章将會給你一個「全面」的問題排查思路,并且針對這些導緻變慢的場景,我還會給你一個高效的解決方案。

在正文開始之前,我需要提醒你的是,這篇文章很長,涵蓋的 Redis 知識點也非常廣,全篇文章接近 2W 字,如果此時你的閱讀環境不适合專注閱讀,我建議你先收藏此文章,然後在合适的時間專注閱讀這篇文章。

如果你能耐心且認真地讀完這篇文章,我可以保證,你對 Redis 的性能調優将會有非常大的收獲。

如果你準備好了,那就跟着我的思路開始吧!

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

Redis真的變慢了嗎?

首先,在開始之前,你需要弄清楚 Redis 是否真的變慢了?

如果你發現你的業務服務 API 響應延遲變長,首先你需要先排查服務内部,究竟是哪個環節拖慢了整個服務。

比較高效的做法是,在服務内部內建鍊路追蹤,也就是在服務通路外部依賴的出入口,記錄下每次請求外部依賴的響應延時。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

如果你發現确實是操作 Redis 的這條鍊路耗時變長了,那麼此刻你需要把焦點關注在業務服務到 Redis 這條鍊路上。

從你的業務服務到 Redis 這條鍊路變慢的原因可能也有 2 個:

  1. 業務伺服器到 Redis 伺服器之間的網絡存在問題,例如網絡線路品質不佳,網絡資料包在傳輸時存在延遲、丢包等情況
  2. Redis 本身存在問題,需要進一步排查是什麼原因導緻 Redis 變慢

通常來說,第一種情況發生的機率比較小,如果是伺服器之間網絡存在問題,那部署在這台業務伺服器上的所有服務都會發生網絡延遲的情況,此時你需要聯系網絡運維同僚,讓其協助解決網絡問題。

我們這篇文章,重點關注的是第二種情況。

也就是從 Redis 角度來排查,是否存在導緻變慢的場景,以及都有哪些因素會導緻 Redis 的延遲增加,然後針對性地進行優化。

排除網絡原因,如何确認你的 Redis 是否真的變慢了?

首先,你需要對 Redis 進行基準性能測試,了解你的 Redis 在生産環境伺服器上的基準性能。

什麼是基準性能?

簡單來講,基準性能就是指 Redis 在一台負載正常的機器上,其最大的響應延遲和平均響應延遲分别是怎樣的?

為什麼要測試基準性能?我參考别人提供的響應延遲,判斷自己的 Redis 是否變慢不行嗎?

答案是否定的。

因為 Redis 在不同的軟硬體環境下,它的性能是各不相同的。

例如,我的機器配置比較低,當延遲為 2ms 時,我就認為 Redis 變慢了,但是如果你的硬體配置比較高,那麼在你的運作環境下,可能延遲是 0.5ms 時就可以認為 Redis 變慢了。

是以,你隻有了解了你的 Redis 在生産環境伺服器上的基準性能,才能進一步評估,當其延遲達到什麼程度時,才認為 Redis 确實變慢了。

具體如何做?

為了避免業務伺服器到 Redis 伺服器之間的網絡延遲,你需要直接在 Redis 伺服器上測試執行個體的響應延遲情況。執行以下指令,就可以測試出這個執行個體 60 秒内的最大響應延遲:

$ redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 1 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 17 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 31 microseconds.
Max latency so far: 32 microseconds.
Max latency so far: 59 microseconds.
Max latency so far: 72 microseconds.

1428669267 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run).
Worst run took 1429x longer than the average latency.
           

從輸出結果可以看到,這 60 秒内的最大響應延遲為 72 微秒(0.072毫秒)。

你還可以使用以下指令,檢視一段時間内 Redis 的最小、最大、平均通路延遲:

$ redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.08 (99 samples) -- 1.01 seconds range
...
           

以上輸出結果是,每間隔 1 秒,采樣 Redis 的平均操作耗時,其結果分布在 0.08 ~ 0.13 毫秒之間。

了解了基準性能測試方法,那麼你就可以按照以下幾步,來判斷你的 Redis 是否真的變慢了:

  1. 在相同配置的伺服器上,測試一個正常 Redis 執行個體的基準性能
  2. 找到你認為可能變慢的 Redis 執行個體,測試這個執行個體的基準性能
  3. 如果你觀察到,這個執行個體的運作延遲是正常 Redis 基準性能的 2 倍以上,即可認為這個 Redis 執行個體确實變慢了

确認是 Redis 變慢了,那如何排查是哪裡發生了問題呢?

下面跟着我的思路,我們從易到難,一步步來分析可能導緻 Redis 變慢的因素。

使用複雜度過高的指令

首先,第一步,你需要去檢視一下 Redis 的慢日志(slowlog)。

Redis 提供了慢日志指令的統計功能,它記錄了有哪些指令在執行時耗時比較久。

檢視 Redis 慢日志之前,你需要設定慢日志的門檻值。例如,設定慢日志的門檻值為 5 毫秒,并且保留最近 500 條慢日志記錄:

# 指令執行耗時超過 5 毫秒,記錄慢日志
CONFIG SET slowlog-log-slower-than 5000
# 隻保留最近 500 條慢日志
CONFIG SET slowlog-max-len 500
           

設定完成之後,所有執行的指令如果操作耗時超過了 5 毫秒,都會被 Redis 記錄下來。

此時,你可以執行以下指令,就可以查詢到最近記錄的慢日志:

127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693       # 慢日志ID
   2) (integer) 1593763337  # 執行時間戳
   3) (integer) 5299        # 執行耗時(微秒)
   4) 1) "LRANGE"           # 具體執行的指令和參數
      2) "user_list:2000"
      3) "0"
      4) "-1"
2) 1) (integer) 32692
   2) (integer) 1593763337
   3) (integer) 5044
   4) 1) "GET"
      2) "user_info:1000"
...
           

通過檢視慢日志,我們就可以知道在什麼時間點,執行了哪些指令比較耗時。

如果你的應用程式執行的 Redis 指令有以下特點,那麼有可能會導緻操作延遲變大:

  1. 經常使用 O(N) 以上複雜度的指令,例如 SORT、SUNION、ZUNIONSTORE 聚合類指令
  2. 使用 O(N) 複雜度的指令,但 N 的值非常大

第一種情況導緻變慢的原因在于,Redis 在操作記憶體資料時,時間複雜度過高,要花費更多的 CPU 資源。

第二種情況導緻變慢的原因在于,Redis 一次需要傳回給用戶端的資料過多,更多時間花費在資料協定的組裝和網絡傳輸過程中。

另外,我們還可以從資源使用率層面來分析,如果你的應用程式操作 Redis 的 OPS 不是很大,但 Redis 執行個體的 CPU 使用率卻很高,那麼很有可能是使用了複雜度過高的指令導緻的。

除此之外,我們都知道,Redis 是單線程處理用戶端請求的,如果你經常使用以上指令,那麼當 Redis 處理用戶端請求時,一旦前面某個指令發生耗時,就會導緻後面的請求發生排隊,對于用戶端來說,響應延遲也會變長。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

針對這種情況如何解決呢?

答案很簡單,你可以使用以下方法優化你的業務:

  1. 盡量不使用 O(N) 以上複雜度過高的指令,對于資料的聚合操作,放在用戶端做
  2. 執行 O(N) 指令,保證 N 盡量的小(推薦 N <= 300),每次擷取盡量少的資料,讓 Redis 可以及時處理傳回

操作bigkey

如果你查詢慢日志發現,并不是複雜度過高的指令導緻的,而都是 SET / DEL 這種簡單指令出現在慢日志中,那麼你就要懷疑你的執行個體否寫入了 bigkey。

Redis 在寫入資料時,需要為新的資料配置設定記憶體,相對應的,當從 Redis 中删除資料時,它會釋放對應的記憶體空間。

如果一個 key 寫入的 value 非常大,那麼 Redis 在配置設定記憶體時就會比較耗時。同樣的,當删除這個 key 時,釋放記憶體也會比較耗時,這種類型的 key 我們一般稱之為 bigkey。

此時,你需要檢查你的業務代碼,是否存在寫入 bigkey 的情況。你需要評估寫入一個 key 的資料大小,盡量避免一個 key 存入過大的資料。

如果已經寫入了 bigkey,那有沒有什麼辦法可以掃描出執行個體中 bigkey 的分布情況呢?

答案是可以的。

Redis 提供了掃描 bigkey 的指令,執行以下指令就可以掃描出,一個執行個體中 bigkey 的分布情況,輸出結果是以類型次元展示的:

$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

...
-------- summary -------

Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)

Biggest string found 'key:291880' has 10 bytes
Biggest   list found 'mylist:004' has 40 items
Biggest    set found 'myset:2386' has 38 members
Biggest   hash found 'myhash:3574' has 37 fields
Biggest   zset found 'myzset:2704' has 42 members

36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)
           

從輸出結果我們可以很清晰地看到,每種資料類型所占用的最大記憶體 / 擁有最多元素的 key 是哪一個,以及每種資料類型在整個執行個體中的占比和平均大小 / 元素數量。

其實,使用這個指令的原理,就是 Redis 在内部執行了 SCAN 指令,周遊整個執行個體中所有的 key,然後針對 key 的類型,分别執行 STRLEN、LLEN、HLEN、SCARD、ZCARD 指令,來擷取 String 類型的長度、容器類型(List、Hash、Set、ZSet)的元素個數。

這裡我需要提醒你的是,當執行這個指令時,要注意 2 個問題:

  1. 對線上執行個體進行 bigkey 掃描時,Redis 的 OPS 會突增,為了降低掃描過程中對 Redis 的影響,最好控制一下掃描的頻率,指定 -i 參數即可,它表示掃描過程中每次掃描後休息的時間間隔,機關是秒
  2. 掃描結果中,對于容器類型(List、Hash、Set、ZSet)的 key,隻能掃描出元素最多的 key。但一個 key 的元素多,不一定表示占用記憶體也多,你還需要根據業務情況,進一步評估記憶體占用情況

那針對 bigkey 導緻延遲的問題,有什麼好的解決方案呢?

這裡有兩點可以優化:

  1. 業務應用盡量避免寫入 bigkey
  2. 如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 指令替代 DEL,此指令可以把釋放 key 記憶體的操作,放到背景線程中去執行,進而降低對 Redis 的影響
  3. 如果你使用的 Redis 是 6.0 以上版本,可以開啟 lazy-free 機制(lazyfree-lazy-user-del = yes),在執行 DEL 指令時,釋放記憶體也會放到背景線程中執行

但即便可以使用方案 2,我也不建議你在執行個體中存入 bigkey。

這是因為 bigkey 在很多場景下,依舊會産生性能問題。例如,bigkey 在分片叢集模式下,對于資料的遷移也會有性能影響,以及我後面即将講到的資料過期、資料淘汰、透明大頁,都會受到 bigkey 的影響。

集中過期

如果你發現,平時在操作 Redis 時,并沒有延遲很大的情況發生,但在某個時間點突然出現一波延時,其現象表現為:變慢的時間點很有規律,例如某個整點,或者每間隔多久就會發生一波延遲。

如果是出現這種情況,那麼你需要排查一下,業務代碼中是否存在設定大量 key 集中過期的情況。

如果有大量的 key 在某個固定時間點集中過期,在這個時間點通路 Redis 時,就有可能導緻延時變大。

為什麼集中過期會導緻 Redis 延遲變大?

這就需要我們了解 Redis 的過期政策是怎樣的。

Redis 的過期資料采用被動過期 + 主動過期兩種政策:

  1. 被動過期:隻有當通路某個 key 時,才判斷這個 key 是否已過期,如果已過期,則從執行個體中删除
  2. 主動過期:Redis 内部維護了一個定時任務,預設每隔 100 毫秒(1秒10次)就會從全局的過期哈希表中随機取出 20 個 key,然後删除其中過期的 key,如果過期 key 的比例超過了 25%,則繼續重複此過程,直到過期 key 的比例下降到 25% 以下,或者這次任務的執行耗時超過了 25 毫秒,才會退出循環

注意,這個主動過期 key 的定時任務,是在 Redis 主線程中執行的。

也就是說如果在執行主動過期的過程中,出現了需要大量删除過期 key 的情況,那麼此時應用程式在通路 Redis 時,必須要等待這個過期任務執行結束,Redis 才可以服務這個用戶端請求。

此時就會出現,應用通路 Redis 延時變大。

如果此時需要過期删除的是一個 bigkey,那麼這個耗時會更久。而且,這個操作延遲的指令并不會記錄在慢日志中。

因為慢日志中隻記錄一個指令真正操作記憶體資料的耗時,而 Redis 主動删除過期 key 的邏輯,是在指令真正執行之前執行的。

是以,此時你會看到,慢日志中沒有操作耗時的指令,但我們的應用程式卻感覺到了延遲變大,其實時間都花費在了删除過期 key 上,這種情況我們需要尤為注意。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

那遇到這種情況,如何分析和排查?

此時,你需要檢查你的業務代碼,是否存在集中過期 key 的邏輯。

一般集中過期使用的是 expireat / pexpireat 指令,你需要在代碼中搜尋這個關鍵字。

排查代碼後,如果确實存在集中過期 key 的邏輯存在,但這種邏輯又是業務所必須的,那此時如何優化,同時又不對 Redis 有性能影響呢?

一般有兩種方案來規避這個問題:

  1. 集中過期 key 增加一個随機過期時間,把集中過期的時間打散,降低 Redis 清理過期 key 的壓力
  2. 如果你使用的 Redis 是 4.0 以上版本,可以開啟 lazy-free 機制,當删除過期 key 時,把釋放記憶體的操作放到背景線程中執行,避免阻塞主線程

第一種方案,在設定 key 的過期時間時,增加一個随機時間,僞代碼可以這麼寫:

# 在過期時間點之後的 5 分鐘内随機過期掉
redis.expireat(key, expire_time + random(300))
           

這樣一來,Redis 在處理過期時,不會因為集中删除過多的 key 導緻壓力過大,進而避免阻塞主線程。

第二種方案,Redis 4.0 以上版本,開啟 lazy-free 機制:

# 釋放過期 key 的記憶體,放到背景線程執行
lazyfree-lazy-expire yes
           

另外,除了業務層面的優化和修改配置之外,你還可以通過運維手段及時發現這種情況。

運維層面,你需要把 Redis 的各項運作狀态資料監控起來,在 Redis 上執行 INFO 指令就可以拿到這個執行個體所有的運作狀态資料。

在這裡我們需要重點關注 expired_keys 這一項,它代表整個執行個體到目前為止,累計删除過期 key 的數量。

你需要把這個名額監控起來,當這個名額在很短時間内出現了突增,需要及時報警出來,然後與業務應用報慢的時間點進行對比分析,确認時間是否一緻,如果一緻,則可以确認确實是因為集中過期 key 導緻的延遲變大。

執行個體記憶體達到上限

如果你的 Redis 執行個體設定了記憶體上限 maxmemory,那麼也有可能導緻 Redis 變慢。

當我們把 Redis 當做純緩存使用時,通常會給這個執行個體設定一個記憶體上限 maxmemory,然後設定一個資料淘汰政策。

而當執行個體的記憶體達到了 maxmemory 後,你可能會發現,在此之後每次寫入新資料,操作延遲變大了。

這是為什麼?

原因在于,當 Redis 記憶體達到 maxmemory 後,每次寫入新的資料之前,Redis 必須先從執行個體中踢出一部分資料,讓整個執行個體的記憶體維持在 maxmemory 之下,然後才能把新資料寫進來。

這個踢出舊資料的邏輯也是需要消耗時間的,而具體耗時的長短,要取決于你配置的淘汰政策:

  • allkeys-lru:不管 key 是否設定了過期,淘汰最近最少通路的 key
  • volatile-lru:隻淘汰最近最少通路、并設定了過期時間的 key
  • allkeys-random:不管 key 是否設定了過期,随機淘汰 key
  • volatile-random:隻随機淘汰設定了過期時間的 key
  • allkeys-ttl:不管 key 是否設定了過期,淘汰即将過期的 key
  • noeviction:不淘汰任何 key,執行個體記憶體達到 maxmeory 後,再寫入新資料直接傳回錯誤
  • allkeys-lfu:不管 key 是否設定了過期,淘汰通路頻率最低的 key(4.0+版本支援)
  • volatile-lfu:隻淘汰通路頻率最低、并設定了過期時間 key(4.0+版本支援)

具體使用哪種政策,我們需要根據具體的業務場景來配置。

一般最常使用的是 allkeys-lru / volatile-lru 淘汰政策,它們的處理邏輯是,每次從執行個體中随機取出一批 key(這個數量可配置),然後淘汰一個最少通路的 key,之後把剩下的 key 暫存到一個池子中,繼續随機取一批 key,并與之前池子中的 key 比較,再淘汰一個最少通路的 key。以此往複,直到執行個體記憶體降到 maxmemory 之下。

需要注意的是,Redis 的淘汰資料的邏輯與删除過期 key 的一樣,也是在指令真正執行之前執行的,也就是說它也會增加我們操作 Redis 的延遲,而且,寫 OPS 越高,延遲也會越明顯。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

另外,如果此時你的 Redis 執行個體中還存儲了 bigkey,那麼在淘汰删除 bigkey 釋放記憶體時,也會耗時比較久。

看到了麼?bigkey 的危害到處都是,這也是前面我提醒你盡量不存儲 bigkey 的原因。

針對這種情況,如何解決呢?

我給你 4 個方面的優化建議:

  1. 避免存儲 bigkey,降低釋放記憶體的耗時
  2. 淘汰政策改為随機淘汰,随機淘汰比 LRU 要快很多(視業務情況調整)
  3. 拆分執行個體,把淘汰 key 的壓力分攤到多個執行個體上
  4. 如果使用的是 Redis 4.0 以上版本,開啟 layz-free 機制,把淘汰 key 釋放記憶體的操作放到背景線程中執行(配置 lazyfree-lazy-eviction = yes)

fork耗時嚴重

為了保證 Redis 資料的安全性,我們可能會開啟背景定時 RDB 和 AOF rewrite 功能。

但如果你發現,操作 Redis 延遲變大,都發生在 Redis 背景 RDB 和 AOF rewrite 期間,那你就需要排查,在這期間有可能導緻變慢的情況。

當 Redis 開啟了背景 RDB 和 AOF rewrite 後,在執行時,它們都需要主程序建立出一個子程序進行資料的持久化。

主程序建立子程序,會調用作業系統提供的 fork 函數。

而 fork 在執行過程中,主程序需要拷貝自己的記憶體頁表給子程序,如果這個執行個體很大,那麼這個拷貝的過程也會比較耗時。

而且這個 fork 過程會消耗大量的 CPU 資源,在完成 fork 之前,整個 Redis 執行個體會被阻塞住,無法處理任何用戶端請求。

如果此時你的 CPU 資源本來就很緊張,那麼 fork 的耗時會更長,甚至達到秒級,這會嚴重影響 Redis 的性能。

那如何确認确實是因為 fork 耗時導緻的 Redis 延遲變大呢?

你可以在 Redis 上執行 INFO 指令,檢視 latest_fork_usec 項,機關微秒。

# 上一次 fork 耗時,機關微秒
latest_fork_usec:59477
           

這個時間就是主程序在 fork 子程序期間,整個執行個體阻塞無法處理用戶端請求的時間。

如果你發現這個耗時很久,就要警惕起來了,這意味在這期間,你的整個 Redis 執行個體都處于不可用的狀态。

除了資料持久化會生成 RDB 之外,當主從節點第一次建立資料同步時,主節點也建立子程序生成 RDB,然後發給從節點進行一次全量同步,是以,這個過程也會對 Redis 産生性能影響。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

要想避免這種情況,你可以采取以下方案進行優化:

  1. 控制 Redis 執行個體的記憶體:盡量在 10G 以下,執行 fork 的耗時與執行個體大小有關,執行個體越大,耗時越久
  2. 合理配置資料持久化政策:在 slave 節點執行 RDB 備份,推薦在低峰期執行,而對于丢失資料不敏感的業務(例如把 Redis 當做純緩存使用),可以關閉 AOF 和 AOF rewrite
  3. Redis 執行個體不要部署在虛拟機上:fork 的耗時也與系統也有關,虛拟機比實體機耗時更久
  4. 降低主從庫全量同步的機率:适當調大 repl-backlog-size 參數,避免主從全量同步

開啟記憶體大頁

除了上面講到的子程序 RDB 和 AOF rewrite 期間,fork 耗時導緻的延時變大之外,這裡還有一個方面也會導緻性能問題,這就是作業系統是否開啟了記憶體大頁機制。

什麼是記憶體大頁?

我們都知道,應用程式向作業系統申請記憶體時,是按記憶體頁進行申請的,而正常的記憶體頁大小是 4KB。

Linux 核心從 2.6.38 開始,支援了記憶體大頁機制,該機制允許應用程式以 2MB 大小為機關,向作業系統申請記憶體。

應用程式每次向作業系統申請的記憶體機關變大了,但這也意味着申請記憶體的耗時變長。

這對 Redis 會有什麼影響呢?

當 Redis 在執行背景 RDB 和 AOF rewrite 時,采用 fork 子程序的方式來處理。但主程序 fork 子程序後,此時的主程序依舊是可以接收寫請求的,而進來的寫請求,會采用 Copy On Write(寫時複制)的方式操作記憶體資料。

也就是說,主程序一旦有資料需要修改,Redis 并不會直接修改現有記憶體中的資料,而是先将這塊記憶體資料拷貝出來,再修改這塊新記憶體的資料,這就是所謂的「寫時複制」。

寫時複制你也可以了解成,誰需要發生寫操作,誰就需要先拷貝,再修改。

這樣做的好處是,父程序有任何寫操作,并不會影響子程序的資料持久化(子程序隻持久化 fork 這一瞬間整個執行個體中的所有資料即可,不關心新的資料變更,因為子程序隻需要一份記憶體快照,然後持久化到磁盤上)。

但是請注意,主程序在拷貝記憶體資料時,這個階段就涉及到新記憶體的申請,如果此時作業系統開啟了記憶體大頁,那麼在此期間,用戶端即便隻修改 10B 的資料,Redis 在申請記憶體時也會以 2MB 為機關向作業系統申請,申請記憶體的耗時變長,進而導緻每個寫請求的延遲增加,影響到 Redis 性能。

同樣地,如果這個寫請求操作的是一個 bigkey,那主程序在拷貝這個 bigkey 記憶體塊時,一次申請的記憶體會更大,時間也會更久。可見,bigkey 在這裡又一次影響到了性能。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

那如何解決這個問題?

很簡單,你隻需要關閉記憶體大頁機制就可以了。

首先,你需要檢視 Redis 機器是否開啟了記憶體大頁:

$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
           

如果輸出選項是 always,就表示目前開啟了記憶體大頁機制,我們需要關掉它:

$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
           

其實,作業系統提供的記憶體大頁機制,其優勢是,可以在一定程式上降低應用程式申請記憶體的次數。

但是對于 Redis 這種對性能和延遲極其敏感的資料庫來說,我們希望 Redis 在每次申請記憶體時,耗時盡量短,是以我不建議你在 Redis 機器上開啟這個機制。

開啟AOF

前面我們分析了 RDB 和 AOF rewrite 對 Redis 性能的影響,主要關注點在 fork 上。

其實,關于資料持久化方面,還有影響 Redis 性能的因素,這次我們重點來看 AOF 資料持久化。

如果你的 AOF 配置不合理,還是有可能會導緻性能問題。

當 Redis 開啟 AOF 後,其工作原理如下:

  1. Redis 執行寫指令後,把這個指令寫入到 AOF 檔案記憶體中(write 系統調用)
  2. Redis 根據配置的 AOF 刷盤政策,把 AOF 記憶體資料刷到磁盤上(fsync 系統調用)

為了保證 AOF 檔案資料的安全性,Redis 提供了 3 種刷盤機制:

  1. appendfsync always:主線程每次執行寫操作後立即刷盤,此方案會占用比較大的磁盤 IO 資源,但資料安全性最高
  2. appendfsync no:主線程每次寫操作隻寫記憶體就傳回,記憶體資料什麼時候刷到磁盤,交由作業系統決定,此方案對性能影響最小,但資料安全性也最低,Redis 當機時丢失的資料取決于作業系統刷盤時機
  3. appendfsync everysec:主線程每次寫操作隻寫記憶體就傳回,然後由背景線程每隔 1 秒執行一次刷盤操作(觸發fsync系統調用),此方案對性能影響相對較小,但當 Redis 當機時會丢失 1 秒的資料

下面我們依次來分析,這幾個機制對性能的影響。

如果你的 AOF 配置為 appendfsync always,那麼 Redis 每處理一次寫操作,都會把這個指令寫入到磁盤中才傳回,整個過程都是在主線程執行的,這個過程必然會加重 Redis 寫負擔。

原因也很簡單,操作磁盤要比操作記憶體慢幾百倍,采用這個配置會嚴重拖慢 Redis 的性能,是以我不建議你把 AOF 刷盤方式配置為 always。

我們接着來看 appendfsync no 配置項。

在這種配置下,Redis 每次寫操作隻寫記憶體,什麼時候把記憶體中的資料刷到磁盤,交給作業系統決定,此方案對 Redis 的性能影響最小,但當 Redis 當機時,會丢失一部分資料,為了資料的安全性,一般我們也不采取這種配置。

如果你的 Redis 隻用作純緩存,對于資料丢失不敏感,采用配置 appendfsync no 也是可以的。

看到這裡,我猜你肯定和大多數人的想法一樣,選比較折中的方案 appendfsync everysec 就沒問題了吧?

這個方案優勢在于,Redis 主線程寫完記憶體後就傳回,具體的刷盤操作是放到背景線程中執行的,背景線程每隔 1 秒把記憶體中的資料刷到磁盤中。

這種方案既兼顧了性能,又盡可能地保證了資料安全,是不是覺得很完美?

但是,這裡我要給你潑一盆冷水了,采用這種方案你也要警惕一下,因為這種方案還是存在導緻 Redis 延遲變大的情況發生,甚至會阻塞整個 Redis。

這是為什麼?我把 AOF 最耗時的刷盤操作,放到背景線程中也會影響到 Redis 主線程?

你試想這樣一種情況:當 Redis 背景線程在執行 AOF 檔案刷盤時,如果此時磁盤的 IO 負載很高,那這個背景線程在執行刷盤操作(fsync系統調用)時就會被阻塞住。

此時的主線程依舊會接收寫請求,緊接着,主線程又需要把資料寫到檔案記憶體中(write 系統調用),但此時的背景子線程由于磁盤負載過高,導緻 fsync 發生阻塞,遲遲不能傳回,那主線程在執行 write 系統調用時,也會被阻塞住,直到背景線程 fsync 執行完成後,主線程執行 write 才能成功傳回。

看到了麼?在這個過程中,主線程依舊有阻塞的風險。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

是以,盡管你的 AOF 配置為 appendfsync everysec,也不能掉以輕心,要警惕磁盤壓力過大導緻的 Redis 有性能問題。

那什麼情況下會導緻磁盤 IO 負載過大?以及如何解決這個問題呢?

我總結了以下幾種情況,你可以參考進行問題排查:

  1. 子程序正在執行 AOF rewrite,這個過程會占用大量的磁盤 IO 資源
  2. 有其他應用程式在執行大量的寫檔案操作,也會占用磁盤 IO 資源

對于情況1,說白了就是,Redis 的 AOF 背景子線程刷盤操作,撞上了子程序 AOF rewrite!

這怎麼辦?難道要關閉 AOF rewrite 才行?

幸運的是,Redis 提供了一個配置項,當子程序在 AOF rewrite 期間,可以讓背景子線程不執行刷盤(不觸發 fsync 系統調用)操作。

這相當于在 AOF rewrite 期間,臨時把 appendfsync 設定為了 none,配置如下:

# AOF rewrite 期間,AOF 背景子線程不進行刷盤操作
# 相當于在這期間,臨時把 appendfsync 設定為了 none
no-appendfsync-on-rewrite yes
           

當然,開啟這個配置項,在 AOF rewrite 期間,如果執行個體發生當機,那麼此時會丢失更多的資料,性能和資料安全性,你需要權衡後進行選擇。

如果占用磁盤資源的是其他應用程式,那就比較簡單了,你需要定位到是哪個應用程式在大量寫磁盤,然後把這個應用程式遷移到其他機器上執行就好了,避免對 Redis 産生影響。

當然,如果你對 Redis 的性能和資料安全都有很高的要求,那麼我建議從硬體層面來優化,更換為 SSD 磁盤,提高磁盤的 IO 能力,保證 AOF 期間有充足的磁盤資源可以使用。

綁定CPU

很多時候,我們在部署服務時,為了提高服務性能,降低應用程式在多個 CPU 核心之間的上下文切換帶來的性能損耗,通常采用的方案是程序綁定 CPU 的方式提高性能。

但在部署 Redis 時,如果你需要綁定 CPU 來提高其性能,我建議你仔細斟酌後再做操作。

為什麼?

因為 Redis 在綁定 CPU 時,是有很多考究的,如果你不了解 Redis 的運作原理,随意綁定 CPU 不僅不會提高性能,甚至有可能會帶來相反的效果。

我們都知道,一般現代的伺服器會有多個 CPU,而每個 CPU 又包含多個實體核心,每個實體核心又分為多個邏輯核心,每個實體核下的邏輯核共用 L1/L2 Cache。

而 Redis Server 除了主線程服務用戶端請求之外,還會建立子程序、子線程。

其中子程序用于資料持久化,而子線程用于執行一些比較耗時操作,例如異步釋放 fd、異步 AOF 刷盤、異步 lazy-free 等等。

如果你把 Redis 程序隻綁定了一個 CPU 邏輯核心上,那麼當 Redis 在進行資料持久化時,fork 出的子程序會繼承父程序的 CPU 使用偏好。

而此時的子程序會消耗大量的 CPU 資源進行資料持久化(把執行個體資料全部掃描出來需要耗費CPU),這就會導緻子程序會與主程序發生 CPU 争搶,進而影響到主程序服務用戶端請求,通路延遲變大。

這就是 Redis 綁定 CPU 帶來的性能問題。

那如何解決這個問題呢?

如果你确實想要綁定 CPU,可以優化的方案是,不要讓 Redis 程序隻綁定在一個 CPU 邏輯核上,而是綁定在多個邏輯核心上,而且,綁定的多個邏輯核心最好是同一個實體核心,這樣它們還可以共用 L1/L2 Cache。

當然,即便我們把 Redis 綁定在多個邏輯核心上,也隻能在一定程度上緩解主線程、子程序、背景線程在 CPU 資源上的競争。

因為這些子程序、子線程還是會在這多個邏輯核心上進行切換,存在性能損耗。

如何再進一步優化?

可能你已經想到了,我們是否可以讓主線程、子程序、背景線程,分别綁定在固定的 CPU 核心上,不讓它們來回切換,這樣一來,他們各自使用的 CPU 資源互不影響。

其實,這個方案 Redis 官方已經想到了。

Redis 在 6.0 版本已經推出了這個功能,我們可以通過以下配置,對主線程、背景線程、背景 RDB 程序、AOF rewrite 程序,綁定固定的 CPU 邏輯核心:

# Redis Server 和 IO 線程綁定到 CPU核心 0,2,4,6
server_cpulist 0-7:2

# 背景子線程綁定到 CPU核心 1,3
bio_cpulist 1,3

# 背景 AOF rewrite 程序綁定到 CPU 核心 8,9,10,11
aof_rewrite_cpulist 8-11

# 背景 RDB 程序綁定到 CPU 核心 1,10,11
# bgsave_cpulist 1,10-1
           

如果你使用的正好是 Redis 6.0 版本,就可以通過以上配置,來進一步提高 Redis 性能。

這裡我需要提醒你的是,一般來說,Redis 的性能已經足夠優秀,除非你對 Redis 的性能有更加嚴苛的要求,否則不建議你綁定 CPU。

從上面的分析你也能看出,綁定 CPU 需要你對計算機體系結構有非常清晰的了解,否則謹慎操作。

我們繼續分析還有什麼場景會導緻 Redis 變慢。

使用Swap

如果你發現 Redis 突然變得非常慢,每次的操作耗時都達到了幾百毫秒甚至秒級,那此時你就需要檢查 Redis 是否使用到了 Swap,在這種情況下 Redis 基本上已經無法提供高性能的服務了。

什麼是 Swap?為什麼使用 Swap 會導緻 Redis 的性能下降?

如果你對作業系統有些了解,就會知道作業系統為了緩解記憶體不足對應用程式的影響,允許把一部分記憶體中的資料換到磁盤上,以達到應用程式對記憶體使用的緩沖,這些記憶體資料被換到磁盤上的區域,就是 Swap。

問題就在于,當記憶體中的資料被換到磁盤上後,Redis 再通路這些資料時,就需要從磁盤上讀取,通路磁盤的速度要比通路記憶體慢幾百倍!

尤其是針對 Redis 這種對性能要求極高、性能極其敏感的資料庫來說,這個操作延時是無法接受的。

此時,你需要檢查 Redis 機器的記憶體使用情況,确認是否存在使用了 Swap。

你可以通過以下方式來檢視 Redis 程序是否使用到了 Swap:

# 先找到 Redis 的程序 ID
$ ps -aux | grep redis-server

# 檢視 Redis Swap 使用情況
$ cat /proc/$pid/smaps | egrep '^(Swap|Size)'
           

輸出結果如下:

Size:               1256 kB
Swap:                  0 kB
Size:                  4 kB
Swap:                  0 kB
Size:                132 kB
Swap:                  0 kB
Size:              63488 kB
Swap:                  0 kB
Size:                132 kB
Swap:                  0 kB
Size:              65404 kB
Swap:                  0 kB
Size:            1921024 kB
Swap:                  0 kB
...
           

這個結果會列出 Redis 程序的記憶體使用情況。

每一行 Size 表示 Redis 所用的一塊記憶體大小,Size 下面的 Swap 就表示這塊 Size 大小的記憶體,有多少資料已經被換到磁盤上了,如果這兩個值相等,說明這塊記憶體的資料都已經完全被換到磁盤上了。

如果隻是少量資料被換到磁盤上,例如每一塊 Swap 占對應 Size 的比例很小,那影響并不是很大。如果是幾百兆甚至上 GB 的記憶體被換到了磁盤上,那麼你就需要警惕了,這種情況 Redis 的性能肯定會急劇下降。

此時的解決方案是:

  1. 增加機器的記憶體,讓 Redis 有足夠的記憶體可以使用
  2. 整理記憶體空間,釋放出足夠的記憶體供 Redis 使用,然後釋放 Redis 的 Swap,讓 Redis 重新使用記憶體

釋放 Redis 的 Swap 過程通常要重新開機執行個體,為了避免重新開機執行個體對業務的影響,一般會先進行主從切換,然後釋放舊主節點的 Swap,重新開機舊主節點執行個體,待從庫資料同步完成後,再進行主從切換即可。

可見,當 Redis 使用到 Swap 後,此時的 Redis 性能基本已達不到高性能的要求(你可以了解為武功被廢),是以你也需要提前預防這種情況。

預防的辦法就是,你需要對 Redis 機器的記憶體和 Swap 使用情況進行監控,在記憶體不足或使用到 Swap 時報警出來,及時處理。

碎片整理

Redis 的資料都存儲在記憶體中,當我們的應用程式頻繁修改 Redis 中的資料時,就有可能會導緻 Redis 産生記憶體碎片。

記憶體碎片會降低 Redis 的記憶體使用率,我們可以通過執行 INFO 指令,得到這個執行個體的記憶體碎片率:

# Memory
used_memory:5709194824
used_memory_human:5.32G
used_memory_rss:8264855552
used_memory_rss_human:7.70G
...
mem_fragmentation_ratio:1.45
           

這個記憶體碎片率是怎麼計算的?

很簡單,mem_fragmentation_ratio = used_memory_rss / used_memory。

其中 used_memory 表示 Redis 存儲資料的記憶體大小,而 used_memory_rss 表示作業系統實際配置設定給 Redis 程序的大小。

如果 mem_fragmentation_ratio > 1.5,說明記憶體碎片率已經超過了 50%,這時我們就需要采取一些措施來降低記憶體碎片了。

解決的方案一般如下:

  1. 如果你使用的是 Redis 4.0 以下版本,隻能通過重新開機執行個體來解決
  2. 如果你使用的是 Redis 4.0 版本,它正好提供了自動碎片整理的功能,可以通過配置開啟碎片自動整理

但是,開啟記憶體碎片整理,它也有可能會導緻 Redis 性能下降。

原因在于,Redis 的碎片整理工作是也在主線程中執行的,當其進行碎片整理時,必然會消耗 CPU 資源,産生更多的耗時,進而影響到用戶端的請求。

是以,當你需要開啟這個功能時,最好提前測試評估它對 Redis 的影響。

Redis 碎片整理的參數配置如下:

# 開啟自動記憶體碎片整理(總開關)
activedefrag yes

# 記憶體使用 100MB 以下,不進行碎片整理
active-defrag-ignore-bytes 100mb

# 記憶體碎片率超過 10%,開始碎片整理
active-defrag-threshold-lower 10
# 記憶體碎片率超過 100%,盡最大努力碎片整理
active-defrag-threshold-upper 100

# 記憶體碎片整理占用 CPU 資源最小百分比
active-defrag-cycle-min 1
# 記憶體碎片整理占用 CPU 資源最大百分比
active-defrag-cycle-max 25

# 碎片整理期間,對于 List/Set/Hash/ZSet 類型元素一次 Scan 的數量
active-defrag-max-scan-fields 1000
           

你需要結合 Redis 機器的負載情況,以及應用程式可接受的延遲範圍進行評估,合理調整碎片整理的參數,盡可能降低碎片整理期間對 Redis 的影響。

網絡帶寬過載

如果以上産生性能問題的場景,你都規避掉了,而且 Redis 也穩定運作了很長時間,但在某個時間點之後開始,操作 Redis 突然開始變慢了,而且一直持續下去,這種情況又是什麼原因導緻?

此時你需要排查一下 Redis 機器的網絡帶寬是否過載,是否存在某個執行個體把整個機器的網路帶寬占滿的情況。

網絡帶寬過載的情況下,伺服器在 TCP 層和網絡層就會出現資料包發送延遲、丢包等情況。

Redis 的高性能,除了操作記憶體之外,就在于網絡 IO 了,如果網絡 IO 存在瓶頸,那麼也會嚴重影響 Redis 的性能。

如果确實出現這種情況,你需要及時确認占滿網絡帶寬 Redis 執行個體,如果屬于正常的業務通路,那就需要及時擴容或遷移執行個體了,避免因為這個執行個體流量過大,影響這個機器的其他執行個體。

運維層面,你需要對 Redis 機器的各項名額增加監控,包括網絡流量,在網絡流量達到一定門檻值時提前報警,及時确認和擴容。

其他原因

好了,以上這些方面就是如何排查 Redis 延遲問題的思路和路徑。

除了以上這些,還有一些比較小的點,你也需要注意一下:

1) 頻繁短連接配接

你的業務應用,應該使用長連接配接操作 Redis,避免頻繁的短連接配接。

頻繁的短連接配接會導緻 Redis 大量時間耗費在連接配接的建立和釋放上,TCP 的三次握手和四次揮手同樣也會增加通路延遲。

2) 運維監控

前面我也提到了,要想提前預知 Redis 變慢的情況發生,必不可少的就是做好完善的監控。

監控其實就是對采集 Redis 的各項運作時名額,通常的做法是監控程式定時采集 Redis 的 INFO 資訊,然後根據 INFO 資訊中的狀态資料做資料展示和報警。

這裡我需要提醒你的是,在寫一些監控腳本,或使用開源的監控元件時,也不能掉以輕心。

在寫監控腳本通路 Redis 時,盡量采用長連接配接的方式采集狀态資訊,避免頻繁短連接配接。同時,你還要注意控制通路 Redis 的頻率,避免影響到業務請求。

在使用一些開源的監控元件時,最好了解一下這些元件的實作原理,以及正确配置這些元件,防止出現監控元件發生 Bug,導緻短時大量操作 Redis,影響 Redis 性能的情況發生。

我們當時就發生過,DBA 在使用一些開源元件時,因為配置和使用問題,導緻監控程式頻繁地與 Redis 建立和斷開連接配接,導緻 Redis 響應變慢。

3)其它程式争搶資源

最後需要提醒你的是,你的 Redis 機器最好專項專用,隻用來部署 Redis 執行個體,不要部署其他應用程式,盡量給 Redis 提供一個相對「安靜」的環境,避免其它程式占用 CPU、記憶體、磁盤資源,導緻配置設定給 Redis 的資源不足而受到影響。

總結

好了,以上就是我總結的在使用 Redis 過程中,常見的可能導緻延遲、甚至阻塞的問題場景,以及如何快速定位和分析這些問題,并且針對性地提供了解決方案。

這裡我也彙總成了思維導圖,友善你在排查 Redis 性能問題時,快速地去分析和定位。

Redis為什麼變慢了?Redis真的變慢了嗎?使用複雜度過高的指令操作bigkey集中過期執行個體記憶體達到上限fork耗時嚴重開啟記憶體大頁開啟AOF綁定CPU使用Swap碎片整理網絡帶寬過載其他原因總結後記

這裡再簡單總結一下,Redis 的性能問題,既涉及到了業務開發人員的使用方面,也涉及到了 DBA 的運維方面。

作為業務開發人員,我們需要了解 Redis 的基本原理,例如各個指令執行的時間複雜度、資料過期政策、資料淘汰政策等,進而更合理地使用 Redis 指令,并且結合業務場景進行優化。

作為 DBA 和運維人員,需要了解 Redis 運作機制,例如資料持久化、記憶體碎片整理、程序綁核配置。除此之外,還需要了解作業系統相關知識,例如寫時複制、記憶體大頁、Swap 機制等等。

同時,DBA 在部署 Redis 時,需要提前對進行容量規劃,預留足夠的機器資源,還要對 Redis 機器和執行個體做好完善的監控,這樣才能盡可能地保證 Redis 的穩定運作。

後記

如果你能耐心地看到這裡,想必你肯定已經對 Redis 的性能調優有了很大的收獲。

你應該也發現了,Redis 的性能問題,涉及到的知識點非常廣,幾乎涵蓋了 CPU、記憶體、網絡、甚至磁盤的方方面面,同時,你還需要了解計算機的體系結構,以及作業系統的各種機制。

從資源使用角度來看,包含的知識點如下:

  • CPU 相關:使用複雜度過高指令、資料的持久化,都與耗費過多的 CPU 資源有關
  • 記憶體相關:bigkey 記憶體的申請和釋放、資料過期、資料淘汰、碎片整理、記憶體大頁、記憶體寫時複制都與記憶體息息相關
  • 磁盤相關:資料持久化、AOF 刷盤政策,也會受到磁盤的影響
  • 網絡相關:短連接配接、執行個體流量過載、網絡流量過載,也會降低 Redis 性能
  • 計算機系統:CPU 結構、記憶體配置設定,都屬于最基礎的計算機系統知識
  • 作業系統:寫時複制、記憶體大頁、Swap、CPU 綁定,都屬于作業系統層面的知識

沒想到吧?Redis 為了把性能做到極緻,涉及到了這麼多項優化。

如果這篇文章内容,你能吸收 90% 以上,說明你對 Redis 原理、計算機基礎、作業系統都已經有了較為深刻的了解。

如果你能吸收 50% 左右,那你可以好好梳理一下,哪些方面是自己的知識盲區,這樣可以針對性地去學習。

如果你吸收的隻在 30% 以下,那麼你可以先從 Redis 的基本原理出發,先了解 Redis 的各種機制,進而思考 Redis 為了提高性能,為什麼使用這些機制?這些機制又是利用了計算機和作業系統的哪些特性去做的?進而一步步地去擴充你的知識體系,這是一個非常高效的學習路徑。

由于篇幅限制,關于 Redis 的很多細節無法全部展開,其實,這篇文章提到的每一個導緻 Redis 性能問題的場景,如果展開來講,都可以寫出一篇文章出來。

例如,關于 Redis 程序綁定 CPU,以及作業系統使用 Swap,其實這些還涉及到了非一緻性記憶體通路 NUMA 架構的影響,其中也有很多細節沒有展開來講。

這篇文章主要站在一個宏觀層面進行性能分析,如果你想了解更多細節,歡迎關注我的公衆号「水滴與銀彈」,我會在後續的文章中,帶你深度剖析 Redis 的運作原理,以及産生性能問題的更多細節。

看到這裡,如果你覺得這篇文章對你有所幫助的話,麻煩幫忙點贊、在看、轉發一下,你的支援會激勵我輸出更高品質的文章,非常感謝!

Java後端交流群已成立
公衆号營運至今,離不開小夥伴們的支援。為了給小夥伴們提供一個互相交流的平台,特地開通了官方交流群。掃描下方二維碼備注 進群 或者關注公衆号 Java後端 後擷取進群通道。




推薦閱讀 1. GitHub 上有哪些好玩的項目2. 畢業設計别再做 XX 管理系統了!3. 牛逼!Docker從入門到上瘾4. 連夜撸了一個簡易聊天室5. 推薦一款 Java 對象映射神器