【大廠面試06期】談一談你對Redis持久化的理解?

Redis持久化是面試中經常會問到的問題,這裏主要通過對以下幾個問題進行分析,幫助大家了解Redis持久化的實現原理。

1.Redis持久化是什麼?

2.Redis持久化有哪些策略?各自的實現原理是怎麼樣的?

3.Redis的數據恢復策略是怎麼樣的?

4.Redis持久化策略該如何進行選擇?

1.Redis持久化是什麼?

因為Redis是一個內存數據庫,數據保存在內存中,一旦發生關機或者重啟,內存中的數據都會丟失,所以為了能夠重啟時恢複數據,Redis提供了持久化的機制,正常運行期間根據策略生成持久化文件。在機器重啟后,可以根據根據持久化文件恢復內存中的數據。Redis還為我們提供了持久化的機制。(雖然有主從同步,主機掛掉之後,可以讓從節點成為主節點,但是如果整個機房都發生停電,那麼主節點和從節點內存中的數據都會丟失,所以這也是持久化存在的意義。)

2.Redis持久化有哪些策略?

Redis持久化的策略主要有AOF持久化,RDB持久化,混合持久化。這是我自己總結的一個圖:

AOF持久化

執行流程

AOF持久化主要是Redis在修改相關的命令后,將命令添加到aof_buf緩存區的末尾,然後在每次事件循環結束時,

根據appendfsync的配置:

  • appendfsync = always 每條修改命令都會更新到磁盤上的AOF文件, 最多只會丟失當前正在寫入的命令

  • appendfsync = everysec 每秒更新到磁盤上的AOF文件一次, 最多丟失2秒的數據(因為執行fsync命令刷盤也需要時間,下面會解釋)

  • appendfsync = no 不自動更新到磁盤上的AOF文件,由操作系統來決定何時刷盤(linux 貌似大部分默認是 30s)。可能會丟失刷盤之前的寫入數據。

(基於性能考慮一般生產環境的配置都是everysec)

(aof_buf是Redis中的SDS結構,可以理解為是一個字符串,只是對C語言的字符串做了一些優化,每次將新執行的更新命令添加到字符串末尾。)

怎麼防止AOF文件越來越大?

為了防止AOF文件越來越大,可以通過執行BGREWRITEAOF命令,會fork子進程出來,讀取當前數據庫的鍵值對信息,生成所需的寫命令,寫入新的AOF文件。在生成期間,父進程繼續正常處理請求,執行修改命令后,不僅會將命令寫入aof_buf緩衝區,還會寫入重寫aof_buf緩衝區。當新的AOF文件生成完畢后,子進程父進程發送信號,父進程將重寫aof_buf緩衝區的修改命令寫入新的AOF文件,寫入完畢后,對新的AOF文件進行改名,原子地(atomic)地替換舊的AOF文件。

什麼是AOF文件追加阻塞?

修改命令添加到aof_buf之後,如果配置是everysec那麼會每秒執行fsync操作,調用write寫入磁盤一次,但是如果硬盤負載過高,fsync操作可能會超過1s,Redis主線程持續高速向aof_buf寫入命令,硬盤的負載可能會越來越大,IO資源消耗更快,所以Redis的處理邏輯是會對比上次fsync成功的時間,如果超過2s,則主線程阻塞直到fsync同步完成,所以最多可能丟失2s的數據,而不是1s。

RDB持久化

RDB持久化指的是在滿足一定的觸發條件時(在一個的時間間隔內執行修改命令達到一定的數量,或者手動執行SAVE和BGSAVE命令),對這個時間點的數據庫所有鍵值對信息生成一個壓縮文件dump.rdb,然後將舊的刪除,進行替換。

執行流程

實現原理是fork一個子進程,然後對鍵值對進行遍歷,生成rdb文件,在生成過程中,父進程會繼續處理客戶端發送的請求,當父進程要對數據進行修改時,會對相關的內存頁進行拷貝,修改的是拷貝后的數據。(也就是COPY ON WRITE,寫時複製技術,就是當多個調用者同時請求同一個資源,如內存或磁盤上的數據存儲,他們會共用同一個指向資源的指針,指向相同的資源,只有當一個調用者試圖修改資源的內容時,系統才會真正複製一份專用副本給這個調用者,其他調用者還是使用最初的資源,在CopyOnWriteArrayList的實現中,也有用到,添加或者插入一個新元素時過程是,加鎖,對原數組進行複製,然後添加新元素,然後替代舊數組,解鎖)

	//CopyOnWriteArrayList的添加元素的方法
public boolean add(E e) {
  final ReentrantLock lock = this.lock;
  lock.lock();
  try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
  } finally {
    lock.unlock();
  }
}

混合持久化(Redis4.0+)

執行流程

混合持久化同樣也是通過bgrewriteaof命令完成的,不同的是當開啟混合持久化時,fork出的子進程先將當前內存中的鍵值對信息全量的以RDB方式寫入aof文件,然後在將重寫緩衝區的增量命令以AOF方式寫入到文件,寫入完成后通知主進程更新統計信息,並將新的含有RDB格式和AOF格式的AOF文件替換舊的的AOF文件。簡單的說:新的AOF文件前半段是RDB格式的全量數據後半段是AOF格式的增量數據,如下圖:

3.Redis的數據恢復策略是怎麼樣的?

1.如果配置了混合持久化,那麼根據混合持久化文件進行恢複數據。(Redis4.0+)

2.只配置 AOF ,重啟時加載 AOF 文件恢複數據。

3.同時配置了 RDB 和 AOF ,啟動時只加載 AOF文件恢複數據,如果AOF文件損壞,那麼根據RDB文件恢複數據。

4.只配置 RDB,啟動時加載RDB持久化文件恢複數據。

4.Redis持久化策略該如何進行選擇?

(因為混合持久化是Redis 4.0之後支持的,目前一般生成環境使用的Redis版本可能都還較低,所以這裏的策略選擇主要是針對AOF持久和RDB持久化進行技術選型。)

以下是幾種持久化方案選擇的場景:

1.不需要考慮數據丟失的情況

那麼不需要考慮持久化。

2.單機實例情況下

可以接受丟失十幾分鐘及更長時間的數據,可以選擇RDB持久化,對性能影響小,如果只能接受秒級的數據丟失,只能選擇AOF持久化。

3.在主從環境下

因為主服務器在執行修改命令后,會將命令發送給從服務器,從服務進行執行后,與主服務器保持數據同步,實現數據熱備份,在master宕掉後繼續提供服務。同時也可以進行讀寫分離,分擔Redis的讀請求。

那麼在從服務器進行數據熱備份的情況下,是否還需要持久化呢?

需要持久化,因為不進行持久化,主服務器,從服務器同時出現故障時,會導致數據丟失。(例如:機房全部機器斷電)。如果系統中有自動拉起機制(即檢測到服務停止后重啟該服務)將master自動重啟,由於沒有持久化文件,那麼master重啟后數據是空的,slave同步數據也變成了空的。應盡量避免“自動拉起機制”和“不做持久化”同時出現。

所以一般可以採用以下方案:

主服務器不開啟持久化,使得主服務器性能更好。

從服務器開啟AOF持久化,關閉RDB持久化,並且定時對AOF文件進行備份,以及在凌晨執行bgaofrewrite命令來進行AOF文件重寫,減小AOF文件大小。(當然如果對數據丟失容忍度高也可以開啟RDB持久化,關閉AOF持久化)

4.異地災備

一般性的故障(停電,關機)不會影響到磁盤,但是一些災難性的故障(地震,洪水)會影響到磁盤,所以需要定時把單機上或從服務器上的AOF文件,RDB文件備份到其他地區的機房。

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

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

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

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

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

自動網絡搜索(NAS)在語義分割上的應用(一)

【摘要】本文簡單介紹了NAS的發展現況和在語義分割中的應用,並且詳細解讀了兩篇流行的work:DARTS和Auto-DeepLab。

自動網絡搜索

多數神經網絡結構都是基於一些成熟的backbone,如ResNet, MobileNet,稍作改進構建而成來完成不同任務。正因如此,深度神經網絡總被詬病為black-box,因為hyparameter是基於實驗求得而並非通過嚴謹的數學推導。所以,很多DNN研究人員將大量時間花在修改模型和實驗“調參”上面,而忽略novelty本身。許多教授戲稱這種現象為“graduate student descent”。

近兩年,學術界興起了“自動網絡搜索”取代人工設計網絡結構。2016年,Google Brain公開了他們的研究成果NASNet【1】,這是第一個用自動網絡搜索Neural Architecture Seach (NAS)完成的神經網絡,為深度學習打開了新局面。NASNet是由一系列operation(如depth separable conv, max pooling等)疊加而成。至於怎樣選擇operation,作者用強化學習(RL)的方法,用一個controller網絡隨機組合operations生成模塊,通過評估選擇最優模塊組成網絡結構。Google提供的雲上服務Cloud AutoML正是基於NAS方法,根據用戶上傳的數據自動搜索神經網絡再將結果輸出。迄今為止,國內外很多工業界和學術界的AI Lab都有NAS相關工作:Google Brain提出了NasNet, MNasNet和Nas-FPN,Google提出MobileNetv3,Auto-DeepLab和Dense Prediction Cell(DPC);MIT Han Song團隊提出ProxylessNet;Facebook提出FbNet;Baidu提出SETN;騰訊提出FPNAS;小米提出FairNAS;京東AI提出CAS;華為諾亞有提出P-DARTS等。通過各大實驗室對NAS的研究成果,似乎可以看出自動網絡搜索將會成為趨勢。

接下來我們簡單介紹一下NAS的流程,詳細內容可參考【2】。NAS的架構主要包含三部分:搜索空間Search Space,搜索策略Search Strategy和評估機制Performance Estimation(如下圖)。

  • 首先搜索空間會定義一些模塊(cell),operations(例如dilated conv 3×3),和宏觀網絡結構。cell是由若干個operation組合而成。通常來說,NAS會自動搜索出一些可以重複的cell,將cell按照設定的宏觀網絡結構堆疊起來形成network。Zoph et al.提出了兩種cell,normal cell和reduction cell【3】。Reduction cell有改變spatial resolution的功能。這兩種cell在現有的算法中最為常見。此外還有來自CAS網絡【4】的multi-scale cell,作用和ASPP類似可用作decoder。至於對宏觀網絡結構的設定,早期的工作會採用chained-structured,或者帶分支結構multi-branch例如ResNet和DenseNet帶有skip和shortcut。
  • 搜索策略在NAS中至關重要,因為它決定了下一步要選擇哪一個cell或者operation組成網絡結構。搜索的目的就是讓現有的網絡結構在unseen data上得到最好的效果。如前文提到,第一篇工作NASNet中用到了強化學習作為搜索策略。雖然實驗結果尚可,但強化學習耗時過長並且需要大量GPU資源。NASNet就是用幾百個GPU搜索了幾天才完成,這對於資源少的研究人員來說不可能完成。繼NASNet之後早期的工作多是基於RL完成搜索。隨着其他方法的研究,如random search, Bayesian optimization, Gradient-based, evolution algorithms,搜索時間在下降並且GPU使用量也在減少,使得NAS得以普及。感謝https://github.com/D-X-Y/Awesome-NAS 提供了搜索策略的調研。
  • 由於一個神經網絡通常有幾十甚至上百層,結構也很複雜,一個及時並有效的反饋對於減少搜索時間很有幫助。最簡單的方法是把訓練數據分成兩部分,train data用來訓練網絡結構,validation data用來評估當前搜索到的網絡結構。一般來說,一個好的評估機制要兼具速度和準確度,舉個例子,train data少則網絡結構訓練不到位,train data多則搜索時間過長。來自Freiburg大學的Elsken團隊發表了關於NAS的調研,裏面對於評估方法講的很詳細,請挪步【2】。

自動網絡搜索在語義分割上的應用

相比於目標檢測,語義分割可以更好的解析圖像,因為圖像上的每一個pixel都會被分類。所以語義分割可以完成目標檢測無法完成的任務,如自動駕駛的全景分割,醫療圖像診斷,衛星圖分割,背景摳圖和AR換裝等。過去的五年繼FCN之後,很多神經網絡在公開數據集上都有不錯的成績。隨着AI產品落地,對神經網絡性能的要求更為嚴格:在保證accuracy的前提下,在有限制的硬件資源運行並且提升inference速度。這為設計神經網絡增加了很大難度。所以NAS是一個很好的選擇,它可以避免通過大量調參實驗來決定最優網絡結構。在語義分割之前,NAS在圖像分類和目標檢測上均有成功的應用。我們總結了近兩年比較流行的NAS在語義分割上的工作。多數的語義分割的網絡都是encoder-decoder結構,在這裏我們對比了實際搜索的部分,搜索性能耗時和方法。由於每一個work都在不同實驗環境下進行,所以實驗結果沒有直接進行對比。

(1)第一篇將NAS應用於語義分割的工作來自Google DeepLab團隊,作者是Liang-Chieh Chen等一眾大佬。DeepLabv3+的準確度已經達到一定高度,繼而DeepLab團隊將研究方向轉移到了NAS上面。基於和DeepLab同樣的encoder-decoder結構, 在【5】工作中,作者將encoder結構固定,試圖搜索出一個更小的 ASPP,用到的搜索策略是random search。在Cityscapes達到82.7% mIoU,在PASCAL VOC 12上達到87.9%,結果尚可但搜索時間過慢,用370個GPU搜索一周才得到網絡,這也限制了該方法的普及。

