分類
發燒車訊

面試官:換人!他連 TCP 這幾個參數都不懂

每日一句英語學習,每天進步一點點:

前言

TCP 性能的提升不僅考察 TCP 的理論知識,還考察了對於操作系統提供的內核參數的理解與應用。

TCP 協議是由操作系統實現,所以操作系統提供了不少調節 TCP 的參數。

Linux TCP 參數

如何正確有效的使用這些參數,來提高 TCP 性能是一個不那麼簡單事情。我們需要針對 TCP 每個階段的問題來對症下藥,而不是病急亂投醫。

接下來,將以三個角度來闡述提升 TCP 的策略,分別是:

  • TCP 三次握手的性能提升;
  • TCP 四次揮手的性能提升;
  • TCP 數據傳輸的性能提升;

本節提綱

正文

01 TCP 三次握手的性能提升

TCP 是面向連接的、可靠的、雙向傳輸的傳輸層通信協議,所以在傳輸數據之前需要經過三次握手才能建立連接。

三次握手與數據傳輸

那麼,三次握手的過程在一個 HTTP 請求的平均時間佔比 10% 以上,在網絡狀態不佳、高併發或者遭遇 SYN 攻擊等場景中,如果不能有效正確的調節三次握手中的參數,就會對性能產生很多的影響。

如何正確有效的使用這些參數,來提高 TCP 三次握手的性能,這就需要理解「三次握手的狀態變遷」,這樣當出現問題時,先用 netstat 命令查看是哪個握手階段出現了問題,再來對症下藥,而不是病急亂投醫。

TCP 三次握手的狀態變遷

客戶端和服務端都可以針對三次握手優化性能。主動發起連接的客戶端優化相對簡單些,而服務端需要監聽端口,屬於被動連接方,其間保持許多的中間狀態,優化方法相對複雜一些。

所以,客戶端(主動發起連接方)和服務端(被動連接方)優化的方式是不同的,接下來分別針對客戶端和服務端優化。

客戶端優化

三次握手建立連接的首要目的是「同步序列號」。

只有同步了序列號才有可靠傳輸,TCP 許多特性都依賴於序列號實現,比如流量控制、丟包重傳等,這也是三次握手中的報文稱為 SYN 的原因,SYN 的全稱就叫 Synchronize Sequence Numbers(同步序列號)。

TCP 頭部

SYN_SENT 狀態的優化

客戶端作為主動發起連接方,首先它將發送 SYN 包,於是客戶端的連接就會處於 SYN_SENT 狀態。

客戶端在等待服務端回復的 ACK 報文,正常情況下,服務器會在幾毫秒內返回 SYN+ACK ,但如果客戶端長時間沒有收到 SYN+ACK 報文,則會重發 SYN 包,重發的次數由 tcp_syn_retries 參數控制,默認是 5 次:

通常,第一次超時重傳是在 1 秒后,第二次超時重傳是在 2 秒,第三次超時重傳是在 4 秒后,第四次超時重傳是在 8 秒后,第五次是在超時重傳 16 秒后。沒錯,每次超時的時間是上一次的 2 倍

當第五次超時重傳后,會繼續等待 32 秒,如果仍然服務端沒有回應 ACK,客戶端就會終止三次握手。

所以,總耗時是 1+2+4+8+16+32=63 秒,大約 1 分鐘左右。

SYN 超時重傳

你可以根據網絡的穩定性和目標服務器的繁忙程度修改 SYN 的重傳次數,調整客戶端的三次握手時間上限。比如內網中通訊時,就可以適當調低重試次數,儘快把錯誤暴露給應用程序。

服務端優化

當服務端收到 SYN 包后,服務端會立馬回復 SYN+ACK 包,表明確認收到了客戶端的序列號,同時也把自己的序列號發給對方。

此時,服務端出現了新連接,狀態是 SYN_RCV。在這個狀態下,Linux 內核就會建立一個「半連接隊列」來維護「未完成」的握手信息,當半連接隊列溢出后,服務端就無法再建立新的連接。

半連接隊列與全連接隊列

SYN 攻擊,攻擊的是就是這個半連接隊列。

如何查看由於 SYN 半連接隊列已滿,而被丟棄連接的情況?

我們可以通過該 netstat -s 命令給出的統計結果中, 可以得到由於半連接隊列已滿,引發的失敗次數:

上面輸出的數值是累計值,表示共有多少個 TCP 連接因為半連接隊列溢出而被丟棄。隔幾秒執行幾次,如果有上升的趨勢,說明當前存在半連接隊列溢出的現象

如何調整 SYN 半連接隊列大小?

要想增大半連接隊列,不能只單純增大 tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大 accept 隊列。否則,只單純增大 tcp_max_syn_backlog 是無效的。

增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 內核參數:

增大 backlog 的方式,每個 Web 服務都不同,比如 Nginx 增大 backlog 的方法如下:

最後,改變了如上這些參數后,要重啟 Nginx 服務,因為 SYN 半連接隊列和 accept 隊列都是在 listen() 初始化的。

如果 SYN 半連接隊列已滿,只能丟棄連接嗎?

並不是這樣,開啟 syncookies 功能就可以在不使用 SYN 半連接隊列的情況下成功建立連接

syncookies 的工作原理:服務器根據當前狀態計算出一個值,放在己方發出的 SYN+ACK 報文中發出,當客戶端返回 ACK 報文時,取出該值驗證,如果合法,就認為連接建立成功,如下圖所示。

開啟 syncookies 功能

syncookies 參數主要有以下三個值:

  • 0 值,表示關閉該功能;
  • 1 值,表示僅當 SYN 半連接隊列放不下時,再啟用它;
  • 2 值,表示無條件開啟功能;

那麼在應對 SYN 攻擊時,只需要設置為 1 即可:

SYN_RCV 狀態的優化

當客戶端接收到服務器發來的 SYN+ACK 報文後,就會回復 ACK 給服務器,同時客戶端連接狀態從 SYN_SENT 轉換為 ESTABLISHED,表示連接建立成功。

服務器端連接成功建立的時間還要再往後,等到服務端收到客戶端的 ACK 后,服務端的連接狀態才變為 ESTABLISHED。

如果服務器沒有收到 ACK,就會重發 SYN+ACK 報文,同時一直處於 SYN_RCV 狀態。

當網絡繁忙、不穩定時,報文丟失就會變嚴重,此時應該調大重發次數。反之則可以調小重發次數。修改重發次數的方法是,調整 tcp_synack_retries 參數

tcp_synack_retries 的默認重試次數是 5 次,與客戶端重傳 SYN 類似,它的重傳會經歷 1、2、4、8、16 秒,最後一次重傳後會繼續等待 32 秒,如果服務端仍然沒有收到 ACK,才會關閉連接,故共需要等待 63 秒。

服務器收到 ACK 后連接建立成功,此時,內核會把連接從半連接隊列移除,然後創建新的完全的連接,並將其添加到 accept 隊列,等待進程調用 accept 函數時把連接取出來。

如果進程不能及時地調用 accept 函數,就會造成 accept 隊列(也稱全連接隊列)溢出,最終導致建立好的 TCP 連接被丟棄。

accept 隊列溢出

accept 隊列已滿,只能丟棄連接嗎?

丟棄連接只是 Linux 的默認行為,我們還可以選擇向客戶端發送 RST 複位報文,告訴客戶端連接已經建立失敗。打開這一功能需要將 tcp_abort_on_overflow 參數設置為 1。

tcp_abort_on_overflow 共有兩個值分別是 0 和 1,其分別表示:

  • 0 :如果 accept 隊列滿了,那麼 server 扔掉 client 發過來的 ack ;
  • 1 :如果 accept 隊列滿了,server 發送一個 RST 包給 client,表示廢掉這個握手過程和這個連接;

如果要想知道客戶端連接不上服務端,是不是服務端 TCP 全連接隊列滿的原因,那麼可以把 tcp_abort_on_overflow 設置為 1,這時如果在客戶端異常中可以看到很多 connection reset by peer 的錯誤,那麼就可以證明是由於服務端 TCP 全連接隊列溢出的問題。

通常情況下,應當把 tcp_abort_on_overflow 設置為 0,因為這樣更有利於應對突發流量。

舉個例子,當 accept 隊列滿導致服務器丟掉了 ACK,與此同時,客戶端的連接狀態卻是 ESTABLISHED,客戶端進程就在建立好的連接上發送請求。只要服務器沒有為請求回復 ACK,客戶端的請求就會被多次「重發」。如果服務器上的進程只是短暫的繁忙造成 accept 隊列滿,那麼當 accept 隊列有空位時,再次接收到的請求報文由於含有 ACK,仍然會觸發服務器端成功建立連接。

tcp_abort_on_overflow 為 0 可以應對突發流量

所以,tcp_abort_on_overflow 設為 0 可以提高連接建立的成功率,只有你非常肯定 TCP 全連接隊列會長期溢出時,才能設置為 1 以儘快通知客戶端。

如何調整 accept 隊列的長度呢?

accept 隊列的長度取決於 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog),其中:

  • somaxconn 是 Linux 內核的參數,默認值是 128,可以通過 net.core.somaxconn 來設置其值;
  • backlog 是 listen(int sockfd, int backlog) 函數中的 backlog 大小;

Tomcat、Nginx、Apache 常見的 Web 服務的 backlog 默認值都是 511。

如何查看服務端進程 accept 隊列的長度?

可以通過 ss -ltn 命令查看:

  • Recv-Q:當前 accept 隊列的大小,也就是當前已完成三次握手並等待服務端 accept() 的 TCP 連接;
  • Send-Q:accept 隊列最大長度,上面的輸出結果說明監聽 8088 端口的 TCP 服務,accept 隊列的最大長度為 128;

如何查看由於 accept 連接隊列已滿,而被丟棄的連接?

當超過了 accept 連接隊列,服務端則會丟掉後續進來的 TCP 連接,丟掉的 TCP 連接的個數會被統計起來,我們可以使用 netstat -s 命令來查看:

上面看到的 41150 times ,表示 accept 隊列溢出的次數,注意這個是累計值。可以隔幾秒鐘執行下,如果這個数字一直在增加的話,說明 accept 連接隊列偶爾滿了。

如果持續不斷地有連接因為 accept 隊列溢出被丟棄,就應該調大 backlog 以及 somaxconn 參數。

如何繞過三次握手?

以上我們只是在對三次握手的過程進行優化,接下來我們看看如何繞過三次握手發送數據。

三次握手建立連接造成的後果就是,HTTP 請求必須在一個 RTT(從客戶端到服務器一個往返的時間)后才能發送。

常規 HTTP 請求

在 Linux 3.7 內核版本之後,提供了 TCP Fast Open 功能,這個功能可以減少 TCP 連接建立的時延。

接下來說說,TCP Fast Open 功能的工作方式。

開啟 TCP Fast Open 功能

在客戶端首次建立連接時的過程:

  1. 客戶端發送 SYN 報文,該報文包含 Fast Open 選項,且該選項的 Cookie 為空,這表明客戶端請求 Fast Open Cookie;
  2. 支持 TCP Fast Open 的服務器生成 Cookie,並將其置於 SYN-ACK 數據包中的 Fast Open 選項以發回客戶端;
  3. 客戶端收到 SYN-ACK 后,本地緩存 Fast Open 選項中的 Cookie。

所以,第一次發起 HTTP GET 請求的時候,還是需要正常的三次握手流程。

之後,如果客戶端再次向服務器建立連接時的過程:

  1. 客戶端發送 SYN 報文,該報文包含「數據」(對於非 TFO 的普通 TCP 握手過程,SYN 報文中不包含「數據」)以及此前記錄的 Cookie;
  2. 支持 TCP Fast Open 的服務器會對收到 Cookie 進行校驗:如果 Cookie 有效,服務器將在 SYN-ACK 報文中對 SYN 和「數據」進行確認,服務器隨後將「數據」遞送至相應的應用程序;如果 Cookie 無效,服務器將丟棄 SYN 報文中包含的「數據」,且其隨後發出的 SYN-ACK 報文將只確認 SYN 的對應序列號;
  3. 如果服務器接受了 SYN 報文中的「數據」,服務器可在握手完成之前發送「數據」,這就減少了握手帶來的 1 個 RTT 的時間消耗
  4. 客戶端將發送 ACK 確認服務器發回的 SYN 以及「數據」,但如果客戶端在初始的 SYN 報文中發送的「數據」沒有被確認,則客戶端將重新發送「數據」;
  5. 此後的 TCP 連接的數據傳輸過程和非 TFO 的正常情況一致。

所以,之後發起 HTTP GET 請求的時候,可以繞過三次握手,這就減少了握手帶來的 1 個 RTT 的時間消耗。

開啟了 TFO 功能,cookie 的值是存放到 TCP option 字段里的:

TCP option 字段 – TFO

注:客戶端在請求並存儲了 Fast Open Cookie 之後,可以不斷重複 TCP Fast Open 直至服務器認為 Cookie 無效(通常為過期)。

Linux 下怎麼打開 TCP Fast Open 功能呢?

在 Linux 系統中,可以通過設置 tcp_fastopn 內核參數,來打開 Fast Open 功能

tcp_fastopn 各個值的意義:

  • 0 關閉
  • 1 作為客戶端使用 Fast Open 功能
  • 2 作為服務端使用 Fast Open 功能
  • 3 無論作為客戶端還是服務器,都可以使用 Fast Open 功能

TCP Fast Open 功能需要客戶端和服務端同時支持,才有效果。

小結

本小結主要介紹了關於優化 TCP 三次握手的幾個 TCP 參數。

三次握手優化策略

客戶端的優化

當客戶端發起 SYN 包時,可以通過 tcp_syn_retries 控制其重傳的次數。

服務端的優化

當服務端 SYN 半連接隊列溢出后,會導致後續連接被丟棄,可以通過 netstat -s 觀察半連接隊列溢出的情況,如果 SYN 半連接隊列溢出情況比較嚴重,可以通過 tcp_max_syn_backlog、somaxconn、backlog 參數來調整 SYN 半連接隊列的大小。

服務端回復 SYN+ACK 的重傳次數由 tcp_synack_retries 參數控制。如果遭受 SYN 攻擊,應把 tcp_syncookies 參數設置為 1,表示僅在 SYN 隊列滿后開啟 syncookie 功能,可以保證正常的連接成功建立。

服務端收到客戶端返回的 ACK,會把連接移入 accpet 隊列,等待進行調用 accpet() 函數取出連接。

可以通過 ss -lnt 查看服務端進程的 accept 隊列長度,如果 accept 隊列溢出,系統默認丟棄 ACK,如果可以把 tcp_abort_on_overflow 設置為 1 ,表示用 RST 通知客戶端連接建立失敗。

如果 accpet 隊列溢出嚴重,可以通過 listen 函數的 backlog 參數和 somaxconn 系統參數提高隊列大小,accept 隊列長度取決於 min(backlog, somaxconn)。

繞過三次握手

TCP Fast Open 功能可以繞過三次握手,使得 HTTP 請求減少了 1 個 RTT 的時間,Linux 下可以通過 tcp_fastopen 開啟該功能,同時必須保證服務端和客戶端同時支持。

02 TCP 四次揮手的性能提升

接下來,我們一起看看針對 TCP 四次揮手關不連接時,如何優化性能。

在開始之前,我們得先了解四次揮手狀態變遷的過程。

客戶端和服務端雙方都可以主動斷開連接,通常先關閉連接的一方稱為主動方,后關閉連接的一方稱為被動方。

客戶端主動關閉

可以看到,四次揮手過程只涉及了兩種報文,分別是 FIN 和 ACK

  • FIN 就是結束連接的意思,誰發出 FIN 報文,就表示它將不會再發送任何數據,關閉這一方向上的傳輸通道;
  • ACK 就是確認的意思,用來通知對方:你方的發送通道已經關閉;