(2)同樣來自DeepLab團隊,Auto-DeepLab【6】相比之前的工作在搜索效率上有顯著的提升。這都歸功於它gradient-based的搜索策略DARTS【9】。與上一篇相反,Auto-DeepLab的目標是搜索encoder,而decoder則採用ASPP。從實驗結果來看,Auto-DeepLab的準確度和DeepLabv3+相差不多,但是FLOPs和參數數量卻是DeepLabv3+的一半。後文我們會具體介紹DARTS和Auto-DeepLab。

(3)Customizable Architecture Search (CAS)【4】是來自京東AI Lab的工作。在搜索空間定義了三種cell,分別是normal cell,reduction cell和新提出的multi-scale cell。其中multi-scale cell是ASPP 的功能類似,所以CAS的搜索區間是針對全部網絡。在搜索策略上依然採用DARTS。值得一提的是,在搜索網絡的時候,CAS不僅考慮accuracy,還考慮了每個模塊的GPU time,CPU time,FLOPs和參數數量。這些屬性對於實時任務至關重要。所以CAS在Cityscapes上的準確度是72.3%,雖然沒有很高,但是在TitanXP GPU的速度fps達到108。

(4)來自Adeleide大學的Vladimir Nekrasov團隊在light weight模型上面做了大量工作,而最近也將研究重心轉到了NAS上面。在【7】中,作者採用RL的搜索策略搜索decoder。眾所周知RL非常耗時,所以作者採用知識蒸餾策略和Polyak Averaging方法結合提升搜索速度,而這正是本文的major contribution。

(5)最後一篇來自商湯發布在Artix的文章,採用圖神經網絡GCN作為搜索策略,試圖尋找cell之間最優的連接方式【8】。和CAS類似,在本文中每個模塊的性能如latency也在搜索評估時考慮進去。在Cityscape上GAS達到73.3%mIoU,在TitanXP GPU的速度是102fps。

簡單總結一下上述工作,我們可以發現NAS在語義分割上的應用還算成功,並且很多團隊已經在NAS上進行研究探索。尤其在DARTS類似的高效搜索策略提出后,個人研究者和小團隊也可以構建自己的NAS網絡,而不受制於GPU資源。

DARTS: Differentiable Architecture Search

通常來說搜索空間是一個離散的空間,在DARTS中【9】,作者將搜索空間定義成一個連續空間,這樣一來搜索到的每一個cell都是可導的,可以用stochastic gradient descent來優化。所以DARTS相比RL並不需要大量GPU資源和搜索時間。在實驗中,DARTS成功用在了CNN模型的圖像分類和RNN模型的language modelling任務上。感恩作者提供了源代碼 https://github.com/quark0/darts ,它可以為我們搭建自己NAS模型提供很好的基礎。

正如前文中提到,多數的NAS網絡會搜索不同類型的可重複的cell,然後將cell連接起來構成神經網絡。這個cell通常用directed acyclic graph(DAG)表示。一個DAG cell包含N個有順序的node,每一個node可以看成在它前面所有node的結合(就像feature map一樣是一個latent representation)。從node i 到node j的連接是某一種operation記作o(i,j). 每一個cell都有兩個input node和一個output node。下圖中是一個DARTS展示的reduction cell。我們可以看到cell k 有兩個input nodes 分別是c_{k-1}和c_{k-2} (來自cell k-1和cell k-2的output node),cell k包含了4個immediate nodes和一個output node c_{k} 。o(i,j)是邊緣上的max_pool_3x3,max_pool等。理論上從一個node到相鄰的node可以有很多種operation,所以搜索最優網絡結構也可以看成是選擇每一個邊上最佳的operation。

DARTS將每對node之間的每一個operation都賦予一個weight,最優解可以用softmax求得,也就是說有最大probability的path代表最優operation,這也是DARTS的核心部分。DARTS在搜索空間中定義了兩種cell,reduction cell和normal cell。宏觀網絡結構是固定的,作者採用了簡單的chained-structured,將reduction cell放在了網絡結構的1/3和2/3處。所以說在搜索的過程中,cell內部不斷更新而宏觀結構沒有變化。我們定義operation的參數為W,將cell中operation的weight記為Alpha。根據論文和source code,我們總結了DARTS的搜索流程如下圖。

網絡搜索的第一步是對模型結構,optimizer,loss進行初始化。文中定義了幾種operation,代碼中的定義在operation.OPS, 兩種cell在代碼中的定義是genotypes.PROMITIVES. 參數Alpha在代碼中定義為arch_parameters()={alphas.normal, alphas.reduce}. 在搜索過程中,train data被分成兩部分,train patch用來訓練網絡參數W而validation patch用來評估搜索到的網絡結構。在代碼中,搜索過程的核心部分在architect.step()。網絡搜索的目標函數就是讓validation在現有網絡的loss最小,文章中公式(3)給出了objective:

為了減少搜索時間,每一輪只用一個training patch去更新參數W計算train loss。在計算Alpha的時候涉及到二階求導,稍微複雜一點,但是論文和代碼都給了詳細解釋,這裏不贅述,代碼中architect._hession_vector_product是求二階導的實現。在更新W和Alpha之後,最優operation通過softmax來計算。文中保留了top-k probability的operation。W和Alpha不斷計算更新直到搜索過程結束。

文中進行了大量實驗,我們這裏只介紹一下在CIFAR-10數據上面進行的圖像分類任務。作者將DARTS與傳統人工設計的網絡DenseNet,和幾個其他常見的NAS網絡進行對比,如AmoeNet和ENet都是常被提及的。DARTS在準確度上優於其他所有算法,並且在搜索速度上明顯比RL快很多。由於結構簡單效果好,而且不需要大量GPU和搜索時間,DARTS已經被大量引用。

Auto-DeepLab: Hierarchical Neural Architecture Search for Semantic Image Segmentation

基於DARTS的結構,Google DeepLab團隊提出了Auto-DeepLab並發表在2019年CVPR上。在tensorflow deeplab官網上公布了nas backbone並且給出了可以訓練的模型結構,但是搜索過程並沒有公開。於是我們訓練了給出的nas網絡結構,在沒有任何pre-training的情況下與deeplab v3+進行對比。代碼參考 https://github.com/tensorflow/models/tree/master/research/deeplab 。

在DARTS中,宏觀網絡結構是提前定義的,而在Auto-DeepLab中宏觀網絡結構也是搜索的一部分。繼承自DeepLab v3+的encoder-decoder結構,Auto-DeepLab的目的是搜索Encoder代替現有的xception65,MobileNet等backbone,decoder採用ASPP。在搜索空間中定義了reduction cell,normal cell和一些operation。Reduction cell用來改變spatial resolution,使其變大兩倍,或不變,或變小兩倍。為了保證feature map的精度,Auto-DeepLab規定最多downsampling 32倍 (s=32)。下圖定義了宏觀網絡結構(左)和cell內部的結構(右)。

Auto-DeepLab定義了12個cell,而上圖(左)中前面兩個白色的node是固定的兩層為了縮小spatial resolution。如圖左灰色箭頭所示,正式搜索之後,每一個cell的位置都有多種cell類型可以選擇:可以來自於當前cell相同的spatial resolution的cell,也可以是比當前cell的spatial resolution大一倍或小一倍的cell。作者將這些空間路徑(灰色箭頭表示的路徑)也賦予一個weight,記作Beta。如圖右,每一個cell的輸出都是由相鄰spatial resolution的cell結合而成,而Beta的值可以理解成不同路徑的probability。為了更直觀,我們把圖右的三個cell分別用藍色,黃色和綠色標註,對應圖左的三個cell。與DARTS類似,我們將operation的parameters記作W,將cell內部operation的權重記作Alpha。所以搜索最優網絡結構,即迭代計算並更新W,Alpha和Beta。文中給出每一個cell的實際輸出為:

從上面公式可以看出,W和{Alpha,Beta}要分別計算和更新。所有的weight都是非負數。Alpha的計算方式依然是ArgMax,而計算Beta用了經典的貪心算法Viterbi算法。下圖給出的宏觀網絡結構是基於Cityscapes搜索到的結果,對應代碼中的backbone是[0,0,0,1,2,1,2,2,3,3,2,1], 数字代表downsample倍數。在模型中,每一個cell中的node由兩個路徑組成,如圖右。

文中用了三組開源數據PASCAL VOC 12, Cityscapes和ADE20k做了對比實驗。具體實驗參數設置和對比算法在論文中有詳細說明,這裏只對比和Deeplab v3+。Cityscapes訓練數據尺寸是[769×769],而PASCAL VOC 12和ADE20k訓練數據尺寸是[513×513]。一般來說,Auto-DeepLab和DeepLabv3+準確度相差無幾,但是速度上要快2.33倍,並且Auto-DeepLab可以從零開始訓練。

除了文中給出的實驗結果以外,我們在PASCAL VOC 12數據上從零開始訓練了Auto-DeepLab,用代碼中給出的模型結構,並且與DeepLabv3+(xception65)進行結果對比。但是並不是所有結果都能復現,分析原因大概是這樣:首先,上文中給出的模型結構是用Cityscapes數據集搜索得到,也許在PASCAL VOC 12上並不是最優解;其次沒有用ImageNet做pre-training,訓練環境也不同。我們在下面表格中對比了FLOPs, 參數數量, 在K80 GPU上面的fps和mIoU。

下圖中直觀對比了ground truth(第二列),deeplabv3+(第三列)和Auto-DeepLab-S(第四列)的分割結果。與上面的mIoU一致,DeepLabv3+的分割結果要比Auto-DeepLab更精準一些,尤其是在邊緣。對於簡單的圖像案例,兩者分割結果相差無幾,但是在較難的情況下,Auto-DeepLab會有很大誤差(在第三個案例中,Auto-DeepLab將女孩識別成狗)。

總結

本文簡單介紹了NAS的發展現況和在語義分割中的應用,並且詳細解讀了兩篇流行的work:DARTS和Auto-DeepLab。從整體實驗結果來看,還不能看出NAS的方法比傳統的模型有壓倒性優勢,尤其在準確度上。但是NAS給深度學習注入了新鮮的血液,為研究者提供了一種新的思路,並且還有很大的提升空間和待開發領域。也許人工設計網絡結構將會被自動網絡搜索取代。

翻譯或有誤差,請參考原文https://medium.com/@majingting2014/neural-architecture-search-on-semantic-segmentation-1801ee48d6c4

 

對這塊比較關注的同學可以移步繼續閱讀《自動網絡搜索(NAS)在語義分割上的應用(二)》

References

[1] Zoph, Barret, and Quoc V. Le. “Neural architecture search with reinforcement learning.” The International Conference on Learning Representations (ICLR) (2017)

[2] Elsken, Thomas, Jan Hendrik Metzen, and Frank Hutter. “Neural Architecture Search: A Survey.” Journal of Machine Learning Research 20.55 (2019): 1-21.

[3] Zoph, Barret, et al. “Learning transferable architectures for scalable image recognition.” Proceedings of the IEEE conference on computer vision and pattern recognition (CVPR). 2018.

[4] Zhang, Yiheng, et al. “Customizable Architecture Search for Semantic Segmentation.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2019.

[5] Chen, Liang-Chieh, et al. “Searching for efficient multi-scale architectures for dense image prediction.” Advances in Neural Information Processing Systems (NIPS). 2018.

[6] Liu, Chenxi, et al. “Auto-deeplab: Hierarchical neural architecture search for semantic image segmentation.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2019.

[7] Nekrasov, Vladimir, et al. “Fast neural architecture search of compact semantic segmentation models via auxiliary cells.” Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (CVPR). 2019.

[8] Lin, Peiwen, et al. “Graph-guided Architecture Search for Real-time Semantic Segmentation.” arXiv preprint arXiv:1909.06793 (2019).

[9] Liu, Hanxiao, Karen Simonyan, and Yiming Yang. “Darts: Differentiable architecture search.” The International Conference on Learning Representations (ICLR) (2019).

點擊關注,第一時間了解華為雲新鮮技術~

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

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

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

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

Jmeter(八) – 從入門到精通 – JMeter配置元件(詳解教程)

1.簡介

JMeter配置元件可以用來初始化默認值和變量,讀取文件數據,設置公共請求參數,賦予變量值等,以便後續採樣器使用。將在其作用域的初始化階段處理。配置元件(Config Element)提供對靜態數據配置的支持,可以為取樣器設置默認值和變量。

首先我們來看一下JMeter的配置元件,路徑:添加-配置元件;我們可以清楚地看到JMeter5中共有19個配置元件,如下圖所示:

如果上圖您看得不是很清楚的話,宏哥總結了一個思維導圖,關於JMeter5的配置元件類型,如下圖所示:

 通過以上的了解,我們對配置元件有了一個大致的了解和認識。下面宏哥就給小夥伴或則童鞋們分享講解一些通常在工作中會用到的配置元件。

 2.常用配置元件詳解

  這一小節,宏哥就由上而下地詳細地講解一下常用的配置元件。

2.1CSV Data Set Config

1、我們先來看看這個CSV Data Set Config長得是啥樣子,如下圖所示:

 2、參數詳解及說明,如下錶所示:

參 數 描 述 是否必填
Name 腳本中显示的這個元件的描述性名稱 是
Filename 待讀取文件的名稱。可以寫入絕對路徑,也可以寫入相對路徑(相對於bin目錄),如果直接寫文件名,則該文件要放在bin目錄中。對於分佈式測試,主機和遠程機中相應目錄下應該有相同的CSV文件 是
File Encoding 文件讀取時的編碼格式,不填則使用操作系統的編碼格式 否
Ignore first line 是否忽略首行,如果csv文件中沒有表頭,則選擇false 是
Variable Names 變量名列表,多個變量名之間必須用分隔符分隔。如果該項為空,則文件首行會被讀取並解析為列名列表 否
Delimiter 參數分隔符,將一行數據分隔成多個變量,默認為逗號,也可以使用“\t”。如果一行數據分隔后的值比Vairable Names中定義的變量少,這些變量將保留以前的值(如果有值的話) 是
Allow quoted data? 是否允許變量使用雙引號,允許的話,變量將可以括在雙引號內,並且這些變量名可以包含分隔符 否
Recycle on EOF? 是否循環讀取csv文件內容,達到文件結尾后,是否從文件開始循環重新讀取;默認為 true 是
Stop thread on EOF? 是否循環讀取csv文件內容,達到文件結尾后,線程是否該終止;默認為 true 是
Recycle on EOF? 當Recycle on EOF為False時,停止線程,當Recycle on EOF為True時,此項無意義,默認為 false 是
Sharing mode 1、All threads(默認):一個線程組內,各個線程(用戶)唯一順序取值;2、current thread:一個線程組內,各個線程(用戶)各自順序取值;3、線程組各自獨立,但每個線程組內各個線程(用戶)唯一順序取值; 是

 3、Recycle on EOF 和Stop thread on EOF的關係:

當Recycle on EOF 選擇true時,Stop thread on EOF選擇true和false無任何意義,因為既然前面已經設置了文件是不停的循環讀取,後面的控制stop就相當於失效;
當Recycle on EOF 選擇false時,Stop thread on EOF選擇true,則當線程數超過文件里的參數的個數時,實際請求數為參數的個數;
當Recycle on EOF 選擇false時,Stop thread on EOF選擇flase,當線程數超過文件里參數的個數時,實際請求次數為線程數,但當線程數超過參數次數時,由於沒有參數,所以結果仍然是失敗的。
4、Sharing mode:如果希望每個線程擁有自己獨立的值集合,那麼就需要創建一系列數據文件,為每個線程準備一個數據文件,如test1.csv、test2.csv等,使用文件名test${__threadNum}.csv,並將“sharing mode”設置為”Current thread”

All threads:文件在所有線程間共享。

Identifier:所有線程共享相同的標識,共享相同的文件。如有4個線程組,測試人員可以使用一個通用ID,以便在兩個或多個線程組之間共享文件。

Current thread:每個文件會針對每個線程單獨打開。

Current thread group:每個文件會針對每個線程組打開一次。

2.2HTTP Header Manager

支持用戶添加或者重寫HTTP請求頭。JMeter支持多個信息頭管理器。多個信息頭條目合併成一個信息頭列表,跟隨http請求一併提交到服務端。

注意:敲黑板,敲腦殼!!!

(1)當有多個信息頭管理器,且不同的管理器內有名稱相同的信息頭條目存在時,順序靠前的管理器的信息頭條目會覆蓋後面的;

(2)當只有一個信息頭管理器,但管理器內有名稱相同的信息頭條目時,會同時生效;

 1、我們先來看看這個HTTP Header Manager長得是啥樣子,如下圖所示:

 2、參數詳解及說明,如下錶所示:

參數 描述 是否必填
Name 請求頭的名稱,比如Content-Type 否
Value 請求頭的值,比如application/json 否

3、常用請求頭,這些一般可以抓包和在瀏覽器中查到,如下錶所示:

2.3HTTP Cookie Manager

主要有兩個功能:

一個功能是:像web瀏覽器一樣存儲和發送Cookie。如果有一個HTTP請求和相應里包含Cookie,Cookie管理器會自動存儲Cookie,那麼接下來針對特定web站點的所有請求中使用該Cookie。可在結果樹中查看。

接收到的Cookie可以被保存為變量,須定義屬性”CookieManager.save.cookie=true”。另外,在被存儲前Cookie名稱會加上前綴“COOKIE_”,要恢復早前處理方式,則定義屬性”CookieManager.name.prefix=”(一個或多個空格)。

如果啟動了該功能,那麼名稱為TEST的Cookie,可以通過${COOKIE_TEST}加以引用。手動為Cookie管理器添加一個Cookie(為所有JMeter線程所共享)。

1、我們先來看看這個HTTP Cookie Manager長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示:

參數 描述 是否必填
Name 樹中显示此元件描述的名稱 是
Comments 註釋 否
Clear cookie each Iteration  每次線程組運行前,都會清楚cookie,但是如果是手動添加的cookie,不會被清除 否
Cookie Policy 選擇Cookie的管理策略,建議選擇兼容性,兼容性強 是
User Define cookie 用戶自定義cookie 否

 

2.4HTTP Cache Manager

  被用來為其作用域內的HTTP請求提供緩存功能,如果“Use Cache-Control/Expires header When …”選中,那麼會根據當前時間來選擇,如果請求是”GET”,而時間指向未來,那麼採樣器就會立即返回,而無須從遠程服務器請求URL,這樣是為了模擬瀏覽器的操作,請注意Cache-Control頭必須是“pulic”的,並且只有”max-age”終結選項會被處理,如果請求文檔自從其被緩存以來沒有發生任何改變,那麼響應包體就會為空。

1、我們先來看看這個HTTP Cache Manager長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示:

參數 描述 是否必填
Name 樹中显示此元件的描述性名稱  是
Comments 註釋 否
Clear Cache each iteration 如果選擇此選項,則在線程開始時清除緩存。 否
Use Cache 如果選擇此選項,則在線程開始時使用緩存。 否
Max Number 如果選擇此選項,則在線程開始時最大緩存。 否

 

2.5HTTP Request Defaults

在實際測試計劃中,我們經常會碰到Http Sampler請求有較多的參數與配置會重複,每一個Http Sampler都單獨設置的話比較浪費時間和精力,為了節省工作量,JMeter提供了HTTP Request Defaults元件,用來把這些重複的部分封裝起來,一次設置多次使用。可以設定一些缺省值,假設有10個請求,訪問域名和端口都是一樣的,那HTTP請求中就不再需要單獨配置了,比較方便(增加腳本的移植性)。

這個元件可以設置HTTP請求控制器使用的默認值。例如,圖中【服務器名稱或IP】項目內填入了【example.com】,後面的HTTP請求如果IP也是example.com的話,那麼只要將【服務器名稱或IP】留空,那麼這個字段將自動繼承HTTP請求默認值中的值。其他諸如【協議】、【端口號】、【路徑】等同此。

1、我們先來看看這個HTTP Request Defaults長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示:

參數 描述 是否必填
Name 用作標識一個取樣器,建議使用一個見名知義的名稱 是
Comments 註釋 否
Protocol 協議,向目標服務器發送HTTP請求時的協議,可以是http或者是Https  
IP HTTP請求發送的目標服務器名稱或者IP地址  
Port Number 目標服務器端口  
Path 目標URL路徑(不包括服務器地址和端口)  
Content encdoing 內容的編碼方式  
Parameter 參數  
body data 參數  

 

2.6Counter

計數器,顧名思義就是在測試執行過程中會記錄迭代次數。可以在線程組任何位置創建,允許用戶配置起點、最大值和增量。配置后,計數器將從起點循環到最大值,然後重新開始,直到線程結束。允許用戶創建一個計數器,可在線程組中任何地方被引用。

1、我們先來看看這個Counter長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示:

參數 描述 是否必填
Name 控制器名稱,可以隨意設置 否
Comments 註釋,可以隨意設置 否
Starting value 啟動,記錄數量起始值  
Increment 遞增,記錄迭代次數步長,1后是2,步長就是1  
Maximum value 記錄的最大值  
Number format 計算器格式,可以是数字,例如000000(6位長度,000,000(6位長度,3位間隔開);字符加数字,例如CUST_000000(字符加6位数字 )  
Exported Variable Name 引用變量名稱,記數器記錄的值可以存入的此引用名(變量),可供其他元件調用  
Track counter independently for each user 與每位用戶獨立的跟蹤計數器,每個線程都有自己的計數器,相互不干擾  
Reset counter on each Thread Group Iteration 每次迭代復原計數器  

 

2.7DNS Cache Manager

1、我們先來看看這個DNS Cache Manager長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示:

參數 描述 是否必填
Name 樹中显示此元件的描述性名稱  
Comments 註釋  
Clear cache each iter 清除每個迭代的緩存,如果選擇此選項,則每次啟動新迭代時,都會清除每個線程的DNS緩存。  
Use System DNS resolver 使用系統DNS解析器;將使用系統DNS解析器。為了正確工作,請編輯 $ JAVA_HOME / jre / lib / security / java.security並添加networkaddress.cache.ttl = 0  
Use custom DNS resolver 使用自定義DNS解析器;將使用自定義DNS解析器(來自dnsjava庫)。  

 

2.8FTP Request Defaults

被用於設置FTP請求的默認值

1、我們先來看看這個FTP Request Defaults長得是啥樣子,如下圖所示:

2.9HTTP Authorization Manager

HTTP認證是一種安全機制,在客戶端、瀏覽器或者程序向服務器發起請求時,需要提供用戶名和密碼且驗證通過(拿到憑證)才能繼續發起交互。

1、我們先來看看這個HTTP Authorization Manager長得是啥樣子,如下圖所示: 

2.10JDBC Connection Configuration

1、我們先來看看這個JDBC Connection COnfiguration長得是啥樣子,如下圖所示: 

 2、關於JDBC Connection COnfiguration參數詳細說明,可以參考宏哥的另一篇文章是非常詳細的:傳送門。

2.11Java Request Defaults

1、我們先來看看這個Java Request Defaults長得是啥樣子,如下圖所示: 

2.12Keystore Configuration

1、我們先來看看這個Keystore Configuration長得是啥樣子,如下圖所示: 

2、參數詳細說明,如下錶所示:

參數 描述 是否必填
Name 樹中显示此元件的描述性名稱。可以默認 是
Comments 註釋 否
Preload 預載,是否預加載秘鑰庫,設置為true通常是最佳選擇 是
Variable name holding certificate alias 變量名稱,將包含用於客戶端證書身份驗證的別名。例如,將從CSV數據集中填充變量值。在屏幕截圖中,“ certificat_ssl”也將是CSV數據集中的變量。 否
Alias Start index 從0開始在Keystore中使用的第一個鍵的索引。 是
Alias End index 基於0的密鑰庫中要使用的最後一個密鑰的索引。使用“變量名稱持有證書別名”時,請確保其足夠大,以便在啟動時加載所有密鑰。 是

 2.13LDAP Extended Request Defaults

1、我們先來看看這個LDAP Extended Request Defaults長得是啥樣子,如下圖所示: 

2.14LDAP Request Defaults

1、我們先來看看這個LDAP Request Defaults長得是啥樣子,如下圖所示: 

2.15Login Config Element

1、我們先來看看這個Login Config Element長得是啥樣子,如下圖所示: 

2.16Random Variable

1、我們先來看看這個Random Variable長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示:  

參數 描述 是否必填
Name 樹中显示的此元件的描述性名稱。  
Comments 註釋  
Variable Name 變量名,存儲隨機字符串的變量的名稱。  
Output Format 格式化字符串,要使用的java.text.DecimalFormat格式字符串。例如,“ 000”將生成至少3位数字,或者“ USER_000”將生成USER_nnn形式的輸出。如果未指定,則默認為使用Long.toString()生成数字。  
Minimum Value 最小值;生成的隨機數的最小值(長整數)。  
Maximum Value 最大值;生成的隨機數的最大值(長整數)。  
Seed for Random function 隨機種子,隨機數生成器的種子。默認值為當前時間,以毫秒為單位。如果在“將每個線程”設置為true的情況下使用相同的種子值,則與“ 隨機” 類一樣,您將為earch線程獲得相同的值  
Per Thread(User)? 每個線程,如果為False,則在線程組中的所有線程之間共享生成器。如果為True,則每個線程都有自己的隨機生成器。  

2.17Simple Config Element

1、我們先來看看這個Simple Config Element長得是啥樣子,如下圖所示: 

2、參數詳細說明,如下錶所示:  

參數 描述 是否必填
Name 樹中显示此元件的描述名稱 是
Comments 註釋 是
Name 參數名稱  
Value 參數值  

2.18TCP Sampler Config

TCP採樣器配置為TCP採樣器提供默認數據

1、我們先來看看這個TCP Sampler Config長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示: 

參數 描述 是否必填
Name 樹中显示此元件的描述性名稱  
Comments 註釋  
TCPClient classname TCPClient類的名稱,默認屬性tcp.handler,使TCPClientImpl失敗  
Sever Name or IP TCP服務器的名稱或者IP  
Port Number 使用的端口  
Re-use connection 重用連接,如果選擇,則連接保持打開狀態,否則,在讀取數據后將其關閉  
Close connection 關閉連接,如果選擇此項,則在運行採樣器后將連接關閉  
Set NoDelay 設置節點布局,應該設置nodelay  
SO_LINGER 創建套接字時,以指定的延遲時間(以秒為單位)啟用/禁用SO_LINGER。如果將“SO_LINGER”值設置為0,則則可以防止大量套接字處於TIME_WAT 的狀態  
End of line byte value 判斷行結束的byte值,如果你指定的值大於127或者小於-128,則會跳過EOL檢測。比如服務器端返回的字符串都是以回車符結尾,那麼我們可以將該選項設置成10。  
Text to send 文字發送,要發送的文字  
Connect 連接超時(毫秒。0禁用)  
Response 響應超時(毫秒。0禁用)  

2.19User Defined Variables

如果您有多個線程組,請確保對不同的值使用不同的名稱,因為UDV在線程組之間共享。同樣,這些變量在處理完元素之後才可用,因此您不能引用在同一元素中定義的變量。您可以引用在早期UDV或測試計劃中定義的變量。

1、我們先來看看這個User Defined Variable長得是啥樣子,如下圖所示:

2、參數詳細說明,如下錶所示: 

參數 描述 是否必填
Name 樹中显示此元件描述的名稱  
Comments 註釋  
User Define Variables 用戶定義的變量。變量名稱/值對。您需要在$ {…}結構的方括號內放置“名稱”(Name)列下的字符串,以便以後使用變量。然後,整個$ {…}將由“值”列中的字符串替換  

3.小結

 好了,今天關於JMeter的配置元件就分享到這裏,其中有些常用的要熟練掌握。

 

您的肯定就是我進步的動力。如果你感覺還不錯,就請鼓勵一下吧!記得隨手點波  推薦  不要忘記哦!!!

別忘了點 推薦 留下您來過的痕迹

 

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

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

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

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

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

※回頭車貨運收費標準

一文說通Dotnet Core的後台任務

這是一文說通系列的第二篇,裏面有些內容會用到第一篇中間件的部分概念。如果需要,可以參看第一篇:一文說通Dotnet Core的中間件

 

一、前言

後台任務在一些特殊的應用場合,有相當的需求。

比方,我們需要實現一個定時任務、或周期性的任務、或非API輸出的業務響應、或不允許併發的業務處理,像提現、支付回調等,都需要用到後台任務。

 

通常,我們在實現後台任務時,有兩種選擇:WebAPI和Console。

下面,我們會用實際的代碼,來理清這兩種工程模式下,後台任務的開發方式。

    為了防止不提供原網址的轉載,特在這裏加上原文鏈接:https://www.cnblogs.com/tiger-wang/p/13081020.html

二、開發環境&基礎工程

這個Demo的開發環境是:Mac + VS Code + Dotnet Core 3.1.2。

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.1.201
 Commit:    b1768b4ae7

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.15
 OS Platform: Darwin
 RID:         osx.10.15-x64
 Base Path:   /usr/local/share/dotnet/sdk/3.1.201/

Host (useful for support):
  Version: 3.1.3
  Commit:  4a9f85e9f8

.NET Core SDKs installed:
  3.1.201 [/usr/local/share/dotnet/sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

 

首先,在這個環境下建立工程:

  1. 創建Solution
% dotnet new sln -o demo
The template "Solution File" was created successfully.
  1. 這次,我們用Webapi創建工程
% cd demo
% dotnet new webapi -o webapidemo
The template "ASP.NET Core Web API" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on webapidemo/webapidemo.csproj...
  Restore completed in 179.13 ms for demo/demo.csproj.

Restore succeeded.
% dotnet new console -o consoledemo
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on consoledemo/consoledemo.csproj...
  Determining projects to restore...
  Restored consoledemo/consoledemo.csproj (in 143 ms).

Restore succeeded.
  1. 把工程加到Solution中
% dotnet sln add webapidemo/webapidemo.csproj
% dotnet sln add consoledemo/consoledemo.csproj

基礎工程搭建完成。

三、在WebAPI下實現一個後台任務

WebAPI下後台任務需要作為託管服務來實現,而託管服務,需要實現IHostedService接口。

 

首先,我們需要引入一個庫:

% cd webapidemo
% dotnet add package Microsoft.Extensions.Hosting

引入后,我們就有了IHostedService。

 

下面,我們來做一個IHostedService的派生託管類:

namespace webapidemo
{
    public class DemoService : IHostedService
    {
        public DemoService()
        
{
        }

        public Task StartAsync(CancellationToken cancellationToken)
        
{
            throw new NotImplementedException();
        }

        public Task StopAsync(CancellationToken cancellationToken)
        
{
            throw new NotImplementedException();
        }
    }
}

IHostedService需要實現兩個方法:StartAsync和StopAsync。其中:

StartAsync: 用於啟動後台任務;

StopAsync:主機Host正常關閉時觸發。

 

如果派生類中有任何非託管資源,那還可以引入IDisposable,並通過實現Dispose來清理非託管資源。

 

這個類生成后,我們將這個類注入到ConfigureServices中,以使這個類在Startup.Configure調用之前被調用:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHostedService<DemoService>();
}

下面,我們用一個定時器的後台任務,來加深理解:

namespace webapidemo
{
    public class TimerService : IHostedService, IDisposable
    {
          /* 下面這兩個參數是演示需要,非必須 */
        private readonly ILogger _logger;
        private int executionCount = 0;

          /* 這個是定時器 */
        private Timer _timer;

        public TimerService(ILogger<TimerService> logger)
        
{
            _logger = logger;
        }

        public void Dispose()
        
{
            _timer?.Dispose();

        }

        private void DoWork(object state)
        
{
            var count = Interlocked.Increment(ref executionCount);

            _logger.LogInformation($"Service proccessing {count}");
        }

        public Task StartAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service starting");

            _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service stopping");

            _timer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }
    }
}