四次揮手的過程:

  • 當主動方關閉連接時,會發送 FIN 報文,此時發送方的 TCP 連接將從 ESTABLISHED 變成 FIN_WAIT1。
  • 當被動方收到 FIN 報文後,內核會自動回復 ACK 報文,連接狀態將從 ESTABLISHED 變成 CLOSE_WAIT,表示被動方在等待進程調用 close 函數關閉連接。
  • 當主動方收到這個 ACK 后,連接狀態由 FIN_WAIT1 變為 FIN_WAIT2,也就是表示主動方的發送通道就關閉了
  • 當被動方進入 CLOSE_WAIT 時,被動方還會繼續處理數據,等到進程的 read 函數返回 0 后,應用程序就會調用 close 函數,進而觸發內核發送 FIN 報文,此時被動方的連接狀態變為 LAST_ACK。
  • 當主動方收到這個 FIN 報文後,內核會回復 ACK 報文給被動方,同時主動方的連接狀態由 FIN_WAIT2 變為 TIME_WAIT,在 Linux 系統下大約等待 1 分鐘后,TIME_WAIT 狀態的連接才會徹底關閉
  • 當被動方收到最後的 ACK 報文後,被動方的連接就會關閉

你可以看到,每個方向都需要一個 FIN 和一個 ACK,因此通常被稱為四次揮手

這裏一點需要注意是:主動關閉連接的,才有 TIME_WAIT 狀態。

主動關閉方和被動關閉方優化的思路也不同,接下來分別說說如何優化他們。

主動方的優化

關閉的連接的方式通常有兩種,分別是 RST 報文關閉和 FIN 報文關閉。

如果進程異常退出了,內核就會發送 RST 報文來關閉,它可以不走四次揮手流程,是一個暴力關閉連接的方式。

安全關閉連接的方式必須通過四次揮手,它由進程調用 closeshutdown 函數發起 FIN 報文(shutdown 參數須傳入 SHUT_WR 或者 SHUT_RDWR 才會發送 FIN)。

調用 close 函數 和 shutdown 函數有什麼區別?

調用了 close 函數意味着完全斷開連接,完全斷開不僅指無法傳輸數據,而且也不能發送數據。 此時,調用了 close 函數的一方的連接叫做「孤兒連接」,如果你用 netstat -p 命令,會發現連接對應的進程名為空。

使用 close 函數關閉連接是不優雅的。於是,就出現了一種優雅關閉連接的 shutdown 函數,它可以控制只關閉一個方向的連接

第二個參數決定斷開連接的方式,主要有以下三種方式:

  • SHUT_RD(0):關閉連接的「讀」這個方向,如果接收緩衝區有已接收的數據,則將會被丟棄,並且後續再收到新的數據,會對數據進行 ACK,然後悄悄地丟棄。也就是說,對端還是會接收到 ACK,在這種情況下根本不知道數據已經被丟棄了。
  • SHUT_WR(1):關閉連接的「寫」這個方向,這就是常被稱為「半關閉」的連接。如果發送緩衝區還有未發送的數據,將被立即發送出去,併發送一個 FIN 報文給對端。
  • SHUT_RDWR(2):相當於 SHUT_RD 和 SHUT_WR 操作各一次,關閉套接字的讀和寫兩個方向

close 和 shutdown 函數都可以關閉連接,但這兩種方式關閉的連接,不只功能上有差異,控制它們的 Linux 參數也不相同。

FIN_WAIT1 狀態的優化

主動方發送 FIN 報文後,連接就處於 FIN_WAIT1 狀態,正常情況下,如果能及時收到被動方的 ACK,則會很快變為 FIN_WAIT2 狀態。

但是當遲遲收不到對方返回的 ACK 時,連接就會一直處於 FIN_WAIT1 狀態。此時,內核會定時重發 FIN 報文,其中重發次數由 tcp_orphan_retries 參數控制(注意,orphan 雖然是孤兒的意思,該參數卻不只對孤兒連接有效,事實上,它對所有 FIN_WAIT1 狀態下的連接都有效),默認值是 0。

你可能會好奇,這 0 表示幾次?實際上當為 0 時,特指 8 次,從下面的內核源碼可知:

如果 FIN_WAIT1 狀態連接很多,我們就需要考慮降低 tcp_orphan_retries 的值,當重傳次數超過 tcp_orphan_retries 時,連接就會直接關閉掉。

對於普遍正常情況時,調低 tcp_orphan_retries 就已經可以了。如果遇到惡意攻擊,FIN 報文根本無法發送出去,這由 TCP 兩個特性導致的:

  • 首先,TCP 必須保證報文是有序發送的,FIN 報文也不例外,當發送緩衝區還有數據沒有發送時,FIN 報文也不能提前發送。
  • 其次,TCP 有流量控制功能,當接收方接收窗口為 0 時,發送方就不能再發送數據。所以,當攻擊者下載大文件時,就可以通過接收窗口設為 0 ,這就會使得 FIN 報文都無法發送出去,那麼連接會一直處於 FIN_WAIT1 狀態。

解決這種問題的方法,是調整 tcp_max_orphans 參數,它定義了「孤兒連接」的最大數量

當進程調用了 close 函數關閉連接,此時連接就會是「孤兒連接」,因為它無法在發送和接收數據。Linux 系統為了防止孤兒連接過多,導致系統資源長時間被佔用,就提供了 tcp_max_orphans 參數。如果孤兒連接數量大於它,新增的孤兒連接將不再走四次揮手,而是直接發送 RST 複位報文強制關閉。

FIN_WAIT2 狀態的優化

當主動方收到 ACK 報文後,會處於 FIN_WAIT2 狀態,就表示主動方的發送通道已經關閉,接下來將等待對方發送 FIN 報文,關閉對方的發送通道。

這時,如果連接是用 shutdown 函數關閉的,連接可以一直處於 FIN_WAIT2 狀態,因為它可能還可以發送或接收數據。但對於 close 函數關閉的孤兒連接,由於無法在發送和接收數據,所以這個狀態不可以持續太久,而 tcp_fin_timeout 控制了這個狀態下連接的持續時長,默認值是 60 秒:

它意味着對於孤兒連接(調用 close 關閉的連接),如果在 60 秒后還沒有收到 FIN 報文,連接就會直接關閉。

這個 60 秒不是隨便決定的,它與 TIME_WAIT 狀態持續的時間是相同的,後面我們在來說說為什麼是 60 秒。

TIME_WAIT 狀態的優化

TIME_WAIT 是主動方四次揮手的最後一個狀態,也是最常遇見的狀態。

當收到被動方發來的 FIN 報文後,主動方會立刻回復 ACK,表示確認對方的發送通道已經關閉,接着就處於 TIME_WAIT 狀態。在 Linux 系統,TIME_WAIT 狀態會持續 60 秒后才會進入關閉狀態。

TIME_WAIT 狀態的連接,在主動方看來確實快已經關閉了。然後,被動方沒有收到 ACK 報文前,還是處於 LAST_ACK 狀態。如果這個 ACK 報文沒有到達被動方,被動方就會重發 FIN 報文。重發次數仍然由前面介紹過的 tcp_orphan_retries 參數控制。

TIME-WAIT 的狀態尤其重要,主要是兩個原因:

  • 防止具有相同「四元組」的「舊」數據包被收到;
  • 保證「被動關閉連接」的一方能被正確的關閉,即保證最後的 ACK 能讓被動關閉方接收,從而幫助其正常關閉;

原因一:防止舊連接的數據包

TIME-WAIT 的一個作用是防止收到歷史數據,從而導致數據錯亂的問題。

假設 TIME-WAIT 沒有等待時間或時間過短,被延遲的數據包抵達後會發生什麼呢?

接收到歷史數據的異常

  • 如上圖黃色框框服務端在關閉連接之前發送的 SEQ = 301 報文,被網絡延遲了。
  • 這時有相同端口的 TCP 連接被複用后,被延遲的 SEQ = 301 抵達了客戶端,那麼客戶端是有可能正常接收這個過期的報文,這就會產生數據錯亂等嚴重的問題。

所以,TCP 就設計出了這麼一個機制,經過 2MSL 這個時間,足以讓兩個方向上的數據包都被丟棄,使得原來連接的數據包在網絡中都自然消失,再出現的數據包一定都是新建立連接所產生的。

原因二:保證連接正確關閉

TIME-WAIT 的另外一個作用是等待足夠的時間以確保最後的 ACK 能讓被動關閉方接收,從而幫助其正常關閉。

假設 TIME-WAIT 沒有等待時間或時間過短,斷開連接會造成什麼問題呢?

沒有確保正常斷開的異常

  • 如上圖紅色框框客戶端四次揮手的最後一個 ACK 報文如果在網絡中被丟失了,此時如果客戶端 TIME-WAIT 過短或沒有,則就直接進入了 CLOSE 狀態了,那麼服務端則會一直處在 LASE-ACK 狀態。
  • 當客戶端發起建立連接的 SYN 請求報文後,服務端會發送 RST 報文給客戶端,連接建立的過程就會被終止。

我們再回過頭來看看,為什麼 TIME_WAIT 狀態要保持 60 秒呢?這與孤兒連接 FIN_WAIT2 狀態默認保留 60 秒的原理是一樣的,因為這兩個狀態都需要保持 2MSL 時長。MSL 全稱是 Maximum Segment Lifetime,它定義了一個報文在網絡中的最長生存時間(報文每經過一次路由器的轉發,IP 頭部的 TTL 字段就會減 1,減到 0 時報文就被丟棄,這就限制了報文的最長存活時間)。

為什麼是 2 MSL 的時長呢?這其實是相當於至少允許報文丟失一次。比如,若 ACK 在一個 MSL 內丟失,這樣被動方重發的 FIN 會在第 2 個 MSL 內到達,TIME_WAIT 狀態的連接可以應對。

為什麼不是 4 或者 8 MSL 的時長呢?你可以想象一個丟包率達到百分之一的糟糕網絡,連續兩次丟包的概率只有萬分之一,這個概率實在是太小了,忽略它比解決它更具性價比。

因此,TIME_WAIT 和 FIN_WAIT2 狀態的最大時長都是 2 MSL,由於在 Linux 系統中,MSL 的值固定為 30 秒,所以它們都是 60 秒。

雖然 TIME_WAIT 狀態有存在的必要,但它畢竟會消耗系統資源。如果發起連接一方的 TIME_WAIT 狀態過多,佔滿了所有端口資源,則會導致無法創建新連接。

  • 客戶端受端口資源限制:如果客戶端 TIME_WAIT 過多,就會導致端口資源被佔用,因為端口就65536個,被佔滿就會導致無法創建新的連接;
  • 服務端受系統資源限制:由於一個 四元組表示TCP連接,理論上服務端可以建立很多連接,服務端確實只監聽一個端口 但是會把連接扔給處理線程,所以理論上監聽的端口可以繼續監聽。但是線程池處理不了那麼多一直不斷的連接了。所以當服務端出現大量 TIME_WAIT 時,系統資源被佔滿時,會導致處理不過來新的連接;

另外,Linux 提供了 tcp_max_tw_buckets 參數,當 TIME_WAIT 的連接數量超過該參數時,新關閉的連接就不再經歷 TIME_WAIT 而直接關閉:

當服務器的併發連接增多時,相應地,同時處於 TIME_WAIT 狀態的連接數量也會變多,此時就應當調大 tcp_max_tw_buckets 參數,減少不同連接間數據錯亂的概率。

tcp_max_tw_buckets 也不是越大越好,畢竟內存和端口都是有限的。

有一種方式可以在建立新連接時,復用處於 TIME_WAIT 狀態的連接,那就是打開 tcp_tw_reuse 參數。但是需要注意,該參數是只用於客戶端(建立連接的發起方),因為是在調用 connect() 時起作用的,而對於服務端(被動連接方)是沒有用的。

tcp_tw_reuse 從協議角度理解是安全可控的,可以復用處於 TIME_WAIT 的端口為新的連接所用。

什麼是協議角度理解的安全可控呢?主要有兩點:

  • 只適用於連接發起方,也就是 C/S 模型中的客戶端;
  • 對應的 TIME_WAIT 狀態的連接創建時間超過 1 秒才可以被複用。

使用這個選項,還有一個前提,需要打開對 TCP 時間戳的支持(對方也要打開 ):

由於引入了時間戳,它能帶來了些好處:

  • 我們在前面提到的 2MSL 問題就不復存在了,因為重複的數據包會因為時間戳過期被自然丟棄;
  • 同時,它還可以防止序列號繞回,也是因為重複的數據包會由於時間戳過期被自然丟棄;

時間戳是在 TCP 的選擇字段里定義的,開啟了時間戳功能,在 TCP 報文傳輸的時候會帶上發送報文的時間戳。

TCP option 字段 – 時間戳

我們來看看開啟了 tcp_tw_reuse 功能,如果四次揮手中的最後一次 ACK 在網絡中丟失了,會發生什麼?

四次揮手中的最後一次 ACK 在網絡中丟失

上圖的流程:

  • 四次揮手中的最後一次 ACK 在網絡中丟失了,服務端一直處於 LAST_ACK 狀態;
  • 客戶端由於開啟了 tcp_tw_reuse 功能,客戶再次發起新連接的時候,會復用超過 1 秒后的 time_wait 狀態的連接。但客戶端新發的 SYN 包會被忽略(由於時間戳),因為服務端比較了客戶端的上一個報文與 SYN 報文的時間戳,過期的報文就會被服務端丟棄
  • 服務端 FIN 報文遲遲沒有收到四次揮手的最後一次 ACK,於是超時重發了 FIN 報文給客戶端;
  • 處於 SYN_SENT 狀態的客戶,由於收到了 FIN 報文,則會回 RST 給服務端,於是服務端就離開了 LAST_ACK 狀態;
  • 最初的客戶端 SYN 報文超時重發了( 1 秒鐘后),此時就與服務端能正確的三次握手了。

所以大家都會說開啟了 tcp_tw_reuse,可以在復用了 time_wait 狀態的 1 秒過後成功建立連接,這 1 秒主要是花費在 SYN 包重傳。

另外,老版本的 Linux 還提供了 tcp_tw_recycle 參數,但是當開啟了它,就有兩個坑:

  • Linux 會加快客戶端和服務端 TIME_WAIT 狀態的時間,也就是它會使得 TIME_WAIT 狀態會小於 60 秒,很容易導致數據錯亂;
  • 另外,Linux 會丟棄所有來自遠端時間戳小於上次記錄的時間戳(由同一個遠端發送的)的任何數據包。就是說要使用該選項,則必須保證數據包的時間戳是單調遞增的。那麼,問題在於,此處的時間戳並不是我們通常意義上面的絕對時間,而是一個相對時間。很多情況下,我們是沒法保證時間戳單調遞增的,比如使用了 NAT、LVS 等情況;

所以,不建議設置為 1 ,在 Linux 4.12 版本后,Linux 內核直接取消了這一參數,建議關閉它:

另外,我們可以在程序中設置 socket 選項,來設置調用 close 關閉連接行為。

如果 l_onoff 為非 0, 且 l_linger 值為 0,那麼調用 close 后,會立該發送一個 RST 標誌給對端,該 TCP 連接將跳過四次揮手,也就跳過了 TIME_WAIT 狀態,直接關閉。

但這為跨越 TIME_WAIT 狀態提供了一個可能,不過是一個非常危險的行為,不值得提倡。

被動方的優化

當被動方收到 FIN 報文時,內核會自動回復 ACK,同時連接處於 CLOSE_WAIT 狀態,顧名思義,它表示等待應用進程調用 close 函數關閉連接。

內核沒有權利替代進程去關閉連接,因為如果主動方是通過 shutdown 關閉連接,那麼它就是想在半關閉連接上接收數據或發送數據。因此,Linux 並沒有限制 CLOSE_WAIT 狀態的持續時間。

當然,大多數應用程序並不使用 shutdown 函數關閉連接。所以,當你用 netstat 命令發現大量 CLOSE_WAIT 狀態。就需要排查你的應用程序,因為可能因為應用程序出現了 Bug,read 函數返回 0 時,沒有調用 close 函數。

處於 CLOSE_WAIT 狀態時,調用了 close 函數,內核就會發出 FIN 報文關閉發送通道,同時連接進入 LAST_ACK 狀態,等待主動方返回 ACK 來確認連接關閉。

如果遲遲收不到這個 ACK,內核就會重發 FIN 報文,重發次數仍然由 tcp_orphan_retries 參數控制,這與主動方重發 FIN 報文的優化策略一致。

還有一點我們需要注意的,如果被動方迅速調用 close 函數,那麼被動方的 ACK 和 FIN 有可能在一個報文中發送,這樣看起來,四次揮手會變成三次揮手,這隻是一種特殊情況,不用在意。

如果連接雙方同時關閉連接,會怎麼樣?

由於 TCP 是雙全工的協議,所以是會出現兩方同時關閉連接的現象,也就是同時發送了 FIN 報文。

此時,上面介紹的優化策略仍然適用。兩方發送 FIN 報文時,都認為自己是主動方,所以都進入了 FIN_WAIT1 狀態,FIN 報文的重發次數仍由 tcp_orphan_retries 參數控制。

同時關閉

接下來,雙方在等待 ACK 報文的過程中,都等來了 FIN 報文。這是一種新情況,所以連接會進入一種叫做 CLOSING 的新狀態,它替代了 FIN_WAIT2 狀態。接着,雙方內核回復 ACK 確認對方發送通道的關閉后,進入 TIME_WAIT 狀態,等待 2MSL 的時間后,連接自動關閉。

小結

針對 TCP 四次揮手的優化,我們需要根據主動方和被動方四次揮手狀態變化來調整系統 TCP 內核參數。

四次揮手的優化策略

主動方的優化

主動發起 FIN 報文斷開連接的一方,如果遲遲沒收到對方的 ACK 回復,則會重傳 FIN 報文,重傳的次數由 tcp_orphan_retries 參數決定。

當主動方收到 ACK 報文後,連接就進入 FIN_WAIT2 狀態,根據關閉的方式不同,優化的方式也不同:

  • 如果這是 close 函數關閉的連接,那麼它就是孤兒連接。如果 tcp_fin_timeout 秒內沒有收到對方的 FIN 報文,連接就直接關閉。同時,為了應對孤兒連接佔用太多的資源,tcp_max_orphans 定義了最大孤兒連接的數量,超過時連接就會直接釋放。
  • 反之是 shutdown 函數關閉的連接,則不受此參數限制;

當主動方接收到 FIN 報文,並返回 ACK 后,主動方的連接進入 TIME_WAIT 狀態。這一狀態會持續 1 分鐘,為了防止 TIME_WAIT 狀態佔用太多的資源,tcp_max_tw_buckets 定義了最大數量,超過時連接也會直接釋放。

當 TIME_WAIT 狀態過多時,還可以通過設置 tcp_tw_reusetcp_timestamps 為 1 ,將 TIME_WAIT 狀態的端口復用於作為客戶端的新連接,注意該參數只適用於客戶端。

被動方的優化

被動關閉的連接方應對非常簡單,它在回復 ACK 后就進入了 CLOSE_WAIT 狀態,等待進程調用 close 函數關閉連接。因此,出現大量 CLOSE_WAIT 狀態的連接時,應當從應用程序中找問題。

當被動方發送 FIN 報文後,連接就進入 LAST_ACK 狀態,在未等到 ACK 時,會在 tcp_orphan_retries 參數的控制下重發 FIN 報文。

03 TCP 傳輸數據的性能提升

在前面介紹的是三次握手和四次揮手的優化策略,接下來主要介紹的是 TCP 傳輸數據時的優化策略。

TCP 連接是由內核維護的,內核會為每個連接建立內存緩衝區:

  • 如果連接的內存配置過小,就無法充分使用網絡帶寬,TCP 傳輸效率就會降低;
  • 如果連接的內存配置過大,很容易把服務器資源耗盡,這樣就會導致新連接無法建立;

因此,我們必須理解 Linux 下 TCP 內存的用途,才能正確地配置內存大小。

滑動窗口是如何影響傳輸速度的?

TCP 會保證每一個報文都能夠抵達對方,它的機制是這樣:報文發出去后,必須接收到對方返回的確認報文 ACK,如果遲遲未收到,就會超時重發該報文,直到收到對方的 ACK 為止。

所以,TCP 報文發出去后,並不會立馬從內存中刪除,因為重傳時還需要用到它。

由於 TCP 是內核維護的,所以報文存放在內核緩衝區。如果連接非常多,我們可以通過 free 命令觀察到 buff/cache 內存是會增大。

如果 TCP 是每發送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。這個模式就有點像我和你面對面聊天,你一句我一句,但這種方式的缺點是效率比較低的。

按數據包進行確認應答

所以,這樣的傳輸方式有一個缺點:數據包的往返時間越長,通信的效率就越低

要解決這一問題不難,并行批量發送報文,再批量確認報文即刻。

并行處理

然而,這引出了另一個問題,發送方可以隨心所欲的發送報文嗎?當然這不現實,我們還得考慮接收方的處理能力。

當接收方硬件不如發送方,或者系統繁忙、資源緊張時,是無法瞬間處理這麼多報文的。於是,這些報文只能被丟掉,使得網絡效率非常低。

為了解決這種現象發生,TCP 提供一種機制可以讓「發送方」根據「接收方」的實際接收能力控制發送的數據量,這就是滑動窗口的由來。

接收方根據它的緩衝區,可以計算出後續能夠接收多少字節的報文,這個数字叫做接收窗口。當內核接收到報文時,必須用緩衝區存放它們,這樣剩餘緩衝區空間變小,接收窗口也就變小了;當進程調用 read 函數后,數據被讀入了用戶空間,內核緩衝區就被清空,這意味着主機可以接收更多的報文,接收窗口就會變大。

因此,接收窗口並不是恆定不變的,接收方會把當前可接收的大小放在 TCP 報文頭部中的窗口字段,這樣就可以起到窗口大小通知的作用。

發送方的窗口等價於接收方的窗口嗎?如果不考慮擁塞控制,發送方的窗口大小「約等於」接收方的窗口大小,因為窗口通知報文在網絡傳輸是存在時延的,所以是約等於的關係。

TCP 頭部

從上圖中可以看到,窗口字段只有 2 個字節,因此它最多能表達 65535 字節大小的窗口,也就是 64KB 大小。

這個窗口大小最大值,在當今高速網絡下,很明顯是不夠用的。所以後續有了擴充窗口的方法:在 TCP 選項(option)字段定義了窗口擴大因子,用於擴大 TCP 通告窗口,其值大小是 2^14,這樣就使 TCP 的窗口大小從 16 位擴大為 30 位(2^16 * 2^ 14 = 2^30),所以此時窗口的最大值可以達到 1GB。

TCP option 選項 – 窗口擴展

Linux 中打開這一功能,需要把 tcp_window_scaling 配置設為 1(默認打開):

要使用窗口擴大選項,通訊雙方必須在各自的 SYN 報文中發送這個選項:

  • 主動建立連接的一方在 SYN 報文中發送這個選項;
  • 而被動建立連接的一方只有在收到帶窗口擴大選項的 SYN 報文之後才能發送這個選項。

這樣看來,只要進程能及時地調用 read 函數讀取數據,並且接收緩衝區配置得足夠大,那麼接收窗口就可以無限地放大,發送方也就無限地提升發送速度。

這是不可能的,因為網絡的傳輸能力是有限的,當發送方依據發送窗口,發送超過網絡處理能力的報文時,路由器會直接丟棄這些報文。因此,緩衝區的內存並不是越大越好。

如果確定最大傳輸速度?

在前面我們知道了 TCP 的傳輸速度,受制於發送窗口與接收窗口,以及網絡設備傳輸能力。其中,窗口大小由內核緩衝區大小決定。如果緩衝區與網絡傳輸能力匹配,那麼緩衝區的利用率就達到了最大化。

問題來了,如何計算網絡的傳輸能力呢?

相信大家都知道網絡是有「帶寬」限制的,帶寬描述的是網絡傳輸能力,它與內核緩衝區的計量單位不同:

  • 帶寬是單位時間內的流量,表達是「速度」,比如常見的帶寬 100 MB/s;
  • 緩衝區單位是字節,當網絡速度乘以時間才能得到字節數;

這裏需要說一個概念,就是帶寬時延積,它決定網絡中飛行報文的大小,它的計算方式:

比如最大帶寬是 100 MB/s,網絡時延(RTT)是 10ms 時,意味着客戶端到服務端的網絡一共可以存放 100MB/s * 0.01s = 1MB 的字節。

這個 1MB 是帶寬和時延的乘積,所以它就叫「帶寬時延積」(縮寫為 BDP,Bandwidth Delay Product)。同時,這 1MB 也表示「飛行中」的 TCP 報文大小,它們就在網絡線路、路由器等網絡設備上。如果飛行報文超過了 1 MB,就會導致網絡過載,容易丟包。

由於發送緩衝區大小決定了發送窗口的上限,而發送窗口又決定了「已發送未確認」的飛行報文的上限。因此,發送緩衝區不能超過「帶寬時延積」。

發送緩衝區與帶寬時延積的關係:

  • 如果發送緩衝區「超過」帶寬時延積,超出的部分就沒辦法有效的網絡傳輸,同時導致網絡過載,容易丟包;
  • 如果發送緩衝區「小於」帶寬時延積,就不能很好的發揮出網絡的傳輸效率。

所以,發送緩衝區的大小最好是往帶寬時延積靠近。

怎樣調整緩衝區大小?

在 Linux 中發送緩衝區和接收緩衝都是可以用參數調節的。設置完后,Linux 會根據你設置的緩衝區進行動態調節

調節發送緩衝區範圍

先來看看發送緩衝區,它的範圍通過 tcp_wmem 參數配置;

上面三個数字單位都是字節,它們分別表示:

  • 第一個數值是動態範圍的最小值,4096 byte = 4K;
  • 第二個數值是初始默認值,87380 byte ≈ 86K;
  • 第三個數值是動態範圍的最大值,4194304 byte = 4096K(4M);

發送緩衝區是自行調節的,當發送方發送的數據被確認后,並且沒有新的數據要發送,就會把發送緩衝區的內存釋放掉。

調節接收緩衝區範圍

而接收緩衝區的調整就比較複雜一些,先來看看設置接收緩衝區範圍的 tcp_rmem 參數:

上面三個数字單位都是字節,它們分別表示:

  • 第一個數值是動態範圍的最小值,表示即使在內存壓力下也可以保證的最小接收緩衝區大小,4096 byte = 4K;
  • 第二個數值是初始默認值,87380 byte ≈ 86K;
  • 第三個數值是動態範圍的最大值,6291456 byte = 6144K(6M);

接收緩衝區可以根據系統空閑內存的大小來調節接收窗口:

  • 如果系統的空閑內存很多,就可以自動把緩衝區增大一些,這樣傳給對方的接收窗口也會變大,因而提升發送方發送的傳輸數據數量;
  • 反正,如果系統的內存很緊張,就會減少緩衝區,這雖然會降低傳輸效率,可以保證更多的併發連接正常工作;

發送緩衝區的調節功能是自動開啟的,而接收緩衝區則需要配置 tcp_moderate_rcvbuf 為 1 來開啟調節功能

調節 TCP 內存範圍

接收緩衝區調節時,怎麼知道當前內存是否緊張或充分呢?這是通過 tcp_mem 配置完成的:

上面三個数字單位不是字節,而是「頁面大小」,1 頁表示 4KB,它們分別表示:

  • 當 TCP 內存小於第 1 個值時,不需要進行自動調節;
  • 在第 1 和第 2 個值之間時,內核開始調節接收緩衝區的大小;
  • 大於第 3 個值時,內核不再為 TCP 分配新內存,此時新連接是無法建立的;

一般情況下這些值是在系統啟動時根據系統內存數量計算得到的。根據當前 tcp_mem 最大內存頁面數是 177120,當內存為 (177120 * 4) / 1024K ≈ 692M 時,系統將無法為新的 TCP 連接分配內存,即 TCP 連接將被拒絕。

根據實際場景調節的策略

在高併發服務器中,為了兼顧網速與大量的併發連接,我們應當保證緩衝區的動態調整的最大值達到帶寬時延積,而最小值保持默認的 4K 不變即可。而對於內存緊張的服務而言,調低默認值是提高併發的有效手段。

同時,如果這是網絡 IO 型服務器,那麼,調大 tcp_mem 的上限可以讓 TCP 連接使用更多的系統內存,這有利於提升併發能力。需要注意的是,tcp_wmem 和 tcp_rmem 的單位是字節,而 tcp_mem 的單位是頁面大小。而且,千萬不要在 socket 上直接設置 SO_SNDBUF 或者 SO_RCVBUF,這樣會關閉緩衝區的動態調整功能。

小結

本節針對 TCP 優化數據傳輸的方式,做了一些介紹。

數據傳輸的優化策略

TCP 可靠性是通過 ACK 確認報文實現的,又依賴滑動窗口提升了發送速度也兼顧了接收方的處理能力。

可是,默認的滑動窗口最大值只有 64 KB,不滿足當今的高速網絡的要求,要想要想提升發送速度必須提升滑動窗口的上限,在 Linux 下是通過設置 tcp_window_scaling 為 1 做到的,此時最大值可高達 1GB。

滑動窗口定義了網絡中飛行報文的最大字節數,當它超過帶寬時延積時,網絡過載,就會發生丟包。而當它小於帶寬時延積時,就無法充分利用網絡帶寬。因此,滑動窗口的設置,必須參考帶寬時延積。

內核緩衝區決定了滑動窗口的上限,緩衝區可分為:發送緩衝區 tcp_wmem 和接收緩衝區 tcp_rmem。

Linux 會對緩衝區動態調節,我們應該把緩衝區的上限設置為帶寬時延積。發送緩衝區的調節功能是自動打開的,而接收緩衝區需要把 tcp_moderate_rcvbuf 設置為 1 來開啟。其中,調節的依據是 TCP 內存範圍 tcp_mem。

但需要注意的是,如果程序中的 socket 設置 SO_SNDBUF 和 SO_RCVBUF,則會關閉緩衝區的動態整功能,所以不建議在程序設置它倆,而是交給內核自動調整比較好。

有效配置這些參數后,既能夠最大程度地保持併發性,也能讓資源充裕時連接傳輸速度達到最大值。

巨人的肩膀

[1] 系統性能調優必知必會.陶輝.極客時間.

[2] 網絡編程實戰專欄.盛延敏.極客時間.

[3] http://www.blogjava.net/yongboy/archive/2013/04/11/397677.html

[4] http://blog.itpub.net/31559359/viewspace-2284113/

[5] https://blog.51cto.com/professor/1909022

[6] https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux

嘮嗑嘮嗑

跟大家說個沉痛的事情。

我想大部分小夥伴都發現了,最近公眾號改版,訂閱號里的信息流不再是以時間順序了,而是以推薦算法方式显示順序。

這對小林這種「周更」的作者,真的一次重重打擊,非常的不友好。