注入到ConfigureServices中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHostedService<TimerService>();
}

就OK了。代碼比較簡單,就不解釋了。

四、WebAPI後台任務的依賴注入變形

上一節的示例,是一個簡單的形態。

下面,我們按照標準的依賴注入,實現一下這個定時器。

 

依賴注入的簡單樣式,請參見一文說通Dotnet Core的中間件。

 

首先,我們創建一個接口IWorkService:

namespace webapidemo
{
    public interface IWorkService
    {
        Task DoWork();
    }
}

再根據IWorkService,建立一個實體類:

namespace webapidemo
{
    public class WorkService : IWorkService
    {
        private readonly ILogger _logger;
        private Timer _timer;
        private int executionCount = 0;

        public WorkService(ILogger<WorkService> logger)
        
{
            _logger = logger;
        }

        public async Task DoWork()
        
{
            var count = Interlocked.Increment(ref executionCount);

            _logger.LogInformation($"Service proccessing {count}");
        }
    }
}

這樣就建好了依賴的全部內容。

 

下面,創建託管類:

namespace webapidemo
{
    public class HostedService : IHostedService, IDisposable
    {
        private readonly ILogger<HostedService> _logger;
        public IServiceProvider Services { get; }
        private Timer _timer;

        public HostedService(IServiceProvider services, ILogger<HostedService> logger)
        
{
            Services = services;
            _logger = logger;
        }

          public void Dispose()
        
{
            _timer?.Dispose();
        }

        private void DoWork(object state)
        
{
            _logger.LogInformation("Service working");

            using (var scope = Services.CreateScope())
            {
                var scopedProcessingService =
                    scope.ServiceProvider
                        .GetRequiredService<IWorkService>();

                scopedProcessingService.DoWork().GetAwaiter().GetResult();
            }
        }

        public Task StartAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service starting");

            _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
            return Task.CompletedTask;
        }


        public Task StopAsync(CancellationToken cancellationToken)
        
{
            _logger.LogInformation("Service stopping");

            _timer?.Change(Timeout.Infinite, 0);
            return Task.CompletedTask;
        }
    }
}

把託管類注入到ConfigureServices中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddHostedService<HostedService>();
    services.AddSingleton<IWorkService, WorkService>();
}

這樣就完成了。

 

這種模式下,可以根據注入的內容切換應用的執行內容。不過,這種模式需要注意services.AddSingleton、services.AddScoped和services.AddTransient的區別。

五、Console下的後台任務

Console應用本身就是後台運行,所以區別於WebAPI,它不需要託管運行,也不需要Microsoft.Extensions.Hosting庫。

我們要做的,就是讓程序運行,就OK。

 

下面是一個簡單的Console模板:

namespace consoledemo
{
    class Program
    {

        private static AutoResetEvent _exitEvent;

        static async Task Main(string[] args)
        
{
                /* 確保程序只有一個實例在運行 */
            bool isRuned;
            Mutex mutex = new Mutex(true, "OnlyRunOneInstance", out isRuned);
            if (!isRuned)
                return;

            await DoWork();

                        /* 後台等待 */
            _exitEvent = new AutoResetEvent(false);
            _exitEvent.WaitOne();
        }

        private static async Task DoWork()
        
{
            throw new NotImplementedException();
        }
    }
}

這個模板有兩個關鍵的內容:

  1. 單實例運行:通常後台任務,只需要有一個實例運行。所以,第一個小段,是解決單實例運行的。多次啟動時,除了第一個實例外,其它的實例會自動退出;
  2. 後台等待:看過很多人寫的,在這兒做後台等待時,用了一個無限的循環。類似於下面的:
while(true)
{
    Thread.Sleep(1000);
}

這種方式也沒什麼太大的問題。不過,這段代碼總是要消耗CPU的計算量,雖然很少,但做為後台任務,或者說Service,畢竟是一種消耗,而且看着不夠高大上。

 

當然如果我們需要中斷,我們也可以把這個模板改成這樣:

namespace consoledemo
{
    class Program
    {

        private static AutoResetEvent _exitEvent;

        static async Task Main(string[] args)
        
{
            bool isRuned;
            Mutex mutex = new Mutex(true, "OnlyRunOneInstance", out isRuned);
            if (!isRuned)
                return;

            _exitEvent = new AutoResetEvent(false);
            await DoWork(_exitEvent);
            _exitEvent.WaitOne();
        }

        private static async Task DoWork(AutoResetEvent _exitEvent)
        
{
            /* Your Code Here */

            _exitEvent.Set();
        }
    }
}

這樣就可以根據需要,來實現中斷程序並退出。

六、Console應用的其它運行方式

上一節介紹的Console,其實是一個應用程序。

在實際應用中,Console程序跑在Linux服務器上,我們可能會有一些其它的要求:

  1. 定時運行

Linux上有一個Service,叫cron,是一個用來定時執行程序的服務。

這個服務的設定,需要另一個命令:crontab,位置在/usr/bin下。

具體命令格式這兒不做解釋,網上隨便查。

  1. 運行到後台

命令後邊加個&字符即可:

$ ./command &
  1. 運行為Service

需要持續運行的應用,如果以Console的形態存在,則設置為Service是最好的方式。

Linux下,設置一個應用為Service很簡單,就這麼簡單三步:

第一步:在/etc/systemd/system下面,創建一個service文件,例如command.service:

[Unit]
# Service的描述,隨便寫
Description=Command

[Service]
RestartSec=2s
Type=simple
# 執行應用的默認用戶。應用如果沒有特殊要求,最好別用root運行
User=your_user_name
Group=your_group_name
# 應用的目錄,絕對路徑
WorkingDirectory=your_app_folder
# 應用的啟動路徑
ExecStart=your_app_folder/your_app
Restart=always

[Install]
WantedBy=multi-user.target

差不多就這麼個格式。參數的詳細說明可以去網上查,實際除了設置,就是運行了一個腳本。

第二步:把這個command.service加上運行權限:

# chmod +x ./command.service

第三步:註冊為Service:

# systemctl enable command.service

完成。

為了配合應用,還需要記住兩個命令:啟動和關閉Service

# #啟動Service
# systemctl start command.service
# #關閉Service
# systemctl stop command.service

七、寫在後邊的話

今天這個文章,是因為前两天,一個兄弟跑過來問我關於數據總線的實現方式,而想到的一個點。

 

很多時候,大家在寫代碼的時候,會有一種固有的思想:寫WebAPI,就想在這個框架中把所有的內容都實現了。這其實不算是一個很好的想法。WebAPI,在業務層面,就應該只是實現簡單的處理請求,返回結果的工作,而後台任務跟這個內容截然不同,通常它只做處理,不做返回 — 事實上也不太好返回,要麼客戶端等待時間太長,要麼客戶端已經斷掉了。換句話說,用WebAPI實現總線,絕不是一個好的方式。

不過,Console運行為Service,倒是一個總線應用的絕好方式。如果需要按序執行,可以配合MQ服務器,例如RabbitMQ,來實現消息的按序處理。

 

再說代碼。很多需求,本來可以用很簡單的方式實現。模式這個東西,用來面試,用來講課,都是很好的內容,但實際開發中,如果有更簡單更有效的方式,用起來!Coding的工作是實現,而不是秀技術。當然,能否找到簡單有效的方式,這個可能跟實際的技術面有關係。但這並不是一個不能跨越的坎。

多看,多想,每天成長一點點!

 

今天的代碼,在:https://github.com/humornif/Demo-Code/tree/master/0012/demo

(全文完)

 

 

微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此聲明和原文鏈接

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

帶你輕鬆了解C# Lock 關鍵字

  相信絕大多數.NET玩家和我一樣,常常使用Timer這個對象,而在WPF中使用DispatcherTimer的人也是很多,DispatcherTimer是在UI線程跑的。我們的程序中大多數都會充斥很多Timer,可以理解它是一個線程,它繼承自 System.Windows.Threading 。

  程序中也許會有一些靜態變量或是單例模式的對象來讓不同的頁面進行交互,但也就是這樣讓每個線程之間打架提供了基礎。因為資源是單獨的,就像是腳踩兩隻船的人,必定會翻車。例如一個List集合,你在一個線程中對它進行了操作,在同步瞬間的另線程中,如果不對它謹慎處理,就會造成 “集合已修改;可能無法執行枚舉操作”。當然我們說的不是關於集合的相關問題,而是關於資源分配的,當然在資源搶奪上,是在耗時的線程中才會出現的,例如下面的這張圖。

 

  這種耗時的操作,並且在同步線程中,沒有對線程進行封裝,很容易造成資源搶奪問題,假如Object是個集合,我在中間把它改了,下一秒的其它線程對它進行臟讀了,就會產生錯誤,我們可以通過Lock關鍵字。

  首先在Microsoft文檔中對Lock的說明是,lock 關鍵字可以用來確保代碼塊完成運行,而不會被其他線程中斷。這是通過在代碼塊運行期間為給定對象獲取互斥鎖來實現的。

  不過我們需要注意的是Lock本質上Monitor.Enter,Monitor.Enter會使值類型裝箱,每次Lock的是裝箱后的對象。Lock其實是類似編譯器的語法糖,因此編譯器直接限制住不能lock值類型,為啥呢,你仔細想想,每次裝箱后都是不同的對象,我怎麼判斷? object.ReferenceEquals 每次都是false…還有就是千萬不要Lock 字符串,簡單來說Lock字符串之後,只要是你以後有字符串匹配和你Lock里的內容有一樣的,那個該字符串也會被鎖定,相當於死鎖了。

  Lock和Monitor的區別不是很大,具體看以下代碼。