因為長時間沒發文,公眾號可能會把推薦的權重降低,這就會導致很多讀者,會收不到我的「最新」的推文,如此下去,那小林文章不就無人問津了?(抱頭痛哭 …)

另外,小林更文時間長的原因,不是因為偷懶。

而是為了把知識點「寫的更清楚,畫的更清晰」,所以這必然會花費更多更長的時間。

如果你認可和喜歡小林的文章,不想錯過文章的第一時間推送,可以動動你的小手手,給小林公眾號一個「星標」。

平時沒事,就讓「小林coding」靜靜地躺在你的訂閱號底部,但是你要知道它在這其間並非無所事事,而是在努力地準備着更好的內容,等準備好了,它自然會「蹦出」在你面前。

小林是專為大家圖解的工具人,Goodbye,我們下次見!

讀者問答

讀者問:“小林,請教個問題,somaxconn和backlog是不是都是指的是accept隊列?然後somaxconn是內核參數,backlog是通過系統調用間隔地修改somaxconn,比如Linux中listen()函數?”

兩者取最小值才是 accpet 隊列。

讀者問:“小林,還有個問題要請教下,“如果 accept 隊列滿了,那麼 server 扔掉 client 發過來的 ack”,也就是說該TCP連接還是位於半連接隊列中,沒有丟棄嗎?”

  1. 當 accept 隊列滿了,後續新進來的syn包都會被丟失
  2. 我文章的突發流量例子是,那個連接進來的時候 accept 隊列還沒滿,但是在第三次握手的時候,accept 隊列突然滿了,就會導致 ack 被丟棄,就一直處於半連接隊列。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

Spring AOP學習筆記02:如何開啟AOP

  上文簡要總結了一些AOP的基本概念,並在此基礎上敘述了Spring AOP的基本原理,並且輔以一個簡單例子幫助理解。從本文開始,我們要開始深入到源碼層面來一探Spring AOP魔法的原理了。

  要使用Spring AOP,第一步是要將這一功能開啟,一般有兩種方式:

  • 通過xml配置文件的方式;
  • 通過註解的方式;

 

1. 配置文件開啟AOP功能

  我們先來看一下配置文件的方式,這個上文也提到過,在xml文件中加上對應的標籤,而且別忘了加上對應的名稱空間(即下面的xmlns:aop。。。):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop = "http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
     http://www.springframework.org/schema/aop
     http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
     
     <aop:aspectj-autoproxy/>

</beans>

  這裡是通過標籤<aop:aspectj-autoproxy/>來完成開啟AOP功能,這是一個自定義標籤,需要自定義其解析,而這些spring都已經實現好了,前面專門寫過一篇文章講述spring是如何解析自定義xml標籤的,我們這裏大致回顧一下解析流程:

  • 定義一個XML文件來描述你的自定義標籤元素;
  • 創建一個Handler,擴展自NamespaceHandlerSupport,用於註冊下面的parser;
  • 創建若干個BeanDefinitionParser的實現,用來解析XML文件中的定義;
  • 將上述文件註冊到Spring中,這裏其實是做一下配置;

  我們就不照着這個步驟來了,我們直接參考spring對這個自定義標籤的解析過程,上面的4個步驟只是作為參考,在整個解析過程中都會涉及到。

  前面講解析自定義xml標籤時候提到過,解析的流程大致如下:

  • 首先會去獲取自定義標籤對應的名稱空間;
  • 然後根據名稱空間找到對應的NamespaceHandler;
  • 調用自定義的NamespaceHandler進行解析;

1.1 獲取名稱空間

  這裏<aop:aspectj-autoproxy/>對應的名稱空間是什麼呢?在上面的開啟aop的配置文件裏面名稱空間那裡給出了一些線索,其實就是下面這個:

http://www.springframework.org/schema/aop

  至於名稱空間的獲取,也無甚好說的,其實就是直接調用org.w3c.dom.Node提供的相應方法來完成名稱空間的提取。

1.2 獲取handler

  然後又是如何根據名稱空間找到對應的NamespaceHandler呢?之前也說到過,在找對應的NamespaceHandler時會去META-INF/spring.handlers這個目錄下加載資源文件,我們來找一下spring.handlers這個文件看看(需要去spring-aop對應的jar報下找):

http\://www.springframework.org/schema/aop=org.springframework.aop.config.AopNamespaceHandler

  看到沒,這裡是以key-value的形式維護着名稱空間和對應handler的關係的,所以對應的handler就是這個AopNamespaceHandler。spring根據名稱空間找到這個handler之後,會通過反射的方式將這個類加載,並緩存起來。

1.3 解析標籤

  上面的handler只有一個自定義的方法:

public void init() {
    // In 2.0 XSD as well as in 2.1 XSD.
    registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
    registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
    registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());

    // Only in 2.0 XSD: moved to context namespace as of 2.1
    registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
}

  這是一個初始化方法,在加載的時候會執行,主要作用就是註冊一些解析器,這裏我們主要關注AspectJAutoProxyBeanDefinitionParser,這就是我們要找的,它的作用就是解析<aop:aspectj-autoproxy/>標籤的。主要流程就是,spring會調用上一步拿到的AopNamespaceHandler的parse()方法,在這個方法裏面,會將解析的工作委託給AspectJAutoProxyBeanDefinitionParser來完成具體解析工作,我們就來看一下具體幹了啥吧。

  開始解析的工作從這裏開始:

return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

  此時我們拿到的handler其實是我們自定義的AopNamespaceHandler了,但是它並沒有實現parse()方法,所以這裏這個應該是調用的父類(NamespaceHandlerSupport)中的parse()方法:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 尋找解析器並進行解析操作
    return findParserForElement(element, parserContext).parse(element, parserContext);
}

private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // 獲取元素名稱,也就是<aop:aspectj-autoproxy/>中的aspectj-autoproxy
    String localName = parserContext.getDelegate().getLocalName(element);
    // 根據aspectj-autoproxy找到對應的解析器,也就是在registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
    // 註冊的解析器
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal(
            "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

  首先是尋找元素對應的解析器,然後調用其parse()方法。結合我們前面的示例,其實就是首先獲取在AopNamespaceHandler類中的init()方法中註冊對應的AspectJAutoProxyBeanDefinitionParser實例,並調用其parse()方法進行進一步解析:

public BeanDefinition parse(Element element, ParserContext parserContext) {
    AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element);
    extendBeanDefinition(element, parserContext);
    return null;
}

// 下面的代碼在AopConfigUtils中
public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary(
        ParserContext parserContext, Element sourceElement) {

    BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(
            parserContext.getRegistry(), parserContext.extractSource(sourceElement));
    useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement);
    registerComponentIfNecessary(beanDefinition, parserContext);
}

public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, Object source) {
    return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}

private static BeanDefinition registerOrEscalateApcAsRequired(Class cls, BeanDefinitionRegistry registry, Object source) {
    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
    if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
        BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
        if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
            int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
            int requiredPriority = findPriorityForClass(cls);
            if (currentPriority < requiredPriority) {
                apcDefinition.setBeanClassName(cls.getName());
            }
        }
        return null;
    }
    RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
    beanDefinition.setSource(source);
    beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
    beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
    registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
    return beanDefinition;
}

  上面這一堆代碼最核心的部分就在後兩個方法中,就是完成了對AnnotationAwareAspectJAutoProxyCreator類的註冊,到這裏對自定義標籤<aop:aspectj-autoproxy/>的解析也就完成了,可以看到其最核心的部分就是完成了對AnnotationAwareAspectJAutoProxyCreator類的註冊,那為什麼註冊了這個類就開啟了aop功能呢?這裏先賣個關子,後面詳細說。

  這裏再回過頭來看一下上面說到的spring對自定義標籤解析的4個步驟,其實第一步的schema對應的是在org.springframework.aop.config路徑下的spring-aop-3.0.xsd文件,其映射關係是維護在META-INF/spring.schemas文件中的,而spring-aop-3.0.xsd的主要作用就是描述自定義標籤。

  當通過META-INF/spring.handlers找到對應的AopNamespaceHandler,並通過在其加載后執行init()方法過程中完成了AspectJAutoProxyBeanDefinitionParser的註冊,有這個parser再來完成對自定義標籤的解析工作,這對應上面4個步驟中的第二步和第三部。至於第四步的配置工作,無非就是將spring.schemas和spring.handlers這兩個配置文件放在META-INF/目錄下罷了。

  關於這部分解析過程,寫得不是非常詳細,如果有不明白,可以參考之前一篇文章,講spring是如何解析自定義xml標籤。

 

2. 註解方式開啟aop

  另一種開啟spring aop的方式是通過註解的方式,使用的註解是@EnableAspectJAutoProxy,可以通過配置類的方式完成註冊:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

   也可以在啟動類上直接加上這個註解,這在springboot中比較常見,其實質也是上面的方式。通過這種方式配置之後,就開啟了aop功能,那具體又是如何實現的呢?我們看一下這個註解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

    /**
     * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
     * to standard Java interface-based proxies. The default is {@code false}.
     */
    boolean proxyTargetClass() default false;

    /**
     * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
     * for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
     * Off by default, i.e. no guarantees that {@code AopContext} access will work.
     * @since 4.3.1
     */
    boolean exposeProxy() default false;

}

  這裏我們的關注點是其通過@Import(AspectJAutoProxyRegistrar.class)引入了AspectJAutoProxyRegistrar,那這又是什麼?

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * Register, escalate, and configure the AspectJ auto proxy creator based on the value
     * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing
     * {@code @Configuration} class.
     */
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }
            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }
    }

}

  看到這裏,是不是有點眼熟了呢?是的,其實它也是和上面說的xml配置使用的方式一樣,通過AopConfigUtils來完成AnnotationAwareAspectJAutoProxyCreator類的註冊。是不是比xml配置文件的方式方便許多呢。

 

4. 開啟aop的魔法

  通過前面的學習我們了解了可以通過Spring自定義配置完成對AnnotationAwareAspectJAutoProxyCreator類型的自動註冊,而這個類到底是做了什麼工作來實現AOP的操作呢?這裏還是先來看一下AnnotationAwareAspectJAutoProxyCreator的類層次結構:

  這裡有一個很重要的點,就是AnnotationAwareAspectJAutoProxyCreator實現了BeanPostProcessor接口。在IOC部分的文章中有詳細說過,Spring在加載Bean的過程中會在實例化bean前後調用BeanPostProcessor的相關方法(相關邏輯是在initializeBean方法中,調用postProcessBeforeInitialization、postProcessAfterInitialization方法),而AOP的魔法就是從這裏開始的。

  每次看到這裏,我內心對spring的軟件架構設計都是湧現出無比的佩服,通過後處理器的方式來做擴展,對原有模塊是沒有任何改動,也不會產生耦合,spring親自踐行着對修改關閉,對擴展開放的原則。

 

3. 總結

   本文我們學習了spring是如何開啟aop功能的,無論是通過xml配置文件方式,還是通過Java config這種註解的方式,其最終都是完成了將AnnotationAwareAspectJAutoProxyCreator這個類註冊到spring容器當中,那這個類又有什麼魔法,可以達到將其註冊到容器即達到開啟aop的功效,其實其繼承自BeanPostProcessor接口,通過後處理器的方式擴展出了開啟spring aop的功能。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

Oracle數據遷移後由列的直方圖統計信息引起的執行計劃異常

(一)問題背景

在使用impdp進行數據導入的時候,往往在導入表和索引的統計信息的時候,速度非常慢,因此我在使用impdp進行導入時,會使用exclude=table_statistics排除表的統計信息,從而加快導入速度,之後再手動收集統計信息。

                                              圖.impdp導入數據的時導入統計信息速度非常慢

導入語句如下:

impdp user/password directory=DUMPDIR dumpfile=TEST01.dmp logfile=TEST01.log remap_schema=TEST_USER:TEST_USER123 exclude=table_statistics

手動收集統計信息語句如下:

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

最近使用以上方法將數據還原到測試環境后,發現與生產環境執行計劃存在偏差,本來應該走全表掃描的,卻走了索引範圍掃描。經過確認,是由於列的直方圖統計信息未收集引發的執行計劃偏差。


(二)列的直方圖統計信息

什麼是列的直方圖統計信息呢?在Oracle數據庫中,Oracle默認列上的值是在最小值與最大值之間均分佈的,當在計算cardinatity時,會以均勻分佈的方式計算,但是在實際生活中某些場景下數據並非均勻分佈。舉個列子,某公司有員工10000人,表A的列COL1記錄員工的績效(分別是:A、B、C、D,A最好,D最差),那麼可能A佔了15%,B佔了60,C佔了20%,D佔了5%。很明顯在該場景下數據並非均勻分佈,假如以均勻分佈的方式去統計員工的績效,可能會導致執行計劃失准。

當列的數據分佈不均勻的時候,就需要統計列上的數據分佈情況,從而走出正確的執行計劃,列的直方圖統計信息就是記錄列上的數據分佈情況的。


(三)異常模擬

STEP1:創建測試表test01

create table test01
(id number,
name varchar2(10)
);
create index idx_test01_id on test01(id);

向test01中插入測試數據

begin
insert into test01 values(1,'a');

for i in 1..10 loop
insert into test01 values(2,'b');
end loop;

for i in 1..100 loop
insert into test01 values(3,'c');
end loop;

for i in 1..1000 loop
insert into test01 values(4,'d');
end loop;

commit;
end;

查看數據分佈情況:

SQL> SELECT ID,NAME,COUNT(*) FROM test01 GROUP BY ID,NAME ORDER BY COUNT(*);

ID          NAME       COUNT(*)
---------- ---------- ----------
1           a          1       
2           b          10
3           c          100
4           d          1000


STEP2:收集統計信息,因為上面查詢過id列,故在收集統計信息的時候,會收集直方圖的統計信息

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

查看是否已經收集了直方圖信息,發現id列上已經收集

SQL> SELECT a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
2 FROM dba_tab_columns a
3 WHERE a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER     TABLE_NAME   COLUMN_NAME   LOW_VALUE     HIGH_VALUE    NUM_BUCKETS   HISTOGRAM
--------- -----------  ------------  ------------  ------------  -----------  ---------------
LIJIAMAN  TEST01       ID            C102          C105          4             FREQUENCY
LIJIAMAN  TEST01       NAME          61            64            1             NONE

查看直方圖,已經將id列的4個值放入了4個bucket中:

SQL> SELECT * FROM dba_tab_histograms a WHERE a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER        TABLE_NAME    COLUMN_NAME    ENDPOINT_NUMBER    ENDPOINT_VALUE ENDPOINT_ACTUAL_VALUE
-----------  ------------  -------------  ---------------    -------------- ----------------------
LIJIAMAN     TEST01        ID                           1                 1 
LIJIAMAN     TEST01        ID                          11                 2 
LIJIAMAN     TEST01        ID                         111                 3 
LIJIAMAN     TEST01        ID                        1111                 4 
LIJIAMAN     TEST01        NAME                         0    5.036527952778 
LIJIAMAN     TEST01        NAME                         1    5.192296858534


STEP3:查看id=1和id=4的執行計劃,當id=1時,走索引範圍掃描,當id=4時,走全表掃描

id列存在直方圖統計信息,當id=1時,走索引範圍掃描 id列存在直方圖統計信息,當id=4時,走全表掃描
SELECT * FROM test01 WHERE ID=1

 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |    1 |     5 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |    1 |     5 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |    1 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=1)
SELECT * FROM test01 WHERE ID=4

 Plan Hash Value  : 262542483 

-----------------------------------------------------------------------
 | Id  | Operation           | Name   | Rows | Bytes | Cost | Time     |
-----------------------------------------------------------------------
 |   0 | SELECT STATEMENT    |        | 1000 |  5000 |    3 | 00:00:01 |
 | * 1 |   TABLE ACCESS FULL | TEST01 | 1000 |  5000 |    3 | 00:00:01 |
-----------------------------------------------------------------------

Predicate Information (identified by operation id):
 ------------------------------------------
 * 1 - filter("ID"=4)

STEP4:接下來模擬數據遷移,排除統計信息

導出表test01

expdp lijiaman/lijiaman directory=DUMPDIR tables=LIJIAMAN.TEST01 dumpfile =test01.dmp

刪除原來的表:

SQL> drop table test01;
Table dropped

再次導入表,排除統計信息:

impdp lijiaman/lijiaman directory=DUMPDIR dumpfile =test01.dmp exclude=table_statistics

查看錶的統計信息,不存在統計信息:

SQL> SELECT   a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
   2  FROM     dba_tab_columns a
   3  WHERE    a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER          TABLE_NAME      COLUMN_NAME     LOW_VALUE    HIGH_VALUE   NUM_BUCKETS HISTOGRAM
 -------------- --------------- --------------- ------------ ------------ ----------- ---------------
 LIJIAMAN       TEST01          ID                                                    NONE
 LIJIAMAN       TEST01          NAME                                                  NONE

STEP5:手動收集統計信息

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

發現統計信息已經收集,但是不存在直方圖的統計信息

SQL> SELECT   a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
  2  FROM     dba_tab_columns a
  3  WHERE    a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER     TABLE_NAME  COLUMN_NAME  LOW_VALUE   HIGH_VALUE  NUM_BUCKETS HISTOGRAM
--------- ----------- -----------  ----------- ----------- ----------- ---------------
LIJIAMAN  TEST01      ID           C102        C105                  1 NONE
LIJIAMAN  TEST01      NAME         61          64                    1 NONE

STEP6:再次查看id=1和id=4的執行計劃,當id=1或id=4時,都走索引範圍掃描

id列未收集直方圖統計信息,當id=1時,走索引範圍掃描 id列未收集直方圖統計信息,當id=4時,走索引範圍掃描
SELECT * FROM test01 WHERE ID=1
 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |  278 |  1390 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |  278 |  1390 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |  278 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=1)
SELECT * FROM test01 WHERE ID=4

 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |  278 |  1390 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |  278 |  1390 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |  278 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=4)

STEP7:再次收集統計信息,因為使用過了id列作為查詢條件,故再次收集統計信息時,會收集id列的直方圖信息:

EXEC dbms_stats.gather_table_stats(ownname => 'LIJIAMAN',tabname => 'TEST01');

可以看到,此時已經收集了id列的直方圖統計信息:

SQL> SELECT   a.OWNER,a.TABLE_NAME,a.COLUMN_NAME,a.LOW_VALUE,a.HIGH_VALUE,a.NUM_BUCKETS,a.HISTOGRAM
  2  FROM     dba_tab_columns a
  3  WHERE    a.OWNER = 'LIJIAMAN' AND a.TABLE_NAME = 'TEST01';

OWNER                          TABLE_NAME                     COLUMN_NAME                    LOW_VALUE     HIGH_VALUE    NUM_BUCKETS HISTOGRAM
------------------------------ ------------------------------ ------------------------------ ------------- ------------- ----------- ---------------
LIJIAMAN                       TEST01                         ID                             C102          C105                    4 FREQUENCY
LIJIAMAN                       TEST01                         NAME                           61            64                      1 NONE

執行計劃已經按照我們想要的方式走:

id列重新收集直方圖統計信息,當id=1時,走索引範圍掃描 id列重新收集直方圖統計信息,當id=4時,走全表掃描
SELECT * FROM test01 WHERE ID=1

 Plan Hash Value  : 1151852672 

----------------------------------------------------------------------------------------
| Id  | Operation                     | Name          | Rows | Bytes | Cost | Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |               |    1 |     5 |    2 | 00:00:01 |
|   1 |   TABLE ACCESS BY INDEX ROWID | TEST01        |    1 |     5 |    2 | 00:00:01 |
| * 2 |    INDEX RANGE SCAN           | IDX_TEST01_ID |    1 |       |    1 | 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 2 - access("ID"=1)
SELECT * FROM test01 WHERE ID=4

 Plan Hash Value  : 262542483 

-----------------------------------------------------------------------
| Id  | Operation           | Name   | Rows | Bytes | Cost | Time     |
-----------------------------------------------------------------------
|   0 | SELECT STATEMENT    |        | 1000 |  5000 |    3 | 00:00:01 |
| * 1 |   TABLE ACCESS FULL | TEST01 | 1000 |  5000 |    3 | 00:00:01 |
-----------------------------------------------------------------------

Predicate Information (identified by operation id):
------------------------------------------
* 1 - filter("ID"=4)

(四)總結

在使用expdp/impdp進行導出/導入數據的時,統計信息是非常重要的,對於大部分統計信息,我們可以在導入結束之後收集獲得。但是對於列的直方圖統計信息,Oracle默認收集的方式是auto,即Oracle會根據用戶對列的使用情況進行判斷是否收集直方圖統計信息,然而數據剛遷移完成,在表還未使用的情況下收集統計信息,往往收集不到列的直方圖信息,這就造成了執行計劃異常,這種情況通常在下一次收集統計信息之後會有所改變。

參考文檔:

DBMS_STATS With METHOD_OPT =>’..SIZE auto’ May Not Collect Histograms (Doc ID 557594.1)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

分類
發燒車訊

02 . Zabbix配置監控項及聚合圖形

安裝Zabbix Agent監控本機

安裝agent軟件

與server端不同,Agent只需安裝zabbix-agent包

cat /etc/yum.repos.d/zabbix.repo 
[zabbix]
name=Zabbix Official Repository - $basearch
baseurl=https://mirrors.aliyun.com/zabbix/zabbix/3.4/rhel/7/$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-ZABBIX-A14FE591
 
[zabbix-non-supported]
name=Zabbix Official Repository non-supported - $basearch
baseurl=https://mirrors.aliyun.com/zabbix/non-supported/rhel/7/$basearch/
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-ZABBIX
gpgcheck=1


curl https://mirrors.aliyun.com/zabbix/RPM-GPG-KEY-ZABBIX-A14FE591 -o /etc/pki/rpm-gpg/RPM-GPG-KEY-ZABBIX-A14FE591
curl https://mirrors.aliyun.com/zabbix/RPM-GPG-KEY-ZABBIX -o /etc/pki/rpm-gpg/RPM-GPG-KEY-ZABBIX

yum -y install zabbix-agent zabbix-get
配置Agent並啟動
vim /etc/zabbix/zabbix_agentd.conf
Server=39.108.140.0                    # 被動模式 zabbix-server-ip
ServerActive=39.108.140.0              # 主動模式 zabbix-server-ip
Hostname=You-Men                       # Agent端主機名,最終显示在監控頁面上的名字
UnsafeUserParameters=1                 # 是否限制用戶自定義keys使用特殊字符

systemctl restart zabbix-agent
netstat -antp|grep agent
tcp        0      0 0.0.0.0:10050           0.0.0.0:*               LISTEN      3898/zabbix_agentd  
tcp6       0      0 :::10050                :::*                    LISTEN      3898/zabbix_agentd
配置snmp(可以不做)

zabbix除了可以使用agent獲取數據之外,還可以通過snmp獲取數據,為了能夠讓zabbix監控更多的信息,將本機的snmp功能啟動起來.

yum -y install net-snmp net-snmp-utils
vim /etc/snmp/snmpd.conf
com2sec notConfigUser 39.108.140.0 public
access notConfigGroup "" any noauth exact all none none
view all included .1 80
systemctl restart snmpd && systemctl enabel snmpd
ss -anup |grep snmp        # 161端口,udp協議
# 測試snmp協議工作是否正常
# snmpwalk -v 1 -c public 39.108.140.0   .1.3.6
# 使用v1版本,共同體為public,來對192.168.0.1的.1.3.6分支進行walk。

snmpwalk -v 2c -c public 39.108.140.0
# 使用v2c版本,共同體為public,對39.108.140.0進行walk。
# -v        显示當前SNMPWALK命令行版本.
# -
# 獲取cisco設備39.108.140.0的接口類型

接下來我們到web界面上配置如何監控本地主機,我們看到接口上是127.0.0.1,但是我們配置文件寫的是39.108.140.0,我們讓這兩個IP一致.點擊3進去然後修改.

更新完后,跳到下面頁面,稍等一會,重新載入一下頁面就是可用性為綠色了

至此,監控本地主機就完成了,如果想看下監控本地主機的網卡流量就做下面圖2步驟.鼠標依次根據数字挨個點,如果想要監控項是中文的話,可以做Zabbix故障例一,但是4.4版本較以前版本有所改善,監控項不是亂碼,而是英文.

如果想要將這種亂碼換成正常中文

如果是windows在C盤搜索simkai.tff中文楷體,拷貝/上傳到服務器,然後cp到zabbix的字體目錄
3.*版本:
cp  /root/simkai.ttf   /usr/share/zabbix/fonts/

# 不同的安裝方式,路徑會有所不同,所以可以直接find / -type d -type fonts找到類似的文件夾,那就是了
# 注意字體權限問題
vim /usr/share/zabbix/include/defines.inc.php
    define('ZBX_GRAPH_FONT_NAME',    'simkai');
    define('ZBX_FONT_NAME',            'simkai');

Zabbix監控遠程主機

如果遠程主機安裝不上zabbix-agent,可以通過裝的上的zabbix-agent的機器把包傳過去

yum -y instlal yum-utils
# 下載到指定目錄
yum install zabbix-agent -y --downloadonly --downloaddir=/root
1.安裝zabbix agent
    # 方法一(國外源zabbix好像下載不下來包了,用上面的源):
    # rpm -Uvh https://repo.zabbix.com/zabbix/4.4/rhel/7/x86_64/zabbix-release-4.4-1.el7.noarch.rpm
    yum clean all
    yum -y install zabbix-agent
    # 方法二:(使用別的機器傳過來的zabbix-agent包直接rpm安裝即可)
    rpm -ivh zabbix-agent-4.4.1-1.el7.x86_64.rpm

# 修改zabbix-agent配置並啟動服務

    vim /etc/zabbix/zabbix_agentd.conf
    Server=192.168.244.144
    Server=192.168.244.144            //監控主機IP地址
    Hostname=agent1.zabbix.com        //被監控主機到監控主機的名字
    UnsafeUserParameters=1

    systemctl start zabbix-agent
    ss -antp |grep 10050
# 接下來我們到web端進行操作
# 為了服務方便管理和易於查看。
# 監控系統中往往根據被監控的主機角色或其他屬性將同類主機劃分到同一個主機組中.

如果等上一段時間,可用性哪裡沒有紅色警告,就說明這台主機被添加進來了,但是因為沒有掛載模板和創建監控項,所以我們接下來嘗試着掛載一下模板,然後再去創建監控項.

我們到agent端裝一個nginx,然後去zabbix的web端找到此模板並掛載.

yum -y install nginx
systemctl start nginx

測試監控主機

接下來我們用瀏覽器或者elinks訪問一下nginx,產生一些數據,然後去zabbix上查看變化

elinks --dump 116.196.83.113

我們以後自定義Key監控項時,先看看最新數據有沒有數據過來,如果數據都不會過來,就別提圖形觸發器報警什麼了.

至此,添加本地主機,遠程主機,創建主機組,掛載模板就已經完了

Zabbix監控項

監控項(Items)簡介

監控項是Zabbix中獲得數據的基礎,沒有監控項,就沒有數據——因為一個主機只有監控項定義了單一的指標或者需要獲得的數據,監控項適用於採集數據的,多個同類的監控項可以定義成一個應用集,如,mysql增刪改查以及每秒鐘的讀表,寫錶速度可以寫成一個Mysql應用集.

對於監控項的示例,需要輸入以下必要的信息

名稱

輸入CPU Load作為值,在列表中和其他地方,都會显示這個值作為監控項名稱.

手動輸入system.cpu.load作為值,這是監控項的一個技術上的名稱,用於識別獲取信息的類型,這個特定值需要是Zabbix Agent預定義值的一種.
https://www.zabbix.com/documentation/3.4/manual/config/items/itemtypes/zabbix_agent # 此網址就是zabbix官網的預定義值.

信息類型

在此處選擇Numeric(float),這個屬性定義了獲得數據的格式
你也需要減少監控項歷史保留的天數,7或者14天,對於數據庫而言,最佳實踐是避免數據庫保留過多的歷史數據.
我們選擇了數據類型后,暫時保持其他選項的默認值.
1> 磁盤容量Units一般為B
2> 網卡流量單位為bps
3> Mysql每秒訪問量qps,例如MySQL每秒select,insert Mysql serlect

點擊添加,新的監控項就出現在監控項列表中了

查看數據

當一個監控項定義完成后,你可能好奇他具體獲取了什麼值,前往監控首頁,點擊最新數據,選擇相應的主機.看數據能不能過來以及是不是自己想要的類型.

圖表

當監控項運行了一段時間后,可以查看可視化圖表,如果沒有可以自己創建一個,下面會有詳細介紹

常用監控項

1.服務器網絡接口進出流量和總流量
    net.if.in[if,<mode>]
    net.if.out[if,<mode>]
    net.if.total[if,<mode>]

2.服務器啟動分區剩餘空間
    vfs.fs.size[fs,<mode>]
    vfs.fs.size[/boot,free]