private static object obj = new object();
        public void LockSomething()
        {
            lock (obj)
            {
                dosomething();
            }
        }
        public void MonitorSomeThing()
        {
                Monitor.Enter(obj);
                dosomething();
                Monitor.Exit(obj);
        }
        public void dosomething()
        { 
            //做具體的事情
        }

   lock和Monitor是.NET用一個特殊結構實現的,Monitor對象是完全託管的、完全可移植的,並且在操作系統資源要求方面可能更為有效,同步速度較快,但不能跨進程同步。主要作用是鎖定臨界區,使臨界區代碼只能被獲得鎖的線程執行。Monitor.Wait和Monitor.Pulse用於線程同步,類似信號操作,個人感覺使用比較複雜,容易造成死鎖。

  lock就是封裝了Monitor.Enter和Monitor.Exit方法其實非常不難理解,只要確定Lock在啥時候用,該怎麼用就可以了,總結一句話。經常會應用於防止多線程操作導致公用變量值出現不確定的異常,用於確保操作的安全性。

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

【其他文章推薦】

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

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

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

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

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

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

工作5年,一位來自廣西測試妹子學習《自動化測試》經驗分享

本文來自一位妹子的投稿

大家好,我是黎婷,一位來自廣西南寧的測試妹子,工作5年,藉著原作者平台,今天給大家分享一下近期在學習《自動化測試實戰寶典》一書過程中的一些感想、學習心得,希望對大家有所幫助。

1. 來自大多數測試人員的困惑

在當前整個軟件行業的高速發展中,作為測試人員如何在這個急速浪潮中站穩腳跟,想必大多數人都知道需要學習更多的知識與技能,鞏固並加強自身能力,才能保證自己不被淘汰。

然而目前相當多行業內的測試人員基本都會面臨這種困惑:大體的方向是確定了,但是具體學什麼,怎麼學,落實到地的方案卻比較少有人能想明白。

2. 自己的經歷

想到自己當初在第一家公司離職選擇跳槽的時候,也是帶着這樣的迷茫進入了一家境外電商公司,初入這家公司時,正值質量管理部門大力推廣要求各種自動化測試工具使用的風潮中,基於對KPI的追求,於是開始着手查找當下各種流行的框架工具,為了KPI為了所謂的數據好看使用工具而學習測試工具,前期也是投入較多的時間,成果也是有的,但是很快也就放棄了使用:時間投入高,產出低,還不如手工測試。

然而隨着公司業務的快速發展,產品迭代頻繁,重複性的回歸工作慢慢的佔據很大一部分工作時間,加班處理工作已經成了常態,鑒於這種處境,又開始重拾了測試框架與工具,這回的目的很明確:在能保障產品質量的同時也要提高自己的工作效率,這些觀點在最近學習的書上提到關於自動化測試的意義點也是不謀而合的。

因此在這種狀況下開始主動學習工具和框架時,也多了份思考:如何利用這個框架的特點和優勢契合到實際工作的使用,能夠剛好解決工作中的痛點。帶着這個目的去學習工具使用工具,搞起的自動化,成效也是有很明顯的提升,從測試環境的回歸測試效率提升,以及後續線上核心功能的持續監控落地,都給自己實際工作帶來很大的推動促進作用,自己也有更多時間投入到更有價值的專項測試中,而不是疲於日常的重複性工作不得發展。

繼續帶着這份思考,本人最近花了一兩個星期大體研讀了《自動化測試實戰寶典 Robot Framework+Python》一書,基本成功快速地掌握了用Robot Framework工具進行接口自動化測試。

在這次學習的過程中,不僅僅能掌握到技術方面的知識,同時發覺它不只是一本工具書,更是一本有着完整且與時俱進的測試理論體系知識的書籍,書中成熟的思想理論加循序漸進的實戰講解,讓我從一個之前只聞RF名不識其真實面目的RF小白,到現在只通過一兩周時間就能快速掌握RF進行自動化測試。當然,在這麼短的時間內,很難一下子消化完這麼豐富的知識點,在接下來的測試工作學習中,希望能在本書的指引下,更深入學習RF框架來推動自己的工作更高效、高質量的交付。

3. 學習后的幾點心得

接下來想分享自己在學習該書籍過程中的幾點心得:

心得之一:筆者在接口測試項目實戰章節中講到的一個點:

做好接口測試除了在‘術’上融合工程化的最佳實踐,武裝方式方法,在‘道’上更需要有一個正確的認知和決心。正確的認知和決心更加強調的是團隊成員之間的目標是否一致,大家是不是真心認同這件事,而不是被迫地或者礙於領導下達的任務指標才去做。所有流於形式或者並非發自內心而被迫做的事情,最後收效往往都是甚微的,即便給你的是一把‘屠龍刀’,也發揮不出應有的作用。只有大家有意識地從心出發,主動尋求改變、追求質量和高效率、精益求精,再結合合適的工程實踐,才能做到‘道’和‘術’的雙重兼顧。”

對於筆者提到的這個點,跟讀者我的實際工作遇到的情況也是非常契合的。

在自己之前的公司中,也多多少少接觸到各種測試工具和框架,在工作之初去學習這類工具框架的時候,與其說初始的目的是為以後長遠的職業發展學習一項或多項技能鞏固能力,更不如說是基於領導的各種任務要求而“心不甘情不願”的去學習,在抱着這種想法去學習的技能,不僅不深入而且成果甚微,更是浪費了很多時間,最後甚至磨滅了繼續學習下去的念頭。

直到工作生涯中後期,才開始有意識自發的通過學習一些工具和技術,來真正地解決實際工作中的問題,在這個過程中領悟到學習的技術知識確切能給自己的工作帶來“收大於支”的成果時,不僅工作能力得到提升,內心也認可了技術帶來的改變,讓“屠龍寶刀”發揮其最大化作用,也真正契合了筆者提到的“道”和術的雙重兼顧。

當內心開始認可了這項東西時,會發現學習技術的路上原來如此有趣而引人入勝。利用自己所學,發揮其最大用處,會將自己的學習帶入一個良性的循環中,讓自己不只是在日常工作中得到效益,長遠來講,何嘗不是對將來的職業生涯鋪墊穩固的基石呢。

心得之二:談談學習過程中解決問題發思路。

在學習接口測試實戰項目時,在一些基礎環境配置、導入三方庫、接口認證鑒權等方面都遇到了阻礙。在解決這些問題的過程中,分享下我對於學習過程中遇到問題解決問題的一個思路。

  1. 首先自己在學習一些新技術時,心態都會放得比較平,因為面對新東西新技術,會遇到問題是在所難免的,一帆風順的狀況基本不存在,所以一定要擁有一份积極的心態,而不是遇到問題就開始心煩氣躁,不僅不利於解決問題,還很容易喪失繼續學習下去的信心。
  2. 其次就是解決問題的方法了,這次學習的RF框架,提供了詳細且比較全面的日誌信息,要學會通過具體報錯信息定位問題產生的原因。通過確定問題關鍵字在網絡上各大技術論壇社區平台進行搜索查找,對搜索到的結果也需要學會進行篩選總結,總結出搜索出來的解決方法的規律去解決問題,多次嘗試,不怕試錯,問題在這個階段80%都能得到解決。而不太建議一遇到問題就去找他人協助解決,首先溝通是有成本的,其次也會佔用他人寶貴的時間。
  3. 如果嘗試多次自己解決還是無果,接下來才是找能幫助自己解決問題可能性比較大的小夥伴,因為漫無目的的請教也是浪費雙方時間的行為。有其他小夥伴的協助,也很難有不能解決的問題了。最後解決問題之後的記錄總結也是很重要,作為學習的沉澱,可以在下次遇到同樣問題能快速解決,也可以給後續遇到同樣問題的小夥伴提供幫助。

總的來說,在這次學習RF接口測試中,自己所在部門的小夥伴都很熱心給予了很大的支持幫助,自己也才能在這麼短的時間掌握該項技能。大家能在百忙之中抽出時間給予協助是非常可貴的,作為請教者,在今後的學習中,也要提供給他人力所能及的協助。畢竟,學習是相互的,大家彙集起來的知識泉源才能壯大,也能更好推動大家一起發展。

4. 談一談RF框架可以優化的幾個點

從RF框架本身來談,它的優勢是絕對佔主導的,接下來談一談在使用RF框架中個人覺得還可以優化的幾點:

  1. RIDE編輯器性能有待提高,在加載現有的測試部的接口腳本,左側目錄樹經常出現卡頓,會一定程度影響腳本編寫效率。解決辦法之一可以嘗試用其他編輯器如pycharm等替代。

  2. 希望可以支持通過抓包工具抓取到接口數據進行轉化,自動生成符合腳本編寫格式的關鍵字,可以進一步提高編寫腳本的效率。如httprunner的har2case命令可以很方便的將har文件轉換成對應框架支持的腳本書寫規範。當然對於這塊優化點,也是有解決辦法的:可以通過自己寫工具去轉換抓到的har文件,而目前自己所在團隊也已經有大佬實現拓展了該工具。

  3. robot framework 的編輯器RIDE目前對Python3 的兼容性還不是特別好,主要是擴展庫的語法有比較多不兼容。當然RF框架本身一直是支持Python3的,若習慣使用如pycharm這些編輯器來編寫腳本,這塊不足也不存在了。但如果是習慣使用RIDE編輯器的話,對於當前主流使用的Python3維護兼容性成本還是比較高。

總得來說,本次的學習實戰時間還是比較短暫,而本書提供的技術及知識理論是非常豐富的,在接下來的學習中,還需要繼續精讀細讀,慢慢消化,利用好這把“屠龍寶刀”加強自己的“戰鬥值”。

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

【其他文章推薦】

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

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

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

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

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

面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

redis源碼分析系列文章

[Redis源碼系列]在Liunx安裝和常見API 

為什麼要從Redis源碼分析 

String底層實現——動態字符串SDS 

Redis的雙向鏈表一文全知道

前言

hello,各位小可愛們,又見面了。今天這篇文章來自去年面試閱文的面試題,結果被虐了。這一part不說了,下次專門開一篇,寫下我面試被虐的名場面,尷尬的不行,全程尬聊。哈哈哈哈,話不多說,開始把。

 

今天要寫Redis的Hash類型,如果有對Redis不熟悉,或者對其他數據類型感興趣的,可以移步上面的系列文章。(最上面的最上面最上面,重要的事情說三遍)

在Redis中Hash類型的應用非常廣泛,其中key到value的映射就通過字典結構來維護的。記筆記,此處要考。

​

API使用

API的使用比較簡單,所以以下就粗略的寫了。

插入數據hset

使用hset命令往myhash中插入兩個key,value的鍵值對,分別是(name,zhangsan)和(age,20),返回值當前的myhash的長度。

獲取數據hget

使用hget命令獲取myhash中key為name的value值。

獲取所有數據hgetall

使用hgetall命令獲取myhash中所有的key和value值。

獲取所有key

使用hkeys命令獲取myhash中所有的key值。

獲取長度

使用hlen命令獲取myhash的長度。

獲取所有value

使用hvals命令獲取myhash中所有的value值。

具體邏輯圖

hash的底層主要是採用字典dict的結構,整體呈現層層封裝。

首先dict有四個部分組成,分別是dictType(類型,不咋重要),dictht(核心),rehashidx(漸進式hash的標誌),iterators(迭代器),這裏面最重要的就是dictht和rehashidx。

接下來是dictht,其有兩個數組構成,一個是真正的數據存儲位置,還有一個用於hash過程,包括的變量分別是真正的數據table和一些常見變量。

最後數據節點,和上篇說的雙向鏈表一樣,每個節點都有next指針,方便指向下一個節點,這樣目的是為了解決hash碰撞。具體的可以看下圖。

這邊看不懂沒關係,後面會針對每個模塊詳細說明。(千萬不要看到這裏就跳過啦)

​

雙向鏈表的定義

字典結構體dict

我們先看字典結構體dict,其包括四個部分,重點是dictht[2](真正的數據)和rehashidx(漸進式hash的標誌)。具體圖如下。

具體代碼如下:

//字典結構體 
 typedef struct dict {
    dictType *type;//類型,包括一些自定義函數,這些函數使得key和value能夠存儲 
    void *privdata;//私有數據 
    dictht ht[2];//兩張hash表 
    long rehashidx; //漸進式hash標記,如果為-1,說明沒在進行hash
    unsigned long iterators; //正在迭代的迭代器數量
} dict;

 

數組結構體dictht

dictht主要包括四個部分,1是真正的數據dictEntry類型的數組,裏面存放的是數據節點;2是數組長度size;3是進行hash運算的參數sizemask,這個不咋重要,只要記住等於size-1;4是數據節點數量used,當前有多少個數據節點。