3.監控虛擬機內存
    vm.memory.size[<mode>]
    vm.memory.size[total
    vm.memory.size[free]
    vm.memory.size[wired]

4.服務器服務狀態
    net.tcp.listen[port]
    net.tcp.port[<ip>,port]
    net.tcp.service[service,<ip>,<port>]
    net.tcp.service.perf[service,<ip>,<port>]

5.服務器進程數量
        proc.num[<name>,<user>,<state>,<cmdine>]
        zabbix_get -s 39.108.140.0 -k proc.num
    121
    zabbix_get -s 39.108.140.0 -k proc.num[,,run]
    3
    zabbix_get -s 39.108.140.0 -k proc.num[,,sleep]
    118

6.服務器CPU狀態(浮點型,無單位)    
    system.cpu.intr
    system.cpu.load[<cpu>,<mode>]
    system.cpu.num
    system.cpu.switches
    system.cpu.util[<cpu>,<type>,<mode>]
    zabbix_get -s 39.108.140.0 -k system.cpu.load[all,avg1]
    0.000000
    zabbix_get -s 39.108.140.0 -k system.cpu.load[,avg5]
    0.010000

7.磁盤IO情況
    vfs.dev.read[device,<type>,<mode>]
    vfs.dev.write[device,<type>,<mode>]
zabbix_get -s 39.108.140.0 -k vfs.dev.read[/dev/vda1]

8.監控文件修改
    vfs.file.chsum[file]        # 如監控/etc/passwd ,/etc/group 文件從而知道是否有新用戶創建
    vfs.file.md5sum[file]
    vfs.file.size[file]        # 通常用來監控日誌
    vfs.fs.size[fs,<mode>]

9.磁盤總和.
監控網卡流量

我們先創建一個應用集,這樣的話之後創建的網卡上傳,下載,總流量不會顯的很亂,都在一個Network應用集裏面,而且能導出成xml文件,放到其他的zabbxi主機上能直接用.

我們此刻做的創建監控項是利用zabbix安裝好自帶的監控項,跟自定義Key差不多,都是寫一個監控腳本然後傳參,每一個鍵值相當於一個監控腳本

接下來我們檢測---> 主機群組裡面去查看下最新數據,我們可以從下圖看到是有數據的

下行寬帶和上行寬帶.

下載就是in,下行寬帶,你發出去的就是out,作為一個服務器來說上行寬帶肯定要高,在家裡就是下行寬帶高,對服務器來說他需要接收很少的數據包,回復很多的數據包,而在家裡我們是發出去一個很小的數據包,返回來整個網頁.

接下來我們再去創建一個網卡輸出流量,然後將他們做成一個圖標,以圖形化展示出來

接下來我們再去監測裏面去查看最新數據,可以養成這個習慣,因為最新數據過來了才是說明當中數據流向沒有問題,如果數據都沒有過來你去創建圖形,圖表說沒有數據,你覺得得等一會,浪費時間影響效率

可以看到,兩個監控項都是有數據的,接下來我們去創建圖形

接下來我們去查看監測 ---> 圖形,選擇相應群組,相應的主機及創建的圖形

這台主機可以裝一個nginx,然後上傳一張大一點圖片到網站根目錄,然後訪問,再查看網絡波動圖.

或者我們直接上傳一個大點的rpm、tar包到其他主機.這樣看着明顯

監控CPU

跟剛才一樣,創建一個CPU應用集,方便管理歸納

接下來我們創建應用集的監控項,cpuintr,cpu中斷數

接下來我們創建一個cpu每隔一分鐘的負載監控項
通過下圖,我們可以看到,每個監控項都是有數據過來的,接下來我們去創建圖形

我們可以看到,數據是可以實時轉換成圖標的,接下來我們去做一個聚合圖形

創建聚合圖形

至此,我們第一個構造函數完成,另外一個構造函數同理,此處就不寫了,直接看結果圖.

創建系統定義好的監控項,跟上面兩個都差不多,多做做自然就會了,如果不習慣使用官方定義好的key,我們可以根據公司環境自己寫腳本自定義key,此章完結.

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

秒懂系列,超詳細Java枚舉教程!!!

所有知識體系文章,GitHub已收錄,歡迎Star!再次感謝,願你早日進入大廠!

GitHub地址: https://github.com/Ziphtracks/JavaLearningmanual

深入理解Java枚舉

一、什麼是枚舉

1.1 什麼是枚舉?

至於枚舉,我們先拿生活中的枚舉來入手,然後再引申Java中的枚舉,其實它們的意義很相似。

談到生活中的枚舉,假如我們在玩擲骰子的遊戲,在我們手中有兩個骰子,要求擲出兩個骰子的點數和必須大於6的概率,那麼在此情此景,我們就需要使用枚舉法一一列舉出骰子點數的所有可能,然後根據列舉出來的可能,求出概率。

可能有的小夥伴發現,這就是數學啊?這就是數學中的概率學和統計學。對,我們的枚舉法就是常用於概率統計中的。

1.2 Java中的枚舉類

Java 5.0引入了枚舉,枚舉限制變量只能是預先設定好的值。使用枚舉可以減少代碼中的 bug,方便很多場景使用。

二、Java枚舉的語法

枚舉類中的聲明

1訪問修辭符 enum 枚舉名 {
2    枚舉成員,
3    枚舉成員,
4    ...
5};

class類中枚舉的聲明

1訪問修飾符 class 類名 {
2    enum 枚舉名 {
3        枚舉成員,
4        枚舉成員,
5        ...
6    }
7}

三、Java枚舉類的使用規則和應用場景

3.1 Java枚舉類的使用規則

至於枚舉你也有所了解了,Java中的枚舉也是一樣的。而Java中枚舉類的使用,也有特定的規則和場景。如果你看了以下的規則不明白的話,沒有關係,繼續向下學你就會明白,因為我在下面都會有講解到這些規則。如下幾個規則:

  • 類的對象是確定的有限個數。
  • 當需要定義一組常量時,建議使用枚舉。
  • 如果枚舉類中只有一個對象,則可以作為單例模式的實現方法。
  • 枚舉類不能被繼承
  • 枚舉類不能被單獨的new創建對象
  • 枚舉類中的枚舉成員是用`,`隔開的,多個枚舉成員之間用`_`隔開
  • 如果枚舉類中的只有一個或多個枚舉成員,其他什麼都沒有,我們在用`,`隔開的同時。最後可以省略`;`結束符。

注意: 如果關於枚舉單例設計模式不太了解的小夥伴可以參考深度學習單例設計模式一文,你肯定會有意想不到收穫,請相信我!

3.2 Java枚舉類的應用場景

根據Java中使用枚舉類的規則,有以下幾種場景適合來使用枚舉類,如下:

  • 星期: Monday(星期一)、Tuesday(星期二)、Wednesday(星期三)、Thursday(星期四)、Firday(星期五)、Saturday(星期六)、Sunday(星期日)
  • 性別: Man(男)、Woman(女)
  • 季節: Spring(春天)、Summer(夏天)、Autumn(秋天)、Winter(冬天)
  • 支付方式: Cash(現金)、WeChatPay(微信)、Alipay(支付寶)、BankCard(銀行卡)、CreditCard(信用卡)
  • 訂單狀態: Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配貨)、Delivered(已發貨)、Return(退貨)、Checked(已確認)
  • 線程狀態: Establish(創建)、Ready(就緒)、Run(運行)、Obstruct(阻塞)、Die(死亡)
  • 等等……

四、枚舉類的基本使用步驟解析

那我們就解釋以下這兩個規則,我們在上述中已經了解了枚舉的作用。Java中枚舉也不例外,也是一一列舉出來方便我們拿出來一個或多個使用。這有點像我們的多選框,我們把需要用到的所有選項內容放在各個多選框後面,當我們在使用的時候只需要勾選自己需要的勾選框即可,這就代表了我們需要被選中多選框後面的內容。

那麼,Java中的枚舉類是如何使用呢?

這裏我們簡單的模擬一個場景,假設你的女朋友十分的喜歡喝點冷飲或熱奶茶之類的飲品,在生活中也有很多像蜜雪冰城等等這種類型的飲品店。當你為女朋友買她愛喝的珍珠奶茶時,服務員會問你,要大杯、中杯還是小杯的。當然,為了滿足女朋友,你通常會選擇大杯。這就意味着店內不允許顧客點規則外的飲品。

注意: 如果你是初學者或是不了解枚舉類的使用,此基本使用不懂沒有關係,請繼續往下看即可!

於是,我用Java代碼來實現一下,上述場景。

首先,創建枚舉類。分別為珍珠奶茶添加大、中、小杯杯型。

 1package com.mylifes1110.java;
2
3/**
4 * @ClassName PearlMilkTea
5 * @Description 為珍珠奶茶添加三個杯型:大、中、小
6 * @Author Ziph
7 * @Date 2020/6/8
8 * @Since 1.8
9 */

10public enum PearlMilkTea {
11    //注意:這裏枚舉類中只有枚舉成員,我在此省略了;結束符
12    SMALL, MEDIUM, LARGE
13}

其次,創建珍珠奶茶對象,再有方法來判斷枚舉類中的大、中、小杯。最後打印女朋友喝哪個杯型的珍珠奶茶!

 1package com.mylifes1110.test;
2
3import com.mylifes1110.java.PearlMilkTea;
4
5/**
6 * @ClassName PearlMilkTeaTest
7 * @Description 為女朋友買哪個杯型的珍珠奶茶(默認大杯)
8 * @Author Ziph
9 * @Date 2020/6/8
10 * @Since 1.8
11 */

12public class PearlMilkTeaTest {
13    public static void main(String[] args) {
14        //創建大杯的珍珠奶茶對象
15        PearlMilkTea pearlMilkTea = PearlMilkTea.LARGE;
16        PearlMilkTeaTest.drinkSize(pearlMilkTea);
17    }
18
19    //判斷為女朋友買哪個杯型的珍珠奶茶
20    public static void drinkSize(PearlMilkTea pearlMilkTea) {
21        if (pearlMilkTea == PearlMilkTea.LARGE) {
22            System.out.println("我為女朋友買了一大杯珍珠奶茶!");
23        } else if (pearlMilkTea == PearlMilkTea.MEDIUM) {
24            System.out.println("我為女朋友買了一中杯珍珠奶茶!");
25        } else {
26            System.out.println("我為女朋友買了一小杯珍珠奶茶!");
27        }
28    }
29}

image-20200608151052517

雖然,我們了解了枚舉類中的基本使用,但是我們在語法中還介紹了一種在類中定義的枚舉。正好,在此也演示一下。如下:

1public class PearlMilkTea {
2    enum DrinkSize {
3        SMALL,
4        MEDIUM, 
5        LARGE
6    }
7}

如果這樣創建就可以在class類中去創建enum枚舉類了。想想前面例子中的代碼其實並不合理,這是為什麼呢?因為我們寫代碼要遵循單一職責原則和見命知意的命名規範。所以,我寫的代碼是在珍珠奶茶的枚舉類中列舉的大、中、小的三種杯型枚舉成員。所以根據規範來講,我們珍珠奶茶中不能擁有杯型相關的枚舉,畢竟我們在生活中的這類飲品店中喝的所有飲品種類都有這三種杯型,因此我們的所有飲品種類中都需要寫一個枚舉類,顯然這是很不合理的。

如果讓它變的更加合理化,我們就細分飲品種類來創建飲品枚舉類和杯型的枚舉類並分別兩兩適用即可。也許有小夥伴會問我為什麼我要說這些合理不合理呢?因為自我感覺這是對枚舉類應用的思想鋪墊,所以你品、你細品!

五、自定義枚舉類

5.1 自定義枚舉類步驟

關於第四章枚舉類的基本使用,也許小夥伴們對枚舉的陌生,而並不知道為什麼這樣去創建枚舉對象。接下來,我來帶你使用常量來自定義枚舉類,試試是不是那個效果。

既然,上述第三章我舉出了這麼多枚舉類的應用場景,那我們挑選一個比較經典的春夏秋冬來實現自定義枚舉類。

首先,我們先創建一個季節類,分別提供屬性、私有構造器、春夏秋冬常量、Getter方法和toString方法,步驟如下:

 1package com.mylifes1110.java;
2
3/**
4 * 自定義季節的枚舉類
5 */

6public class Season {
7    //聲明Season對象的屬性,為private final修飾
8    private final String seasonName;
9
10    //私有化構造器,併為對象賦值
11    private Season(String seasonName) {
12        this.seasonName = seasonName;
13    }
14
15    //提供當前枚舉的多個對象,為public static final修飾
16    public static final Season SPRING = new Season("春天");
17    public static final Season SUMMER = new Season("夏天");
18    public static final Season AUTUMN = new Season("秋天");
19    public static final Season WINTER = new Season("冬天");
20
21    //提供外界通過getter方法來獲取枚舉對象的屬性
22    public String getSeasonName() {
23        return seasonName;
24    }
25
26    //重寫toString方法,以便打印出枚舉結果
27    @Override
28    public String toString() {
29        return "Season{" +
30                "seasonName='" + seasonName + '\'' +
31                '}';
32    }
33}

其次,我們去創建一個測試類,來使用該自定義枚舉類創建對象。由此看來,我們就可以根據類名來句點出常量對象了!

 1package com.mylifes1110.test;
2
3import com.mylifes1110.java.Season;
4
5/**
6 * 測試類
7 */

8public class SeasonTest {
9    public static void main(String[] args) {
10        Season spring = Season.SPRING;
11        System.out.println(spring);
12    }
13}

最後打印結果是春天的對象,由於我們覆蓋了toString方法,即可見對象內的內容。

image-20200608160220000

5.2 使用帶有參枚舉類

如果你在第三章時Java枚舉類的基本使用不明白,估計看完自定義枚舉類也了解的大差不差了。但是你有沒有發現我們自定義枚舉類是使用的有參數的對象呢?那我們怎樣使用真正的枚舉類來實現有參數的枚舉類呢?繼續看吧那就!

在這裏我將自定義枚舉類改裝了一下,改裝成了enum枚舉類實現的使用有參對象。如下:

 1package com.mylifes1110.java;
2
3public enum Season {
4    SPRING("春天"),
5    SUMMER("夏天"),
6    AUTUMN("秋天"),
7    WINTER("冬天");
8
9    private final String seasonName;
10
11    Season1(String seasonName) {
12        this.seasonName = seasonName;
13    }
14
15    public String getSeasonName() {
16        return seasonName;
17    }
18}

不知道你有沒有發現少了點什麼,少的部分其實就是我們創建常量對象的部分,而且在這個枚舉類中我也沒有去重寫toString方法,至於為什麼,下面就告訴你。

注意: 枚舉對象之間用,隔開!

其次,去創建了該枚舉類的測試類,我們測試以下,並看一下沒有重寫toString方法打印出來的結果。

 1package com.mylifes1110.test;
2
3import com.mylifes1110.java.Season;
4
5public class Seaso1Test {
6    public static void main(String[] args) {
7        Season1 spring = Season.SPRING;
8        System.out.println(spring);                     //SPRING
9        System.out.println(spring.getSeasonName());     //春天
10    }
11}

這裏我將打印的結果放在了打印語句後面的註釋中。我們發現沒有重寫toString方法竟然打印出來的是SPRING,這是為什麼呢?這應該從我們的繼承關係中分析,如果繼承的是基類Object的話,沒有重寫toString方法會打印對象地址。那麼我們就可以斷定,enum枚舉類的父類不是Object。那它的父類是誰呢?我們可以藉助來對象來獲取其父類,如下:

1System.out.println(Season.class.getSuperclass());        //class java.lang.Enum

同樣,答案放在了代碼後面的註釋中。我們發現它默認繼承的是Enum類。那麼,我們稍後就來就看看這個類中到底寫了些什麼方法。

六、Enum常用方法的使用

6.1 Enum中的所有方法

關於Enum類中的所有方法我以表格的方式列舉出來!

返回值 方法 描述
String name() 獲取枚舉成員的名稱
static T valueOf(Class<T> enumType, String name) 獲取指定枚舉成員名稱和類型的枚舉成員
String[] values() 獲取枚舉成員的所有值
int compareTo(E o) 比較此枚舉與指定對象的順序
int hashCode() 獲取枚舉成員的哈希值
int ordinal() 獲取枚舉成員的序數(第一個枚舉成員位置為0)
String toString() 返回枚舉成員名稱
Class<E> getDeclaringClass() 獲取枚舉成員的類對象

6.2 name和toString

關於name方法和toString方法,其實很簡單。name()就是根據枚舉成員來獲取該枚舉成員的字符串名稱。而同String方法也是用來獲取枚舉成員的字符串名稱。雖然作用都是相同的,但是name方法是用final修飾的不能被重寫,而toString是可以被重寫的。這裏我們還使用季節的案例來演示,打印結果並放在了代碼後面的註釋中,如下:

1System.out.println(Season.SUMMER.name());            //SUMMER
2System.out.println(Season.SUMMER.toString());        //SUMMER

6.3 valueOf

此方法的作用是傳入一個字符串,然後將它轉換成對應的枚舉成員。這裏傳入的字符串必須與定義的枚舉成員的名稱一致,嚴格區分大小寫。如果傳入的字符串並沒有找到其對應的枚舉成員對象,就會拋出異常。如下:

1System.out.println(Season.valueOf("WINTER"));            //WINTER
2System.out.println(Season.valueOf("WIN"));                //java.lang.IllegalArgumentException

image-20200608173858862

6.4 values

values方法的名字中就帶有一個s,再加上它的返回值是一個字符串數組。所以我們就可以得出它的作用是獲取枚舉成員的所有值,這些值並以數組的形式存儲。

1Season[] seasons = Season.values();
2for (Season season : seasons) {
3    System.out.print(season + " ");
4}

結果為:

1SPRING SUMMER AUTUMN WINTER 

6.5 ordinal

該方法是獲取枚舉成員的序數,其第一個枚舉成員位置為0。其實,為了好理解的話,可以把它看作數組中的索引。數組中的第一個元素位置同樣也是從0開始。那我們打印一下,看看結果如何,如下:

1//獲取指定枚舉成員的次序
2System.out.println(Season.SUMMER.ordinal());
3
4//獲取所有成員的次序
5Season[] seasons = Season.values();
6for (Season s : seasons) {
7    System.out.println(s + " -> " + s.ordinal());
8}

結果為:

image-20200608175529079

其源碼就是返回了一個從0開始int類型的值,從源碼中也可以看出最大值是int取值範圍的最大值。如下:

image-20200608180839568

6.6 compareTo

compareTo方法相信我們已經是很熟悉了。其作用就是用來比較的。但是在枚舉類中它比較的是什麼呢?實際上compareTo方法比較的是兩個枚舉成員的次序數,並返回次序相減后的結果。

首先,我們要知道SUMMER的次序數為1,WINTER的次序數為3。當使用前者比較後者,打印的結果是前者與後者相減后的差值,即1-3=-2

1System.out.println(Season.SUMMER.compareTo(Season.WINTER));            //-2

它的源碼是怎麼做的呢?那我們進入查看一下。

其中,前面的操作都是在判斷比較的雙方是否是一個枚舉類,如果不是的話就拋出異常。如果為枚舉類的話,就直接將次序數做了相減操作並返回。

image-20200608180532795

七、Java枚舉的高級特性

7.1 常量

我們知道,常量是用public static final修飾的。1.5之後有了枚舉,我們就可以把相關的常量放在一個枚舉容器中,而且使用枚舉的好處還在於枚舉為我們提供了很多便捷的的方法。

示例:

1public enum Season {
2    SPRING, SUMMER, AUTUMN, WINTER
3}

7.2 switch語句

你了解的switch語句都支持哪種類型呢?我這裏說一下,switch語句支持的類型有如下幾種:

  • 基本數據類型: byte、short、char、int
  • 包裝數據類型: Byte、Short、Character、Integer
  • 枚舉類型: Enum
  • 字符串類型: String(jdk7+ 開始支持)

具體枚舉類與switch語句的使用是如何實現呢?枚舉又是如何為switch語句提供便利的呢?來看一下吧。

 1package com.mylifes1110.java;
2
3public class WeekTest {
4    public static void main(String[] args) {
5        Week week = Week.MONDAY;
6        switch (week) {
7            case MONDAY:
8                System.out.println("星期一");
9                break;
10            case TUESDAY:
11                System.out.println("星期二");
12                break;
13            case WEDNESDAY:
14                System.out.println("星期三");
15                break;
16            case THURSDAY:
17                System.out.println("星期四");
18                break;
19            case FRIDAY:
20                System.out.println("星期五");
21                break;
22            case SATURDAY:
23                System.out.println("星期六");
24                break;
25            case SUNDAY:
26                System.out.println("星期日");
27                break;
28            default:
29                System.out.println("null");
30        }
31    }
32}
33
34enum Week {
35    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
36}

7.3 枚舉中定義多個參數與方法

有參枚舉在5.2中我已經做了詳細說明,我們在定義枚舉時不只是可以定義多個參數,還可以定義其他的普通方法來使用,而關於普通方法的使用是根據場景的,這裏我就不再做過多的贅述了。

 1package com.mylifes1110.java;
2
3public enum Season {
4    SPRING("春天"),
5    SUMMER("夏天"),
6    AUTUMN("秋天"),
7    WINTER("冬天");
8
9    private final String seasonName;
10
11    public static String getName(int index) {  
12        for (Season s : Season.values()) {  
13            if (c.getIndex() == index) {  
14                return c.name;  
15            }  
16        }  
17        return null;  
18    }
19
20    Season1(String seasonName) {
21        this.seasonName = seasonName;
22    }
23
24    public String getSeasonName() {
25        return seasonName;
26    }
27}

7.4 枚舉類實現接口

雖然枚舉類不能繼承,但是可以實現接口。以下是一個實現過程。

首先,創建一個接口。

1package com.mylifes1110.inter;
2
3public interface Show {
4    void show();
5}

其次,讓我們的四季枚舉類實現該接口並重寫方法。

 1package com.mylifes1110.java;
2
3import com.mylifes1110.inter.Show;
4
5public enum Season implements Show {
6    SPRING("春天"),
7    SUMMER("夏天"),
8    AUTUMN("秋天"),
9    WINTER("冬天");
10
11    private final String seasonName;
12
13    Season1(String seasonName) {
14        this.seasonName = seasonName;
15    }
16
17    public String getSeasonName() {
18        return seasonName;
19    }
20
21    @Override
22    public void show() {
23        System.out.println("嚮往四季如春");
24    }
25}

最後,當我們使用每一個枚舉類都可以調用show方法,而打印的結果也都是“嚮往四季如春”

1Season.WINTER.show();                //嚮往四季如春

聰明的你我相信發現了這個缺點,我們不管使用哪一個枚舉成員時,調用的show方法都是同一個。所以,我們在實現接口后,可以這樣重寫方法,如下:

 1package com.mylifes1110.java;
2
3import com.mylifes1110.inter.Show;
4
5public enum Season1 implements Show {
6    SPRING("春天") {
7        @Override
8        public void show() {
9            System.out.println("春天是個踏青的季節");
10        }
11    },
12    SUMMER("夏天") {
13        @Override
14        public void show() {
15            System.out.println("夏天是個炎熱的季節,我要吃冰棍");
16        }
17    },
18    AUTUMN("秋天") {
19        @Override
20        public void show() {
21            System.out.println("秋天還算是涼爽");
22        }
23    },
24    WINTER("冬天") {
25        @Override
26        public void show() {
27            System.out.println("冬天的雪還不錯,就是有點冷");
28        }
29    };
30
31    private final String seasonName;
32
33    Season1(String seasonName) {
34        this.seasonName = seasonName;
35    }
36
37    public String getSeasonName() {
38        return seasonName;
39    }
40}

我們在枚舉成員的後面加了{},而重寫的方法可以寫在各個枚舉成員中,這樣就接觸了上述所有的那個限制。這下,我們使用哪個枚舉成員對象調用show方法都是不同的。是不是非常NICE?

7.5 使用接口對枚舉分類

使用接口對枚舉分類,我們需要創建一個接口容器,裏面存放着此接口容器所存放的多個枚舉類,然後將各個枚舉類實現此接口,以這樣的方式可實現對枚舉分類。代碼如下,打印結果放在了代碼後面的註釋中:

 1package com.mylifes1110.inter;
2
3public interface Weeks {
4    //工作日
5    enum WorkingDay implements Weeks {
6        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY
7    }
8
9    //雙休日
10    enum Weekend implements Weeks {
11        SATURDAY, SUNDAY
12    }
13}
14
15class WeeksTest {
16    public static void main(String[] args) {
17        System.out.print("雙休日:");
18        for (Weeks.Weekend weekend : Weeks.Weekend.values()) {
19            System.out.print(weekend + " ");        //雙休日:SATURDAY SUNDAY
20        }
21
22        //換行
23        System.out.println();
24
25        System.out.print("工作日:");
26        for (Weeks.WorkingDay workingDay : Weeks.WorkingDay.values()) {
27            System.out.print(workingDay + " ");     //工作日:MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY
28        }
29
30        //換行
31        System.out.println();
32
33        Weeks.WorkingDay friday = Weeks.WorkingDay.FRIDAY;
34        System.out.println("星期五:" + friday);      //星期五:FRIDAY
35    }
36}

image-20200608194649335

八 枚舉類集合

8.1 EnumSet集合

關於Set集合,我們知道其集合中的元素是不重複的。其中的方法有以下幾種:

返回值 方法 描述
static EnumSet<E> allOf(Class<E> elementType) 創建一個包含指定元素類型的所有元素的枚舉 set。
EnumSet<E> clone() 返回一個set集合。
static EnumSet<E> complementOf(EnumSet<E> s) 創建一個其元素類型與指定枚舉set相同的set集合(新集合中包含原集合所不包含的枚舉成員)
static EnumSet<E> copyOf(EnumSet<E> s) 創建一個其元素類型與指定枚舉 set 相同的枚舉 set集合(新集合中包含與原集合相同的枚舉成員)
static EnumSet<E> copyOf(Collection<E> s) 創建一個從指定 collection 初始化的枚舉 set
static EnumSet<E> noneOf(Class<E> elementType) 創建一個具有指定元素類型的空枚舉 set
static EnumSet<E> range(E from, E to) 創建一個最初包含由兩個指定端點所定義範圍內的所有元素的枚舉 set。
static EnumSet<E> of 創建一個最初包含指定元素的枚舉 set。注意:可以指定多個元素,所以在這裏我沒有列舉參數
8.1.1 allOf

allOf方法需要我們傳入一個枚舉的類對象,它會根據傳入的枚舉類對象生成一個具有該類對象枚舉成員的Set集合。

1//創建一個包含Week所有枚舉元素的Set集合
2EnumSet<Week> weeks = EnumSet.allOf(Week.class);
3System.out.println(weeks);              //[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
4
5//打印Set集合中的元素
6for (Week week1 : weeks) {
7    System.out.print(week1 + " ");      //MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY SATURDAY SUNDAY
8}
8.1.2 clone

clone方法與直接打印枚舉的Set集合結果相同!

1//返回一個Set集合
2System.out.println(weeks.clone());      //[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
8.1.3 range

上面詳細講過枚舉是有序數的,而且枚舉類中的枚舉成員是秉承着從左向右的順序。所以我們可以使用range方法來創建指定枚舉成員端點的Set集合,也就是說我們需要傳入枚舉成員的起始與結束去創建一個該擁有該範圍枚舉成員的Set集合。如下:

1//創建一個最初包含由兩個指定端點所定義範圍內的所有元素的枚舉 set。
2System.out.println(EnumSet.range(Week.MONDAY, Week.FRIDAY));        //[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
8.1.4 complementOf

該方法有點特殊,它根據EnumSet去創建一個新Set集合。而新Set集合中枚舉成員相當於舊Set集合中枚舉成員的取反。

我們用場景來模擬一下,當前Week枚舉類中有星期一到星期日7個枚舉成員。我們使用range方法創建一個從星期一到星期五的Set集合(s1),隨後我在將使用complementOf方法根據s1生成新的Set集合(s2),最後打印s2查看集合中的元素就只有星期六和星期日。

注意: 如果我們的舊Set集合佔據了枚舉類中的所有枚舉成員,在使用complementOf方法生成的新Set集合,新集合中的元素打印後為空Set,即[]

1//創建一個其元素類型與指定枚舉set相同的set集合(新集合中包含原集合所不包含的枚舉成員)
2EnumSet<Week> weeks1 = EnumSet.complementOf(weeks);
3System.out.println(weeks1);             //[]
4
5EnumSet<Week> range = EnumSet.range(Week.MONDAY, Week.FRIDAY);
6EnumSet<Week> weeks3 = EnumSet.complementOf(range);
7System.out.println(weeks3);                //[SATURDAY, SUNDAY]
8.1.5 copyOf

copyOf方法與complementOf相反,它創建一個新Set集合。而新Set集合中的枚舉成員與舊Set集合中的枚舉成員相同,這相當於就是Copy(複製功能)。如果你理解了complementOf方法,這個方法對你來說也是沒有挑戰。以下我使用copyOf方法複製了一份weeks,其枚舉成員一個不少。

注意: copyOf方法還有一個可以複製connection集合來創建Set集合,其connection集合中必須存儲的是枚舉成員。

1//創建一個其元素類型與指定枚舉 set 相同的枚舉 set集合(新集合中包含與原集合相同的枚舉成員)
2EnumSet<Week> weeks2 = EnumSet.copyOf(weeks);
3System.out.println(weeks2);             //[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
1//複製存儲枚舉成員的HashSet集合
2Set set = new HashSet();
3set.add(Week.MONDAY);
4EnumSet set1 = EnumSet.copyOf(set);
5System.out.println(set1);        //[MONDAY]
8.1.6 of

of方法為我們提供了選擇性的便利,我們可以挑選任意枚舉成員成為Set集合的元素。

1//創建一個最初包含指定元素的枚舉 set。
2System.out.println(EnumSet.of(Week.MONDAY,Week.FRIDAY));            //[MONDAY, FRIDAY]
8.1.7 noneOf

傳入一個枚舉的類對象去創建一個空Set集合

1EnumSet<Week> noneOf = EnumSet.noneOf(Week.class);
2System.out.println(noneOf);                     //[]

8.2 EnumMap集合

8.2.1 EnumMap集合的方法列表

關於Map集合,我們知道它是由鍵和值組成。EnumMap集合與HashMap集合的效率比較來說,EnumMap的效率高些。

關於EnumMap集合的使用與HashMap是一致的,沒有什麼特殊的。至於EnumMap集合的方法,我這裏列舉一下。

返回值 方法 描述
void clear() 移除所有映射關係。
EnumMap clone() 返回EnumMap集合。
boolean containsKey(Object key) 包含此鍵,則返回true
boolean containsValue(Object value) 包含一個或多個鍵映射到的該指定值,則返回true
Set > entrySet() 返回映射鍵值關係的Set集合
boolean equals(Object o) 比較對象與映射的相等關係
V get(Object key) 獲取指定鍵映射的值,如果沒有,返回null
Set<K> keySet() 返回所有鍵的Set集合
V put(K key, V value) 將指定鍵值存儲在EnumMap集合中
void putAll(Map m) 將所有鍵值對存儲在集合中
V remove(Object key) 如果存在映射關係,則移除該映射關係
int size() 返回存在映射關係的數量
Collection<V> values() 返回此映射中所包含值的 Collection集合
8.2.2 EnumMap集合的基本使用

由於EnumMap集合與HashMap集合基本相似,這裏我就演示一下基本使用與HashMap不同的地方。

EnumMap集合是我們new出來的對象,創建出來的對象需要傳入一個枚舉的類對象,才返回一個Map集合。Map集合是鍵值對形式存儲,所以我們在寫EnumMap集合的泛型時,根據需求來寫,如果需要鍵是某枚舉類型,我們泛型就寫它。如果有枚舉類是值的要求,那就泛型中的值寫枚舉類。鍵值對都要求是枚舉那也是OK的,我們寫泛型時都寫需求的枚舉類即可。除了創建對象和存儲對象需要指定枚舉類外,其他的與HashMap基本相同。

如下,我在創建EnumMap集合時執行的Week枚舉類的類對象,泛型的鍵寫的是Week枚舉類,值寫的Integer,這就意味着我們在put(存儲鍵值對)的時候,鍵需要存儲Week枚舉類中的枚舉成員,值需要存儲Integer數值。

1EnumMap<Week, Integer> map = new EnumMap<>(Week.class);
2map.put(Week.MONDAY, 1);
3map.put(Week.THURSDAY, 4);
4System.out.println(map);            //{MONDAY=1, THURSDAY=4}

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※回頭車貨運收費標準

台中搬家公司費用怎麼算?

分類
發燒車訊

20萬大空間/高顏值轎車 媳婦喜歡丈母娘滿意

國內消費者最關心的乘坐空間,2805mm軸距的起亞表現的很好,以175cm的體驗者為例,前後排可活的一拳的頭部空間,後排頭部空間超兩拳,提供了1。6T/2。0L/2。0T三種排量可供選擇,搭配6擋手自一體或者7擋雙離合變速箱,選擇性很強。

國人買東西都有一個共同的特點

就是大大大大大大大大大大大大

手機必須上大屏的

鑽戒要買大的

那車子當然是越大越好

前段時間一條火爆的廣告刷爆了朋友圈

致25歲還一無是處的本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

能兼顧到家用還開着爽 這幾款車只需要10萬起!