具體代碼如下:

//hash結構體 
typedef struct dictht {
    dictEntry **table;//真正數據的數組 
    unsigned long size;//數組的大小 
    unsigned long sizemask;//用戶將hash映射到table的位置索引,他的值總是等於size-1 
    unsigned long used;//已用節點數量 
} dictht;

 

數據節點dictEntry

dictEntry為真正的數據節點,包括key,value和next節點。

//每個節點的結構體  
typedef struct dictEntry {
    void *key; //key
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;//value
    struct dictEntry *next; //下一個數據節點的地址
} dictEntry;

 

 

 

擴容過程和漸進式Hash圖解

我們先來第一個部分,dictht[2]為什麼會要2個數組存放,真正的數據只要一個數組就夠了?

這其實和Java的HashMap相似,都是數據加鏈表的結構,隨着數據量的增加,hash碰撞發生的就越頻繁,每個數組後面的鏈表就越長,整個鏈表顯得非常累贅。如果業務需要大量查詢操作,因為是鏈表,只能從頭部開始查詢,等一個數組的鏈表全部查詢完才能開始下一個數組,這樣查詢時間將無線拉長。

這無疑是要進行擴容,所以第一個數組存放真正的數據,第二個數組用於擴容用。第一個數組中的節點經過hash運算映射到第二個數組上,然後依次進行。那麼過程中還能對外提供服務嗎?答案是可以的,因為他可以隨時停止,這就到了下一個變量rehashidx。(一點都不生硬的轉場,哈哈哈)

rehashidx其實是一個標誌量,如果為-1說明當前沒有擴容,如果不為-1則表示當前擴容到哪個下標位置,方便下次進行從該下標位置繼續擴容。

這樣說是不是太抽象了,還是一臉懵逼,貼心的送上擴容過程全解,一定要點贊評論多誇誇我哦。(越來越不要臉了。。。)

步驟1

首先是未擴容前,rehashidx為-1,表示未擴容,第一個數組的dictEntry長度為4,一共有5個節點,所以used為5。

步驟2

當發生擴容了,rahashidx為第一個數組的第一個下標位置,即0。擴容之後的大小為大於used*2的2的n次方的最小值,即能包含這些節點*2的2的倍數的最小值。因為當前為5個數據節點,所以used*2=10,擴容后的數組大小為大於10的2的次方的最小值,為16。從第一個數組0下標位置開始,查找第一個元素,找到key為name,value為張三的節點,將其hash過,找到在第二個數組的下標為1的位置,將節點移過去,其實是指針的移動。這邊就簡單說了。

 

步驟3

key為name,value為張三的節點移動結束后,繼續移動第一個數組dictht[0]的下標為0的後續節點,移動步驟和上面相同。

步驟4

繼續移動第一個數組dictht[0]的下標為0的後續節點都移動完了,開始移動下標為1的節點,發現其沒有數據,所以移動下標為2的節點,同時修改rehashidx為2,移動步驟和上面相同。

整個過程的重點在於rehashidx,其為第一個數組正在移動的下標位置,如果當前內存不夠,或者操作系統繁忙,擴容的過程可以隨時停止。

停止之後如果對該對象進行操作,那是什麼樣子的呢?

  • 如果是新增,則直接新增后第二個數組,因為如果新增到第一個數組,以後還是要移過來,沒必要浪費時間
  • 如果是刪除,更新,查詢,則先查找第一個數組,如果沒找到,則再查詢第二個數組。

字典的實現(源碼分析)

創建並初始化字典

首先分配內存,接着調用初始化方法_dictInit,主要是賦值操作,重點看下rehashidx賦值為-1(這驗證了剛才的圖解,-1表示未進行hash擴容),最後返回是否創建成功。

/* 創建並初始化字典 */
dict *dictCreate(dictType *type,
        void *privDataPtr)
{
    dict *d = zmalloc(sizeof(*d));
    _dictInit(d,type,privDataPtr);
    return d;
}

/* Initialize the hash table */
int _dictInit(dict *d, dictType *type,
        void *privDataPtr)
{
    _dictReset(&d->ht[0]);
    _dictReset(&d->ht[1]);
    d->type = type;
    d->privdata = privDataPtr;
    d->rehashidx = -1;//賦值為-1,表示未進行hash
    d->iterators = 0;
    return DICT_OK;
}

 

擴容

dict裏面有一個靜態方法_dictExpandIfNeed,判斷是否需要擴容。

首先判斷通過dictIsRehashing方法,判斷是否處於hash狀態,其調用的是宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1),即判斷rehashidx是否為-1,如果為-1,即不處於hash狀態,if條件為false,可以進行擴容,如果不為-1,即處於hash狀態,if條件為true,不可以進行擴容,直接返回常量DICT_OK。

接着判斷第一個數組的size是否為0,如果為0,則擴容為默認大小4,如果不為0,則執行下面的代碼。

再接着判斷是否需要擴容,if中有三個條件,具體的分析如下。

最後就是調用dictExpand擴容方法了,參數為數據節點的雙倍大小ht[0].used*2。此處驗證了上面擴容過程的數組大小16。

擴容方法比較簡單點,獲取擴容后的大小,將第二個設置新的大小。

這樣講感覺有點空,看下流程圖。

擴容流程圖

具體代碼:

static int _dictExpandIfNeeded(dict *d)
{
    //判斷是否處於擴容狀態中,通過調用宏常量#define dictIsRehashing(d) ((d)->rehashidx != -1)
    //來判斷是否可以擴容
    if (dictIsRehashing(d)) return DICT_OK;

    //判斷第一個數組size是否為0,如果為0,則調用擴容方法,大小為宏常量
    //#define DICT_HT_INITIAL_SIZE     4
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    //下面先列出if條件中所使用到的參數 
    // static int dict_can_resize = 1;數值為1表示可以擴容
    //static unsigned int dict_force_resize_ratio = 5;
    //我們來分析if條件,如果第一個數組的所有節點數量大於等於第一個數組的大小(表示節點數據已經有些多)
    //並且可用擴容(數值為1)或者所有節點數量除以數組大小大於5
    //這個條件表示擴容那個的條件,第一個就是節點必要大於等於數組長度,
    //第二點就再可以擴容和數據太多,超過5兩个中選其一
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        //調用擴容方法
        return dictExpand(d, d->ht[0].used*2);
    }
    return DICT_OK;
}

int dictExpand(dict *d, unsigned long size)
{
    dictht n;
    //獲取擴容后真正的大小,找到比size大的最小值,且是2的倍數
    unsigned long realsize = _dictNextPower(size);

    //一些判斷條件
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    if (realsize == d->ht[0].size) return DICT_ERR;

    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    //第一個hash為null,說明在初始化 
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }
    //正在hash,給第二個hash的長度設置新的, 
    d->ht[1] = n;
    d->rehashidx = 0;//設置當前正在hash 
    return DICT_OK;
}

/* 找到比size大的最小值,且是2的倍數 */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

 

 

漸進式hash

漸進式hash過程已經通過上面圖解說明,以下主要看下代碼是如何實現的,以及過程是不是對的。

擴容之後就是執行dictRehash方法,參數包括待移動的哈希表d和步驟数字n。

首先判斷標誌量rehashidx是否等於-1,如果等於-1,則表示hash完成,如果不等於-1,則執行下面的代碼。

接着進行循環,遍歷第一個數組上的每個下標,每次移動下標位置,都需要更新rehashidx值,每次加1。

再接着進行第二個循環,遍歷下標的鏈表每個節點,完成數據的遷移,主要是指針的移動和一些參數的修改。

最後,返回int數值,如果為0表示整個數據全部hash完成,如果返回1則表示部分hash結束,並沒有全部完成,下次可以通過rehashidx值繼續hash。

具體代碼如下:

//重新hash這個哈希表
 // Redis的哈希表結構公有兩個table數組,t0和t1,平常只使用一個t0,當需要重hash時則重hash到另一個table數組中
 //參數列表
 // 1. d: 待移動的哈希表,結構中存有目前已經重hash到哪個桶了
  //  2. n: N步進行rehash 
// 返回值 返回0說明整個表都重hash完成了,返回1代表未完成
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; 
    //如果當前rehashidx=-1,則返回0,表示hash完成 
    if (!dictIsRehashing(d)) return 0;
    //分n步,而且ht[0]還有沒有移動的節點 
    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        //第一個循環用來更新 rehashidx 的值,因為有些桶為空,所以 rehashidx並非每次都比原來前進一個位置,而是有可能前進幾個位置,但最多不超過 10。
        //將rehashidx移動到ht[0]有節點的下標,也就是table[d->rehashidx]非空
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];  //第二個循環用來將ht[0]表中每次找到的非空桶中的鏈表(或者就是單個節點)拷貝到ht[1]中

        /* 利用循環講數據節點移過去 */
        while(de) {
            unsigned int h;

            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }

    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    return 1;
}

總結

該篇主要講了Redis的Hash數據類型的底層實現字典結構Dict,先從Hash的一些API使用,引出字典結構Dict,剖析了其三個主要組成部分,字典結構體Dict,數組結構體Dictht,數據節點結構體DictEntry,進而通過多幅過程圖解釋了擴容過程和rehash過程,最後結合源碼對字典進行描述,如創建過程,擴容過程,漸進式hash過程,中間穿插流程圖講解。

如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

如果覺得有說的不對的地方,歡迎評論指出。

好了,拜拜咯。

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

【其他文章推薦】

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

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

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

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

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

從linux源碼看socket(tcp)的timeout

從linux源碼看socket(tcp)的timeout

前言

網絡編程中超時時間是一個重要但又容易被忽略的問題,對其的設置需要仔細斟酌。在經歷了數次物理機宕機之後,筆者詳細的考察了在網絡編程(tcp)中的各種超時設置,於是就有了本篇博文。本文大部分討論的是socket設置為block的情況,即setNonblock(false),僅在最後提及了nonblock socket(本文基於linux 2.6.32-431內核)。

connectTimeout

在討論connectTimeout之前,讓我們先看下java和C語言對於socket connect調用的函數簽名:

java:
 // 函數調用中攜帶有超時時間
 public void connect(SocketAddress endpoint, int timeout) ;
C語言:
 // 函數調用中並不攜帶超時時間
 int connect(int sockfd, const struct sockaddr * sockaddr, socklen_t socklent) 	 

操作系統提供的connect系統調用並沒有提供timeout的參數設置而java卻有,我們先考察一下原生系統調用的超時策略。

connect系統調用

我們觀察一下此系統調用的kernel源碼,調用棧如下所示:

connect[用戶態]
	|->SYSCALL_DEFINE3(connect)[內核態]
			|->sock->ops->connect

由於我們考察的是tcp的connect,其socket的內部結構如下圖所示:

最終調用的是tcp_connect,代碼如下所示:

int tcp_connect(struct sock *sk) {
	......
	// 發送SYN
	err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
	...
	/* Timer for repeating the SYN until an answer. */
	// 由於是剛建立連接,所以其rto是TCP_TIMEOUT_INIT
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	return 0;	
}

又上面代碼可知,在tcp_connect設置了重傳定時器之後return回了tcp_v4_connect再return到inet_stream_connect。我們繼續考察:

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
			int addr_len, int flags)
{
	......
	// tcp_v4_connect=>tcp_connect
	err = sk->sk_prot->connect(sk, uaddr, addr_len);
	// 這邊用的是sk->sk_sndtimeo
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
	......
	inet_wait_for_connect(sk, timeo));
	......
out:
	release_sock(sk);
	return err;

sock_error:
	err = sock_error(sk) ? : -ECONNABORTED;
	sock->state = SS_UNCONNECTED;
	if (sk->sk_prot->disconnect(sk, flags))
		sock->state = SS_DISCONNECTING;
	goto out
}

由上面代碼可見,可以採用設置SO_SNDTIMEO來控制connect系統調用的超時,如下所示:

setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);

不設置SO_SNDTIMEO

如果不設置SO_SNDTIMEO,那麼會由tcp重傳定時器在重傳超過設置的時候后超時,如下圖所示:

這個syn重傳的次數由:

cat /proc/sys/net/ipv4/tcp_syn_retries 筆者機器上是5 

來決定。那麼我們就來看一下這個重傳到底是多長時間:

tcp_connect中:
		// 設置的初始超時時間為icsk_rto=TCP_TIMEOUT_INIT為1s
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);

其重傳定時器的回掉函數為tcp_retransmit_timer:

void tcp_retransmit_timer(struct sock *sk)
{
	......
	// 檢測是否超時
	if (tcp_write_timeout(sk))
		goto out;
	......
	// icsk_rto = icsk_rto * 2,由於syn階段,所以isck_rto不會由於網絡傳輸而改變
	// 重傳的時候會以1,2,4,8指數遞增
	icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
	// 重設timer
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
out:;		
}

而計算tcp_write_timeout的邏輯則是在這篇blog中已經詳細描述過,

https://my.oschina.net/alchemystar/blog/1936433

只不過在connect時刻,重傳的計算以TCP_TIMEOUT_INIT為單位進行計算。而ESTABLISHED(read/write)時刻,重傳以TCP_RTO_MIN進行計算。那麼根據這段重傳邏輯,我們就可以計算出不同tcp_syn_retries最終表現的超時時間。如下圖所示:

那麼整理下錶格,對於系統調用,connect的超時時間為:

tcp_syn_retries timeout
1 min(so_sndtimeo,3s)
2 min(so_sndtimeo,7s)
3 min(so_sndtimeo,15s)
4 min(so_sndtimeo,31s)
5 min(so_sndtimeo,63s)

上述超時時間和筆者的實測一致。

kernel代碼版本細微變化

值得注意的是,linux本身官方發布的2.6.32源碼對於tcp_syn_retries2的解釋和RFC並不一致(至少筆者閱讀的代碼如此,這個細微的變化困擾了筆者好久,筆者下載了和機器對應的內核版本后才發現代碼改了)。而redhat發布的2.6.32-431已經修復了這個問題(不清楚具體哪個小版本修改的),並將初始RTO設置為1s(官方2.6.32為3s)。這也是,不同內核小版本上的實驗會有不同的connect timeout表現的原因(有的抓包到的重傳SYN時間間隔為3,6,12……)。以下為代碼對比:

========================>linux 內核版本2.6.32-431<========================
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))	/* RFC2988bis initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary,
					 unsigned int timeout,
					 bool syn_set)
{
	......
	unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;
	......
	timeout = ((2 << boundary) - 1) * rto_base;
	......

}
========================>linux 內核版本2.6.32.63<========================
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ))	/* RFC 1122 initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary
{
	......
	timeout = ((2 << boundary) - 1) * TCP_RTO_MIN;
	......
}

另外,tcp_syn_retries重傳次數可以在單個socket中通過setsockopt設置。

JAVA connect API

現在我們考察下java的connect api,其connect最終調用下面的代碼:

Java_java_net_PlainSocketImpl_socketConnect(...){

    if (timeout <= 0) {
    	 ......
        connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
    	 .....
    }else{
    	 // 如果timeout > 0 ,則設置為nonblock模式
        SET_NONBLOCKING(fd);
        /* no need to use NET_Connect as non-blocking */
        connect_rv = connect(fd, (struct sockaddr *)&him, len);
        /*
         * 這邊用系統調用select來模擬阻塞調用超時
         */
        while (1) {
            ......
            struct timeval t;
            t.tv_sec = timeout / 1000;
            t.tv_usec = (timeout % 1000) * 1000;
            connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t);
            ......
        }
        ......
        // 重新設置為阻塞模式
        SET_BLOCKING(fd);
        ......
    }
}

其和connect系統調用的不同點是,在timeout為0的時候,走默認的系統調用不設置超時時間的邏輯。在timeout>0時,將socket設置為非阻塞,然後用select系統調用去模擬超時,而沒有走linux本身的超時邏輯,如下圖所示:

由於沒有java並沒有設置so_sndtimeo的選項,所以在timeout為0的時候,直接就通過重傳次數來控制超時時間。而在調用connect時設置了timeout(不為0)的時候,超時時間如下錶格所示:

tcp_syn_retries timeout
1 min(timeout,3s)
2 min(timeout,7s)
3 min(timeout,15s)
4 min(timeout,31s)
5 min(timeout,63s)

socketTimeout

write系統調用的超時時間

socket的write系統調用最後調用的是tcp_sendmsg,源碼如下所示:

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		size_t size){
	......
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
	......
	while (--iovlen >= 0) {
		......
		// 此種情況是buffer不夠了
		if (copy <= 0) {
	new_segment:
		  ......
		  if (!sk_stream_memory_free(sk))
			  goto wait_for_sndbuf;

		  skb = sk_stream_alloc_skb(sk, select_size(sk),sk->sk_allocation);
		  if (!skb)
			  goto wait_for_memory;
		}
		......
	}
	......
	// 這邊等待write buffer有空間
wait_for_sndbuf:
		set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
		if (copied)
			tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
			// 這邊等待timeo長的時間
		if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
			goto do_error;
		......
out:
	// 如果拷貝了數據,則返回
	if (copied)
		tcp_push(sk, flags, mss_now, tp->nonagle);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return copied;		
out_err:
	// error的處理
	err = sk_stream_error(sk, flags, err);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return err;		
}

從上面的內核代碼看出,如果socket的write buffer依舊有空間的時候,會立馬返回,並不會有timeout。但是write buffer不夠的時候,會等待SO_SNDTIMEO的時間(nonblock時候為0)。但是如果SO_SNDTIMEO沒有設置的時候,默認初始化為MAX_SCHEDULE_TIMEOUT,可以認為其超時時間為無限。那麼其超時時間會有另一個條件來決定,我們看下sk_stream_wait_memory的源碼:

int sk_stream_wait_memory(struct sock *sk, long *timeo_p){
		// 等待socket shutdown或者socket出現err
		sk_wait_event(sk, &current_timeo, sk->sk_err ||
						  (sk->sk_shutdown & SEND_SHUTDOWN) ||
						  (sk_stream_memory_free(sk) &&
						  !vm_wait));
}						 

在write等待的時候,如果出現socket被shutdown或者socket出現錯誤的時候,則會跳出wait進而返回錯誤。在不考慮對端shutdown的情況下,出現sk_err的時間其實就是其write的timeout時間,那麼我們看下什麼時候出現sk->sk_err。

SO_SNDTIMEO不設置,write buffer滿之後ack一直不返回的情況(例如,物理機宕機)

物理機宕機后,tcp發送msg的時候,ack不會返回,則會在重傳定時器tcp_retransmit_timer到期后timeout,其重傳到期時間通過tcp_retries2以及TCP_RTO_MIN計算出來。其源碼可見筆者的blog:

https://my.oschina.net/alchemystar/blog/1936433

tcp_retries2的設置位置為:

cat /proc/sys/net/ipv4/tcp_retries2 筆者機器上是5,默認是15

SO_SNDTIMEO不設置,write buffer滿之後對端不消費,導致buffer一直滿的情況

和上面ack超時有些許不一樣的是,一個邏輯是用TCP_RTO_MIN通過tcp_retries2計算出來的時間。另一個是真的通過重傳超過tcp_retries2次數來time_out,兩者的區別和rto的動態計算有關。但是可以大致認為是一致的。

上述邏輯如下圖所示:

write_timeout表格

tcp_retries2 buffer未滿 buffer滿
5 立即返回 min(SO_SNDTIMEO,(25.6s-51.2s)根據動態rto定
15 立即返回 min(SO_SNDTIMEO,(924.6s-1044.6s)根據動態rto定

java的SocketOutputStream的sockWrite0超時時間

java的sockWrite0沒有設置超時時間的地方,同時也沒有設置過SO_SNDTIMEOUT,其直接調用了系統調用,所以其超時時間和write系統調用保持一致。

readTimeout

ReadTimeout可能是最容易導致問題的地方。我們先看下系統調用的源碼:

read系統調用

socket的read系統調用最終調用的是tcp_recvmsg, 其源碼如下:

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)
{
	......
	// 這邊timeo=SO_RCVTIMEO
	timeo = sock_rcvtimeo(sk, nonblock);
	......
	do{
		......
		// 下面這一堆判斷表明,如果出現錯誤,或者已經被CLOSE/SHUTDOWN則跳出循環
		if(copied) {
			if (sk->sk_err ||
			    sk->sk_state == TCP_CLOSE ||
			    (sk->sk_shutdown & RCV_SHUTDOWN) ||
			    !timeo ||
			    signal_pending(current))
				break;
		} else {
			if (sock_flag(sk, SOCK_DONE))
				break;

			if (sk->sk_err) {
				copied = sock_error(sk);
				break;
			}
			// 如果socket shudown跳出
			if (sk->sk_shutdown & RCV_SHUTDOWN)
				break;
			// 如果socket close跳出
			if (sk->sk_state == TCP_CLOSE) {
				if (!sock_flag(sk, SOCK_DONE)) {
					/* This occurs when user tries to read
					 * from never connected socket.
					 */
					copied = -ENOTCONN;
					break;
				}
				break;
			}
			.......
		}
		.......

		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else /* 如果沒有讀到target自己數(和水位有關,可以暫認為是1),則等待SO_RCVTIMEO的時間 */
			sk_wait_data(sk, &timeo);	
	} while (len > 0);
	......
}

上面的邏輯如下圖所示:

重傳以及探測定時器timeout事件的觸發時機如下圖所示:

如果內核層面ack正常返回而且對端窗口不為0,僅僅應用層不返回任何數據,那麼就會無限等待,直到對端有數據或者socket close/shutdown為止,如下圖所示:

很多應用就是基於這個無限超時來設計的,例如activemq的消費者邏輯。

java的SocketInputStream的sockRead0超時時間

java的超時時間由SO_TIMOUT決定,而linux的socket並沒有這個選項。其sockRead0和上面的java connect一樣,在SO_TIMEOUT>0的時候依舊是由nonblock socket模擬,在此就不再贅述了。

ReadTimeout超時表格

C系統調用:

tcp_retries2 對端無響應 對端內核響應正常
5 min(SO_RCVTIMEO,(25.6s-51.2s)根據動態rto定 SO_RCVTIMEO==0?無限,SO_RCVTIMEO)
15 min(SO_RCVTIMEO,(924.6s-1044.6s)根據動態rto定 SO_RCVTIMEO==0?無限,SO_RCVTIMEO)

Java系統調用

tcp_retries2 對端無響應 對端內核響應正常
5 min(SO_TIMEOUT,(25.6s-51.2s)根據動態rto定 SO_TIMEOUT==0?無限,SO_RCVTIMEO
15 min(SO_TIMEOUT,(924.6s-1044.6s)根據動態rto定 SO_TIMEOUT==0?無限,SO_RCVTIMEO

對端物理機宕機之後的timeout

對端物理機宕機后還依舊有數據發送

對端物理機宕機時對端內核也gg了(不會發出任何包通知宕機),那麼本端發送任何數據給對端都不會有響應。其超時時間就由上面討論的
min(設置的socket超時[例如SO_TIMEOUT],內核內部的定時器超時來決定)。

對端物理機宕機后沒有數據發送,但在read等待

這時候如果設置了超時時間timeout,則在timeout后返回。但是,如果僅僅是在read等待,由於底層沒有數據交互,那麼其無法知道對端是否宕機,所以會一直等待。但是,內核會在一個socket兩個小時都沒有數據交互情況下(可設置)啟動keepalive定時器來探測對端的socket。如下圖所示:

大概是2小時11分鐘之後會超時返回。keepalive的設置由內核參數指定:

cat /proc/sys/net/ipv4/tcp_keepalive_time 7200 即兩個小時后開始探測
cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 即每次探測間隔為75s
cat /proc/sys/net/ipv4/tcp_keepalve_probes 9 即一共探測9次

可以在setsockops中對單獨的socket指定是否啟用keepalive定時器(java也可以)。

對端物理機宕機后沒有數據發送,也沒有read等待

和上面同理,也是在keepalive定時器超時之後,將連接close。所以我們可以看到一個不活躍的socket在對端物理機突然宕機之後,依舊是ESTABLISHED狀態,過很長一段時間之後才會關閉。

進程宕后的超時

如果僅僅是對端進程宕機的話(進程所在內核會close其所擁有的所有socket),由於fin包的發送,本端內核可以立刻知道當前socket的狀態。如果socket是阻塞的,那麼將會在當前或者下一次write/read系統調用的時候返回給應用層相應的錯誤。如果是nonblock,那麼會在select/epoll中觸發出對應的事件通知應用層去處理。
如果fin包沒發送到對端,那麼在下一次write/read的時候內核會發送reset包作為回應。

nonblock

設置為nonblock=true后,由於read/write都是立刻返回,且通過select/epoll等處理重傳超時/probe超時/keep alive超時/socket close等事件,所以根據應用層代碼決定其超時特性。定時器超時事件發生的時間如上面幾小節所述,和是否nonblock無關。nonblock的編程模式可以讓應用層對這些事件做出響應。

總結

網絡編程中超時時間是個重要但又容易被忽略的問題,這個問題只有在遇到物理機宕機等平時遇不到的現象時候才會凸顯。筆者在經曆數次物理機宕機之後才好好的研究了一番,希望本篇文章可以對讀者在以後遇到類似超時問題時有所幫助。

公眾號

關注筆者公眾號,獲取更多乾貨文章:

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【String註解驅動開發】如何按照條件向Spring容器中註冊bean?這次我懂了!!

寫在前面

當bean是單實例,並且沒有設置懶加載時,Spring容器啟動時,就會實例化bean,並將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,直接返回IOC容器中的bean,不再創建新的bean。

如果bean是單實例,並且使用@Lazy註解設置了懶加載,則Spring容器啟動時,不會實例化bean,也不會將bean註冊到IOC容器中,只有第一次獲取bean的時候,才會實例化bean,並且將bean註冊到IOC容器中。

如果bean是多實例,則Spring容器啟動時,不會實例化bean,也不會將bean註冊到IOC容器中,以後每次從IOC容器中獲取bean時,都會創建一個新的bean返回。

Spring支持按照條件向IOC容器中註冊bean,滿足條件的bean就會被註冊到IOC容器中,不滿足條件的bean就不會被註冊到IOC容器中。接下來,我們就一起來探討Spring中如何實現按照條件向IOC容器中註冊bean。

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

@Conditional註解概述

@Conditional註解可以按照一定的條件進行判斷,滿足條件向容器中註冊bean,不滿足條件就不向容器中註冊bean。

@Conditional註解是由 SpringFramework 提供的一個註解,位於 org.springframework.context.annotation 包內,定義如下。

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

從@Conditional註解的源碼來看,@Conditional註解可以添加到類上,也可以添加到方法上。在@Conditional註解中,存在一個Condition類型或者其子類型的Class對象數組,Condition是個啥?我們點進去看一下。

package org.springframework.context.annotation;

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.type.AnnotatedTypeMetadata;
@FunctionalInterface
public interface Condition {
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

可以看到,Condition是一個函數式接口,對於函數式接口不了解的同學可以參見【Java8新特性】中的《【Java8新特性】還沒搞懂函數式接口?趕快過來看看吧!》一文。也可以直接查看《Java8新特性專欄》來系統學習Java8的新特性。

所以,我們使用@Conditional註解時,需要一個類實現Spring提供的Condition接口,它會匹配@Conditional所符合的方法,然後我們可以使用我們在@Conditional註解中定義的類來檢查。

@Conditional註解的使用場景如下所示。

  • 可以作為類級別的註解直接或者間接的與@Component相關聯,包括@Configuration類;
  • 可以作為元註解,用於自動編寫構造性註解;
  • 作為方法級別的註解,作用在任何@Bean方法上。

向Spring容器註冊bean

不帶條件註冊bean

我們在PersonConfig2類中新增person01()方法和person02()方法,併為兩個方法添加@Bean註解,如下所示。

@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

那麼,這兩個bean默認是否會被註冊到Spring容器中呢,我們新建一個測試用例來測試一下。在SpringBeanTest類中新建testAnnotationConfig6()方法,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);
}

我們運行testAnnotationConfig6()方法,輸出的結果信息如下所示。

person
binghe001
binghe002

從輸出結果可以看出,同時輸出了binghe001和binghe002。說明默認情況下,Spring容器會將單實例並且非懶加載的bean註冊到IOC容器中。

接下來,我們再輸出bean的名稱和bean實例對象信息,此時我們在testAnnotationConfig6()方法中添加相應的代碼片段,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

再次運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出結果如下所示。

person
binghe001
binghe002
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

可以看到,輸出了註冊到容器的bean。

帶條件註冊bean

現在,我們就要提出新的需求了,比如,如果當前操作系統是Windows操作系統,則向Spring容器中註冊binghe001;如果當前操作系統是Linux操作系統,則向Spring容器中註冊binghe002。此時,我們就需要使用@Conditional註解了。

這裏,有小夥伴可能會問:如何獲取操作系統的類型呢,別急,這個問題很簡單,我們繼續向下看。

使用Spring的ApplicationContext接口就能夠獲取到當前操作系統的類型,如下所示。

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
Environment environment = context.getEnvironment();
String osName = environment.getProperty("os.name");
System.out.println(osName);

我們將上述代碼整合到SpringBeanTest類中的testAnnotationConfig6()方法中,如下所示。

@Test
public void testAnnotationConfig6(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    Environment environment = context.getEnvironment();
    String osName = environment.getProperty("os.name");
    System.out.println(osName);

    String[] names = context.getBeanNamesForType(Person.class);
    Arrays.stream(names).forEach(System.out::println);

    Map<String, Person> beans = context.getBeansOfType(Person.class);
    System.out.println(beans);
}

接下來,我們運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果信息如下所示。

Windows 10
person
binghe001
binghe002
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18), binghe002=Person(name=binghe002, age=20)}