6L、1。8L以及2。0L三款發動機,傳動系統配備5擋手動以及大家最熟悉的CVT變速箱。CVT變速箱動力輸出十分平順,也非常適合在日常城市道路駕馭。硬朗的底盤以及韌性不錯的懸挂,加上ASC車身穩定系統,能給駕駛者足夠的信心。一汽-大眾-速騰指導價:13。

前言

對於80後部分朋友已成家立室,上有老下有小,買車必須要考慮到家用,而心中存在着一份激情,開着也能讓自己爽。所以對於這部分朋友來說,選擇操控好,還能兼顧到家用,也是他們首要的目標。

長安馬自達-馬自達3 Axela昂克賽拉

指導價:11.49-15.99萬

昂克賽拉沿用了馬自達最新“魂動”家族化設計, 前臉造型如同獵豹,視覺感受更加富有攻擊性以及活力的風格,尾部設計較為簡潔,整體設計風格時尚年輕。

內飾正是昂克賽拉的創新所在,雖然簡潔,但整體內飾氛圍更具運動感,車廂做工用料方面,比較厚道!

昂克賽拉更有兩個版本可選,三廂以及兩廂車型,空間方面,能夠滿足日常出行,三廂版/兩廂版行李箱的容積為419L、350L,長途駕駛三廂版實用性更高。

昂克賽拉搭載了1.5L以及2.0L兩款自然吸氣發動機,也是馬自達最新“創馳藍天”發動機,我相信大家也非常了解這款發動機,其優點就是能擁有相對低的油耗還能兼顧到充沛的動力輸出,傳動系統配備了6擋手動、6擋手自一體變速箱。底盤的質感高級,從容紮實,懸挂的調校偏運動,能有很好的操控性還能兼顧舒適性。

東南汽車-翼神

指導價:9.58-13.98萬

翼神外觀採用了三菱經典的鯊魚嘴仿生學造型設計,黑色的大嘴結合成一體,尾部設計較為驚艷,有層次感。翼神整體的設計更加霸氣有力!

翼神內飾主要以黑色為主,中控設計布局簡潔以及在物理按鍵上很到位,整體車廂氛圍很有戰鬥感。

翼神車身長寬高為4570*1760*1490mm,軸距為2635mm,空間表現,寬敞也能輕鬆翹個二郎腿,座椅填充物軟硬適中,包裹性也很足。

動力方面,翼神搭載了1.6L、1.8L以及2.0L三款發動機,傳動系統配備5擋手動以及大家最熟悉的CVT變速箱。CVT變速箱動力輸出十分平順,也非常適合在日常城市道路駕馭。硬朗的底盤以及韌性不錯的懸挂,加上ASC車身穩定系統,能給駕駛者足夠的信心。

一汽-大眾-速騰

指導價:13.18-21.88萬

速騰外觀設計依然還是很大眾,家族式設計並沒有太多的亮點,圓滑的車身造型以及流暢動感線條,速騰依然不失時尚感。

內飾設計依然還是大眾風格,雖然是大眾風格,但其整體設計風格很耐看,甚至會有那麼一絲高級感,做工用料上採用了些許硬質材料,視覺效果並不差,依然簡潔。

速騰車身長寬高為4655*1780*1453mm,軸距為2651mm,空間表現,可以說是速騰最引以為傲的地方,蹺二郎腿毫無壓力。

動力方面,速騰搭載了1.2T、1.4T.1.6T以及2.0T四款發動機,可供消費者選擇較多,傳動系統方面配備5擋手動、6擋手自一體、7擋雙離合變速箱,購買雙離合車型的消費者也無需擔心其的耐用性以及平順性,大眾也做了特別多的優化。底盤依然是大眾一貫的風格調校,紮實,速騰懸挂調校也偏向舒適。

全文總結

整體來說,這三款車都有運動基因的底子,能兼顧到家用以及在操控性方面也不錯,產品力也很強!加上現在優惠幅度也比較大,也值得入手。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

網頁設計最專業,超強功能平台可客製化

※別再煩惱如何寫文案,掌握八大原則!

分類
發燒車訊

都是六萬多,這三個漂亮的、配置高的SUV哪個是你的菜

如果車長真的達到4420mm,軸距也不會只有2510mm。雖然都是“3”,但是瑞虎3和DX3是截然不同的兩種設計風格。DX3走的完全是時尚路線,瑞虎3則成熟穩重許多。沒有改款前的內飾看起來比較過時,如今2016款的造型還說的過去。變得更加有層次感,看起來也年輕許多了。

六萬多的裸車,想買一台不錯的車子,只有考慮自主品牌的車子了,因為這個價位合資車幾乎沒有什麼好的選擇,要麼就是丑到家,要麼就是配置很寒磣。

但是如果選擇自主品牌的話,那麼路一下子就會變得特別寬,可以有很多不錯的選擇,尤其是對於自主品牌密集布局的SUV領域。所以小編選出了下面三台車子,看看哪台是你的菜。

東南汽車-東南DX3

指導價:6.79-10.09萬

DX3的車身尺寸為4354*1840*1670mm,軸距為2610mm,因為DX7的外觀獲得了不少消費者的讚譽,所以這次DX3的設計還是由賓尼法利納設計中心完成。不得不說,DX3的外觀確實比較驚艷,犀利的前臉,凌厲的腰線,懸浮式車頂設計,這一切都讓DX3看起來非常時尚。

內飾和外觀一樣,不僅好看,更重要的是原創性也非常高。據DX3的設計師設計透露,DX3的內飾設計風格為“永恆之美”,反正我也不知道什麼叫做永恆之美,只知道看起來挺時尚的。不過內飾的配色也很豐富,可以提供棕+黑、紅+黑兩種配色。

DX3的空間表現很好,後排空間較為寬裕,在同級別中處於中等偏上的水準。它的動力系統為1.5L 120馬力+5擋手動,1.5T 156馬力+CVT變速箱。其實如果預算比較吃緊的話,1.5L的發動機就可以滿足日常家用了,即使是最低配,也會配備車身穩定系統、胎壓監測、上坡輔助、陡坡緩降、后視鏡電動調節。安全配置和實用配置都有,所以對於資金不充足的消費者來說,低配就夠了。

奇瑞汽車-瑞虎3

指導價:6.89-9.29萬

瑞虎3的車身尺寸為4420*1760*1670mm,軸距為2510mm。不要被數據蒙蔽了,其實虎3的車長是加上那個外掛的備胎了。如果車長真的達到4420mm,軸距也不會只有2510mm。雖然都是“3”,但是瑞虎3和DX3是截然不同的兩種設計風格。DX3走的完全是時尚路線,瑞虎3則成熟穩重許多。

沒有改款前的內飾看起來比較過時,如今2016款的造型還說的過去。變得更加有層次感,看起來也年輕許多了。尤其是中控大屏,可以立馬增加中控的檔次感。

瑞虎3的動力系統為1.6L 126馬力+5擋手動/CVT變速箱。老款的瑞虎3沒有車身穩定系統,遭到了吐槽,現在瑞虎3終於標配了ESp,性價比提升了很多。同時奇瑞的瑞虎系列也有了一定的歷史了,質量還是挺讓人放心的,不管是手動擋還是CVT車型,質量方面都有着比較穩定的表現。

凱翼汽車-凱翼X3

指導價:6.66-9.69萬

凱翼X3的車身尺寸為4335*1796*1665mm,軸距為2530mm。凱翼X3也是小型SUV,車身尺寸在同級別並不佔什麼優勢,雖然凱翼X3的外觀經過了重新的設計,變得年輕時尚許多,但是在車身側面和車尾,仍有一些瑞虎3的影子。

中控內飾極其簡約,沒有什麼複雜的線條做映襯,可以看出來設計師是盡量追求簡介的設計風格,中控台的按鍵數目屈指可數。不過中控台的用料很不錯。部分區域採用了搪塑工藝,手感較好。

凱翼X3的動力系統為1.6L 126馬力+5擋手動/CVT,動力表現和瑞虎3較為相近。其實就配置而言,凱翼X3的配置要低於DX3和瑞虎3的。不過凱翼X3也有着自己的特色,比如更加討人喜歡的外觀。

總結:相對來說DX3和瑞虎3的性價比都很高,配置很實在,DX3的外觀看起來更年輕,瑞虎3則更適合比較穩重的消費者,至於凱翼X3,如果你覺得瑞虎3的外觀有點過於成熟的話,那麼可以看看凱翼X3,雖然它的性價比沒有瑞虎3那麼高。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

※回頭車貨運收費標準

分類
發燒車訊

要是明年別克將這車國產 告訴我你買不買賬?

基於輕量化平台打造后,新君威不僅車重減輕,車身和軸距都相應加長了55mm和92mm,加上車尾採用的溜背設計,未來新君威的後排空間必定提升不少。新君威在未來也有望搭載1。5T/2。0T發動機,其中1。5T車型極有可能搭載7速雙離合變速箱。

相信大家對別克君威都不陌生,這輛中級轎車從以前主打舒適穩重的風格向後期年輕化的運動范轉變。而最近它的姊妹車歐寶Insignia實車也亮相了,新一代君威的外觀基本會與其相同。

可能很多人已經淡忘了歐寶這個品牌,實際上作為通用旗下的子品牌,歐寶、別克和沃克斯豪爾(Vaxuhall)都存在着換標車型。同一款車彼此換上不同的logo,在不同的國家地區發售。

而歐寶Insignia所對應的車型分別是別克的君威和霍頓的Commodore(沒錯,霍頓同樣是通用的子品牌)。這輛未來的新君威走了略帶豪華的運動風格,不論是前臉還是腰線的設計都顯得非常運動,只不過到時進來國產的時候,外觀肯定會有一些調整,而這款Insignia則有可能在明年日內瓦車展發布,而新君威則有望在明年國產。

基於輕量化平台打造后,新君威不僅車重減輕,車身和軸距都相應加長了55mm和92mm,加上車尾採用的溜背設計,未來新君威的後排空間必定提升不少。

新君威在未來也有望搭載1.5T/2.0T發動機,其中1.5T車型極有可能搭載7速雙離合變速箱。只不過在國內眾多消費者對於別克的變速箱印象都很一般,未來國產車型還是得要多花點心思好好調整下了。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

分類
發燒車訊

看懂就是省錢!賣一輛車4S店可以賺你多少錢?

汽車廠家整車利潤據網上資料显示,在國內,主流汽車廠家的整車利潤一般在10%左右,這個数字可能大家看了都不相信,因為在中國,很多人都認為汽車廠家是超賺錢的,當然,這10%其實不包括其中零散配件的價格利潤,現在是全球化時代,許多汽車廠家都會和許多知名的汽車配件,發動機和變速箱公司合作,其中這些核心部件在買賣當中究竟是有着多少利潤可言,關於這個問題,汽車廠家則以汽車技術不外露為理由,並沒有公開給我們廣大消費者。

相信大家在買車的時候都相信一個道理,絕大多數的汽車,具有一個官方的廠商指導價,但一般我們在4S店裡購買的話,在這個基礎價格上,還能有着幾千上萬的優惠幅度,放眼全國,也存在着30萬的車降價7萬,20萬的車降價3萬元的情況,降價幅度如此之大,不由得讓人懷疑,這樣子4S店為何還能賺到錢?而且一輛車的成本是有多低?

許多消費者在買車的時候,似乎不會去關注這個問題,認為只要車合適價格滿意,這就達成了購買的協議,但一輛車為何能夠降價這麼多,例如花了20萬元買了台雅閣,其中有多少錢真正屬於這輛車的?繼續往下看。

買一輛車的錢,可以分為5個部分,分別是:稅費,技術轉讓費,汽車廠家整車利潤,4S店利潤,轎車成本。

稅費

廠家需要繳納的稅費:消費稅,增值稅

消費者需要繳納是稅費:購置稅

從網上相關數據的查閱中發現,每一輛車需要繳納的稅收佔著汽車價格的23%左右,消費者購車后繳納10%的車輛購置稅(雖然精準的購置稅算法為發票÷1.17×10%,但我們姑且算個大概),所以這麼一來,一輛車從生產到消費者手中需要繳納的稅收就約佔汽車價格的33%左右。

除此之外,汽車廠家還要繳納教育附加的地方教育費,總費用合計在5%左右,這麼加起來,一輛車從生產出來直到消費者手中,其中所需要的稅費總的加起來就高達38%,稅費真的讓人防不勝防。

技術轉讓費

對於我們國內一些汽車合資品牌來說,每生產出一台合資車,都需要給外方支付10%的技術轉讓費,比如你買了一台20萬的轎車,其中技術轉讓費就高達2萬元,哦我的天,怪不得在同級轎車中,如果購買相同配置的話,合資車總要貴那麼幾萬塊,如此高的技術轉讓費,會導致國外一些價格較低的車型,很難引入國內,畢竟引入后,在價格戰上,始終斗不過寶駿310,或者長安奔奔的。

汽車廠家整車利潤

據網上資料显示,在國內,主流汽車廠家的整車利潤一般在10%左右,這個数字可能大家看了都不相信,因為在中國,很多人都認為汽車廠家是超賺錢的,當然,這10%其實不包括其中零散配件的價格利潤,現在是全球化時代,許多汽車廠家都會和許多知名的汽車配件,發動機和變速箱公司合作,其中這些核心部件在買賣當中究竟是有着多少利潤可言,關於這個問題,汽車廠家則以汽車技術不外露為理由,並沒有公開給我們廣大消費者。

4S店利潤

終於說到大家非常關心的問題了,據數據統計,在30萬元以下的車型,銷售一輛車的平均利潤為5%左右,賣一輛30萬的車4S店的利潤才1.5萬塊?怎麼可能!單純賣車賺的錢肯定不夠一整個4S店的開支(地租,人工費,設備耗損費,水電費),而且據了解,現在很多4S店由於資金不足,所以都把現車的合格證和主鑰匙抵押給銀行去貸款,等到這輛車賣出去了再去銀行贖回來,但也出現過賣車后無力贖回車輛的合格證的情況,導致車主無法按時上牌,所以這麼來說,許多4S店其實單車賣出來賺的利潤不多,主要還是靠拚命地賣車,在年底拿廠家返點。

那麼賣一輛車,4S店如何通過其他方式賺到你的錢呢?

保險費:對於一般人來說,在店裡買車,大多數人還是會在這裏把保險順便買了,方便以後出險的時候,這裏也有保險專員可以幫你跟進車輛的情況,更加方便,服務到位,大部分4S店都要求客戶買車的時候,保險也一定要在本店購買,更離譜的還要求必須買一定金額的保險,買個全險六七千,其中的利潤還是有一兩千的。

上牌費:其實上牌費也就是500塊錢而已,但是在一二線城市例如廣州,上個牌需要收取3000塊錢手續費,且一定要在本店執行,客戶不能夠提裸車,而如果需要上比較偏遠城市的外地牌,手續費去到6000元也有。

精品加裝費:對於一些熱銷車型來說,4S店肯定要大賺一筆,加價加價繼續加價,但是直接加價會影響市場,被稅局查到也有影響,所以都會利用一些其他方式來執行這種加價的“套路”,很簡單,通過一大堆高昂的精品加裝,價值就能夠上到一兩萬元,確實是炒高了價的“淘寶貨”啊。

售後維修服務:雖然說4S店通過正兒八經的方式確實掙不到什麼錢,但是售後服務這個大蛋糕,確實一直讓他們吃得挺爽的,一般新車都規定3個月或者5000公里後進行首保,但是4S店一般會在3000公里的時候,就提醒客戶該過來首保了,雖然說免費,但送的就是服務,且在質保期內如果在其他地方維修出現什麼其他問題,4S店也不承認,所以很多人在質保期內,還是會乖乖地去4S店裡做一下保養,收費可都是外面的好幾倍。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!