由於我使用的操作系統是Windows 10操作系統,所以在結果信息中輸出了Windows 10。

到這裏,我們成功獲取到了操作系統的類型,接下來,就可以實現:如果當前操作系統是Windows操作系統,則向Spring容器中註冊binghe001;如果當前操作系統是Linux操作系統,則向Spring容器中註冊binghe002的需求了。此時,我們就需要藉助Spring的@Conditional註解來實現了。

要想使用@Conditional註解,我們需要實現Condition接口來為@Conditional註解設置條件,所以,這裏,我們創建了兩個實現Condition接口的類,分別為WindowsCondition和LinuxCondition,如下所示。

  • WindowsCondition
package io.mykit.spring.plugins.register.condition;


import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Windows條件,判斷操作系統是否是Windows
 */
public class WindowsCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋信息
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類加載器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境信息
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry對象查看
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry對象向
        //Spring容器中註冊bean,移除bean,查看bean的定義,查看是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("Windows");
    }
}
  • LinuxCondition
package io.mykit.spring.plugins.register.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * @author binghe
 * @version 1.0.0
 * @description Linux條件,判斷操作系統是否是Linux
 */
public class LinuxCondition implements Condition {
    /**
     * ConditionContext:判斷條件使用的上下文環境
     * AnnotatedTypeMetadata:註釋信息
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //判斷是否是Linux系統
        //1.獲取到IOC容器使用的BeanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //2.獲取類加載器
        ClassLoader classLoader = context.getClassLoader();
        //3.獲取當前的環境信息
        Environment environment = context.getEnvironment();
        //4.獲取bean定義的註冊類,我們可以通過BeanDefinitionRegistry對象查看
        //Spring容器中註冊了哪些bean,也可以通過BeanDefinitionRegistry對象向
        //Spring容器中註冊bean,移除bean,查看bean的定義,查看是否包含某個bean的定義
        BeanDefinitionRegistry registry = context.getRegistry();
        String property = environment.getProperty("os.name");
        return property.contains("linux");
    }
}

接下來,我們就需要在PersonConfig2類中使用@Conditional註解添加條件了。添加註解后的方法如下所示。

@Conditional({WindowsCondition.class})
@Bean("binghe001")
public Person person01(){
    return new Person("binghe001", 18);
}

@Conditional({LinuxCondition.class})
@Bean("binghe002")
public Person person02(){
    return new Person("binghe002", 20);
}

此時,我們再次運行SpringBeanTest類中的testAnnotationConfig6()方法,輸出的結果信息如下所示。

Windows 10
person
binghe001
給容器中添加Person....
{person=Person(name=binghe002, age=18), binghe001=Person(name=binghe001, age=18)}

可以看到,輸出結果中不再含有名稱為binghe002的bean了,說明程序中檢測到當前操作系統為Windows10,沒有向Spring容器中註冊名稱為binghe002的bean。

@Conditional註解也可以標註在類上,標註在類上含義為:滿足當前條件,這個類中配置的所有bean註冊才能生效,大家可以自行驗證@Conditional註解標註在類上的情況

@Conditional的擴展註解

@ConditionalOnBean:僅僅在當前上下文中存在某個對象時,才會實例化一個Bean。
@ConditionalOnClass:某個class位於類路徑上,才會實例化一個Bean。
@ConditionalOnExpression:當表達式為true的時候,才會實例化一個Bean。
@ConditionalOnMissingBean:僅僅在當前上下文中不存在某個對象時,才會實例化一個Bean。
@ConditionalOnMissingClass:某個class類路徑上不存在的時候,才會實例化一個Bean。
@ConditionalOnNotWebApplication:不是web應用,才會實例化一個Bean。
@ConditionalOnBean:當容器中有指定Bean的條件下進行實例化。
@ConditionalOnMissingBean:當容器里沒有指定Bean的條件下進行實例化。
@ConditionalOnClass:當classpath類路徑下有指定類的條件下進行實例化。
@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下進行實例化。
@ConditionalOnWebApplication:當項目是一個Web項目時進行實例化。
@ConditionalOnNotWebApplication:當項目不是一個Web項目時進行實例化。
@ConditionalOnProperty:當指定的屬性有指定的值時進行實例化。
@ConditionalOnExpression:基於SpEL表達式的條件判斷。
@ConditionalOnJava:當JVM版本為指定的版本範圍時觸發實例化。
@ConditionalOnResource:當類路徑下有指定的資源時觸發實例化。
@ConditionalOnJndi:在JNDI存在的條件下觸發實例化。
@ConditionalOnSingleCandidate:當指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時觸發實例化。

@Conditional 與@Profile 的對比

Spring3.0 也有一些和@Conditional 相似的註解,它們是Spring SPEL 表達式和Spring Profiles 註解 Spring4.0的@Conditional 註解要比@Profile 註解更加高級。@Profile 註解用來加載應用程序的環境。@Profile註解僅限於根據預定義屬性編寫條件檢查。 @Conditional註釋則沒有此限制。

Spring中的@Profile 和 @Conditional 註解用來檢查”If…then…else”的語義。然而,Spring4 @Conditional是@Profile 註解的更通用法。

  • Spring 3中的 @Profile僅用於編寫基於Environment變量的條件檢查。 配置文件可用於基於環境加載應用程序配置。
  • Spring 4 @Conditional註解允許開發人員為條件檢查定義用戶定義的策略。 @Conditional可用於條件bean註冊。

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

項目工程源碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回復“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

參考:
https://www.cnblogs.com/cxuanBlog/p/10960575.html

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

數據誤操作,教你使用ApexSQLLog工具從 SQLServer日誌恢複數據!

前幾天同事不小心誤操作,將SQLServer庫的一張表的一個狀態字段給刷成了一個統一狀態,由於是update執行所以原來的相關狀態無法確定。發生這種事情的時候我的小夥伴背後 一涼。

由於是在開發試運行中的項目,還沒來得及進行備份處理,所以從備份恢復宣告失敗。就算有備份那麼恢復的也是備份時間節點的數據,意味着使用平台做的數據需要從備份時間重新做過,而且有可能有遺漏。

小夥伴問我這咋辦,首先沒有備份,那麼只有從數據庫日誌查找,然後看能不能通過日誌找回之前的數據,再還原到刷狀態之前的數據。然後就找到了ApexSQLLog工具,接下來我介紹下這款工具的使用和如何恢複數據。ApexSQLLog有幾個版本,我是用的是ApexSQLLog2014支持SqlServer更高的版本,數據庫使用的是SqlSerVer2014。

ApexSQLLog2014
提取碼: np4f

  • 首先建一個測試庫,和一張測試表。
    測試庫ApexSQLLogTest和測試用的表TestUser,然後我手動編輯了三條數據進去,保存編輯的數據。

  • 用ApexSQLLog打開測試庫日誌
    選擇要連接的數據庫,也可以從最近的session中打開,打開篩選過的記錄可以保存未session。
    然後選中要篩選的日誌文件,如果有備份數據庫文件也會自動查找到並在這裏羅列出來,自己按情況選擇。

  • 條件篩選
    我們選擇日誌文件后就進入到篩選條件選擇,可以在篩選條件裏面自由組合。
    可以選擇時間段(Time range)、操作(operations)、表(tables)。

    高級選項(advanced options)裏面還有用戶、字段條件等可以選擇。

  • 查看日誌數據
    當我們組合完篩選條件后,就進入到日誌分析界面,可以看到我們之前手動插入的三條數據實際已經在日誌裏面了,分成了三條insert語句。在選中其中一條日誌的時候在下面可以看到執行的各字段值的修改情況。

    可以點擊 下面的Row history查看記錄,Redo script可以生成執行的操作, Undo script可以還原到之前的數據。我們恢複數據就是使用Undo script。

  • 恢複數據測試。
    我們使用update語句將Status狀態全都重置為3。

update TestUser set Status=3 

然後刷新下日誌,會看到多出了三條Update日誌記錄,點擊第一條看到下面的Status字段從0變為了3。

我們選中這三條記錄右鍵或者上面的菜單欄功能,用create undo script 生成恢復sql。

--	This UNDO script was generated with ApexSQL Log 2014.04.1133 on 2020-06-10 11:18:47.601
--	NOTE: Operations in UNDO scripts are always output in descending order.
--	SERVER VIP-966\SQLEXPRESS
--	DATABASE ApexSQLLogTest

--	UPDATE (00000024:000000A0:0004) done at 2020-06-10 11:09:36.293 by VIP-966\Administrator in transaction 0000:0000034B (Committed)
BEGIN TRANSACTION 
UPDATE [dbo].[TestUser] SET [Status] = 2 WHERE [Id] = 3
IF @@ROWCOUNT <= 1 COMMIT TRANSACTION ELSE BEGIN ROLLBACK TRANSACTION; PRINT 'ERROR: STATEMENT AFFECTED MORE THAN ONE ROW. ALL THE CHANGES WERE ROLLED BACK.' END
--	UPDATE (00000024:000000A0:0003) done at 2020-06-10 11:09:36.293 by VIP-966\Administrator in transaction 0000:0000034B (Committed)
BEGIN TRANSACTION 
UPDATE [dbo].[TestUser] SET [Status] = 1 WHERE [Id] = 2
IF @@ROWCOUNT <= 1 COMMIT TRANSACTION ELSE BEGIN ROLLBACK TRANSACTION; PRINT 'ERROR: STATEMENT AFFECTED MORE THAN ONE ROW. ALL THE CHANGES WERE ROLLED BACK.' END
--	UPDATE (00000024:000000A0:0002) done at 2020-06-10 11:09:36.293 by VIP-966\Administrator in transaction 0000:0000034B (Committed)
BEGIN TRANSACTION 
UPDATE [dbo].[TestUser] SET [Status] = 0 WHERE [Id] = 1
IF @@ROWCOUNT <= 1 COMMIT TRANSACTION ELSE BEGIN ROLLBACK TRANSACTION; PRINT 'ERROR: STATEMENT AFFECTED MORE THAN ONE ROW. ALL THE CHANGES WERE ROLLED BACK.' END
GO

--	FINISHED ON 2020-06-10 11:18:47.697
--	TOTAL OPERATIONS PROCESSED 3
--	END OF FILE

最後我們就可以使用這個腳本去恢複數據了。

注意

我們在使用日誌恢復的時候如果表有主鍵會根據主鍵生成sql,如上圖sql中 where後面的條件。如果表沒有主鍵那麼生成的sql後面的where條件會帶上所有的字段。在我幫小夥伴恢複數據的時候發現他的表沒有設置主鍵,而且字段有20多個,3萬多條數據生成的sql都是100多M,還要拆分執行。
比如我把Id主鍵去了再更新下Status狀態到4,生成的sql如下,會提示沒有主鍵。

以上就是一次數據恢復的分享,如果下次你也遇到這種情況希望能幫到你。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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