網絡KPI異常檢測之時序分解算法

時間序列數據伴隨着我們的生活和工作。從牙牙學語時的“1, 2, 3, 4, 5, ……”到房價的走勢變化,從金融領域的刷卡記錄到運維領域的核心網性能指標。時間序列中的規律能加深我們對事物和場景的認識,時間序列中的異常能提醒我們某些部分可能出現問題。那麼如何去發現時間序列中的規律、找出其中的異常點呢?接下來,我們將揭開這些問題的面紗。

什麼是異常

直觀上講,異常就是現實與心理預期產生較大差距的特殊情形。如2020年春節的新型肺炎(COVID-19,coronavirus disease 2019),可以看到2月12日有一個明顯的確診病例的升高,這就是一個異常點,如下圖:

從統計上講,嚴重偏離預期的點,常見的可以通過3-sigma準則來判定。

從數學上講,它就是一個分段函數:

那麼我們有哪些方法來發現異常呢?異常分析的方法有很多,在本文中,我們主要講解時間序列分解的算法。接下來,我們先從時間序列的定義開始講起。

什麼是時間序列

前面章節,我們列舉了生活和工作中的一些時間序列的例子,但是並沒有給出定義。在本節中,我們將首先給出時間序列的定義,然後給出時間序列的分類方法,最後再給大家展示常見的時間序列。

1.時間序列的定義

時間序列是不同時間點的一系列變量所組成的有序序列。例如北京市2013年4月每日的平均氣溫就構成了一個時間序列,為了方便,我們一般認為序列中相鄰元素具有相同的時間間隔。

時間序列可以分為確定的和隨機的。例如,一個1990年出生的人,從1990年到1999年年齡可以表述為{0,1,2,…,9},這個序列並沒有任何隨機因素。這是一個確定性的時間序列。現實生活中我們所面對的序列更多的是摻雜了隨機因素的時間序列,例如氣溫、銷售量等等,這些是帶有隨機性的例子。我們說的時間序列一般是指帶有隨機性的。

那麼對於隨機性的時間序列,又如何進行分類呢?

2.時間序列的分類

從研究對象上分,時間序列分為一元時間序列和多元時間序列,如新冠肺炎例子中,只看確診病例的變化,它是一元時間序列。如果把確診病例和疑似病例聯合起來看,它是一個多元時間序列。

從時間參數上分,時間序列分為離散時間的時間序列和連續時間的時間序列。例如氣溫變化曲線,通常是按照天、小時進行預測、計算的,這個採集的時間是離散的,因此,它是一個離散時間的時間序列。再如花粉在水中呈現不規則的運動,它無時無刻不在運動,它是一個連續時間的時間序列,這就是大家眾所周知的布朗運動。在我們的工作中,我們一般遇到的都是離散時間的時間序列。

從統計特徵上分,時間序列分為平穩時間序列和非平穩時間序列。平穩序列從直觀上講,均值和標準差不隨着時間發生變化,而非平穩序列均值或者標準差一般會隨着時間發生變化。下面兩個圖分別給出平穩序列和非平穩序列的例子。

3.常見的時間序列

在本節,我們將給大家列舉一些常見的時間序列,讓大家對常見的時間序列有一個直觀的概念。

時間序列的分解

前面給大家講了異常和時間序列的概念,本章將給大家講解時間序列分解技術。

1.目的

時間序列分解是探索時序變化規律的一種方法,主要探索周期性和趨勢性。基於時序分解的結果,我們可以進行後續的時間預測和異常檢測。

2.主要組成部分

在時間序列分析中,我們經常要關注趨勢和周期。因此,一般地,我們將時序分成三個部分:趨勢部分、周期部分和殘差部分。結合下圖CO2含量的例子(見下圖)對這三個主要部分進行解釋:

1)趨勢部分:展示了CO2含量逐年增加;

2)周期部分:反應了一年中CO2含量是周期波動的;

3)殘差部分:趨勢和周期部分不能解釋的部分。

3.時序分解模型

時間序列分解基於分解模型的假設。通常,我們會考慮以下兩種模型:

加法模型適用於以下場景:

  1. 當周期性不隨着趨勢發生變化時,首選加法模型,如下圖(a);
  2. 當目標存在負值時,應選擇加法模型;

乘法模型適用於以下場景:

  1. 周期隨着隨時發生變化時,首選乘法模型,如下圖(b);
  2. 經濟數據,首選乘法模型(增長率、可解釋)。

另外,當我們不清楚選擇哪個模型時,可以兩個模型都使用,選擇誤差最小的那一個。

由於乘法模型與加法模型可以相互轉化,我們後面僅以加法模型來進行介紹。

4.時序分解算法

基於周期、趨勢分解的時序分解算法主要有經典時序分解算法、Holt-Winters算法和STL算法。經典時序分解算法起源於20世紀20年代,方法較簡單。Holt-Winters算法於1960年由Holt的學生 Peter Winters 提出,能夠適應隨着時間變化的季節項。STL(Seasonal and Trend decomposition using Loess)分解法,由Cleveland 等於1990年提出,比較通用,且較為穩健。三者之間的關係,如下圖所示:

 

4.1經典時序分解算法

經典時序分解算法是最簡單的一種分解算法,它是很多其他分解算法的基礎。該算法基於“季節部分不隨着時間發生變化”這一假設,且需要知道序列的周期。另外,該算法基於滑動平均技術。

其中,m=2k+1. 也就是說,時刻t的趨勢項的估計值可以通過前後k個時刻內的平均值得到。階數 m 越大,趨勢越光滑。由上面的公式可以看出,m一般取奇數,這保證了對稱性。但是在很多場景下,周期是偶數,例如一年有4個季度,則周期為4,是偶數。此時,需要做先做一個4階滑動平均(4-MA),再對所得結果做一個2階滑動平均(2-MA),整個過程記為 。這樣處理后的結果是對稱的,即加權的滑動平均,數學表達如下:

下面我們將講解經典時序分解算法的計算步驟。

經典時序分解算法雖然簡單、應用廣泛,但是也存在一些問題:

1) 無法估計序列最前面幾個和最後面幾個的趨勢和周期部分,例如若m=4,則無法估計前2個和后2個觀測的趨勢和周期的部分;

2) 嚴重依賴“季節性部分每個周期都是相同的”這一假設;

3) 過度光滑趨勢部分。

4.2Holt-Winters算法

在上一節中,我們介紹了經典時序分解算法,但是它嚴重依賴“季節性部分每個周期都是相同的”這一假設。為了能夠適應季節部分隨時間發生變化,Holt-Winters算法被提出。Holt-Winters算法是基於簡單指數光滑技術。首先,我們先介紹簡單指數光滑技術。

簡單指數光滑的思想主要是以下兩點:

  1. 對未來的預測:用當前的水平對下一時刻的點進行預測;
  2. 當前水平的估計:使用當前時刻的觀測值和預測值(基於歷史觀測數據的預測值,即上一時刻的水平)的加權平均作為當前水平的估計。

簡單指數光滑的模型比較簡單,如下:

Holt-Winters算法是簡單指數光滑在趨勢(可理解為水平的變化率)和季節性上的推廣,主要包括水平(前文中的趨勢項)、趨勢項和季節項三個部分。

4.3 STL算法

STL(Seasonal and Trend decomposition using Loess)是一個非常通用的、穩健性強的時序分解方法,其中Loess是一種估算非線性關係的方法。STL分解法由 Cleveland et al. (1990) 提出。

STL算法中最主要的是局部光滑技術 (locally weighted scatterplot smoothing, LOWESS or LOESS),有時也稱為局部多項式回歸擬合。它是對兩維散點圖進行平滑的常用方法,它結合了傳統線性回歸的簡潔性和非線性回歸的靈活性。當要估計某個響應變量值時,先從其預測點附近取一個數據子集(如下圖實點 是要預測的點,選取周圍的需點來進行擬合),然後對該子集進行線性回歸或二次回歸,回歸時採用加權最小二乘法(如下圖,採用的是高斯核進行加權),即越靠近估計點的值其權重越大,最後利用得到的局部回歸模型來估計響應變量的值。用這種方法進行逐點運算得到整條擬合曲線。

STL算法的主要環節包含內循環、外循環和季節項后平滑三個部分:

  • 內循環:
  • 外循環:

外循環主要作用則是引入了一個穩健性權重項,以控制數據中異常值產生的影響,這一項將會考慮到下一階段內循環的臨近權重中去。

  • 季節項后平滑:

趨勢分量和季節分量都是在內循環中得到的。循環完后,季節項將出現一定程度的毛刺現象,因為在內循環中平滑時是在每一個截口中進行的,因此,在按照時間序列重排后,就無法保證相鄰時段的平滑了,為此,還需要進行季節項的后平滑,后平滑基於局部二次擬合,並且不再需要在loess中進行穩健性迭代。

異常判斷的準則

對於異常的判斷,我們常用的有 n-sigma 準則和boxplot準則(箱線圖準則)。那這些準備是如何計算的,有哪些區別和聯繫呢?

1.n-sigma 準則

n-sigma準則有計算簡單、效率高且有很強的理論支撐,但是需要近似正態的假設,且均值和標準差的計算用到了全部的數據,因此,受異常點的影響較大。

2.boxplot 準則

為了降低異常點的影響,boxplot準則被提出。boxplot(箱線圖)是一種用作显示一組數據分散情況的統計圖,經常用於異常檢測。BoxPlot的核心在於計算一組數據的中位數、兩個四分位數、上限和下限,基於這些統計值畫出箱線圖。

根據上面的統計值就可以畫出下面的圖,超過上限的點或這個低於下限的點都可以認為是異常點。

從上面的計算上可以看出,boxplot對異常點是穩健的。

基於時序分解的異常檢測算法

在前面的章節,我們了解了時序分解的算法,也學習了異常判斷的準則,那麼如何基於時序分解進行異常檢測呢?在本章,我們將首先給出異常檢測算法的原理,再給出基於時序分解的異常檢測算法步驟。

1.異常檢測算法原理

回顧一下異常的定義,它是一個分段函數:

我們可以看到預測值(擬合值)和閾值是不知道的。對於預測值,我們可以通過找規律來猜這個預測值是多少,本章我們可以通過時序分解找周期和趨勢的規律,進而得到預測值。對於閾值,我們可以看到閾值是針對真實值和預測值的差值設置的,目的是把異常值找到,因此我們只要找到正常值的殘差和異常值的殘差的邊界即可。而我們n-sigma準則和boxplot準則就可以根據殘差把邊界找出來,即閾值。這個思考和實現的過程示意圖如下:

2.基於時序分解的異常檢測算法

Demo代碼下載地址 ,本文主要是想記錄基於時間序列的異常檢測方法,希望能夠幫到你。

 

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

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

曹工說JDK源碼(4)–抄了一小段ConcurrentHashMap的代碼,我解決了部分場景下的Redis緩存雪崩問題

曹工說JDK源碼(1)–ConcurrentHashMap,擴容前大家同在一個哈希桶,為啥擴容后,你去新數組的高位,我只能去低位?

曹工說JDK源碼(2)–ConcurrentHashMap的多線程擴容,說白了,就是分段取任務

曹工說JDK源碼(3)–ConcurrentHashMap,Hash算法優化、位運算揭秘

什麼是緩存雪崩

基本概念梳理

這個基本也是redis 面試的經典題目了,然而,網上不少博客對這個詞的定義都含糊不清,各執一詞。

主要有兩類說法:

  • 大量緩存key,由於設置了相同的過期時間,在某個時刻同時失效,導致此刻的查詢請求,全部湧向db,本來db的tps大概是幾千左右,結果湧入了幾十萬的請求,那db肯定直接就扛不住了

    這種說法下面,解決方案一般是,把過期時間增加一個隨機值,這樣,也就不會大批量的key同時失效了

  • 另外一種說法是,本來redis扛下了大部分的請求,但是,由於緩存所在的機器,發生了宕機。此時,緩存這台機器之間就連不上了,redis服務也掛了,此時,你的服務里,發現redis取不到,然後全都跑去查數據庫,那,就發生和前面一樣的情況了,請求全部湧向db,db無響應。

兩類說法,也不用覺得,這個對,那個不對,不過是一個技術名詞,當初發明這個詞的人,估計也沒想那麼多,結果傳播開來之後,就變成了現在這個樣子。

我們這裏主要採用下面那一種說法,因為下面這種說法,其實是已經包含了上面的情景。但是,下面這種場景,要複雜的多,因為redis此時就是一個完全不可信的東西了,你得想好,怎麼不讓它掛掉,那是不是應該部署sentinel、cluster集群?同時,持久化必須要開啟。

這樣呢,掛掉后,短暫的不可用之後,大概幾十s吧,緩存集群就恢復了,就又可用了。

同時,我們還得考慮,假設,現在redis掛了,我們代碼的降級策略是什麼?

大家發現redis掛了,首先,估計是會拋異常了,連接超時;拋了異常后,要直接拋到前端嗎?作為一個穩健的後端程序,那肯定是不行的,你redis掛了,數據庫又沒掛;好吧,那我們就大家一起去查數據庫。

結果,大量的查詢請求,就烏泱泱地跑去查庫了,然後,db卒。這個肯定不行。

所以,我們必須要控制的一點是,當發現某個key失效了,不是大家都去查庫,而是要進行 併發控制

什麼是併發控制?就是不能全部放過去查庫,只能放少部分,免得把脆弱的db打死。

併發控制,基本就是要爭奪去查庫的權利了,這一步,基本就是一個選舉的過程,可以通過搶鎖的方式,比如Reentrentlock,synchronized,cas也可以。

  1. 搶到鎖的線程,有資格去查庫,其他線程要麼被阻塞,要麼自旋

  2. 搶到鎖的線程,去查庫,查到數據后,將數據存放在某個地方,通知其他線程去取(如果其他線程被阻塞的話);或者,如果其他線程沒被阻塞,比如sleep 50ms,再去指定的地方拿數據那種,這種就不需要通知

    總之,如果其他線程要我們通知,我們就通知;不要我們通知,我們就不通知。

搶到鎖的線程,在構建緩存時,其他線程應該干什麼?

  1. 在while(true)里,sleep 50ms,然後再去取數據

    這種類似於忙等待,但是每次sleep一會,所以還不錯

  2. 將自己阻塞,等待搶到鎖的線程,構建完緩存后,來喚醒

  3. 在while(true)里,一直忙循環,期間一直檢查數據是否已經ok了,這種方案呢,要看裏面:檢查數據的操作,是否耗時;如果只是檢查jvm內存里的數據,那還好;否則的話,假設要去檢查redis的話,這種io比較耗時的操作的話,就不合適了,cpu會一直空轉。

本文採用的方案

主線程構建緩存時,其他線程,在while(true)里,sleep 一定時間,然後再檢查數據是否ready。

說了這麼多,好像和題目里的concurrenthashmap沒啥關係,不,是有關係的,因為,這個思路,其實就是來自於concurrentHashMap。

ConcurrentHashMap中,是怎麼去初始化底層數組的

在我們用無參構造函數,去new一個ConcurrentHashMap時,此時還不會去創建底層數組,這個是一個小優化。什麼時候創建數組呢,是在我們第一次去put的時候。

put的時候,會調用putVal。

其中,putVal代碼如下:

    transient volatile Node<K,V>[] table;

	final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
      	// 1
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
          	// 2
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
  • 1處,把field table,賦值給局部變量tab

  • 2處,如果tab為null,則進行initTable初始化

    這個2處,在多線程put的時候,是可能多個線程同時進來的。有併發問題。

我們接下來,看看initTable是怎麼解決這個問題的,畢竟,我們new數組,只new一次即可,new那麼多次,沒用,對性能有損耗。所以,這裏面肯定會多線程爭奪初始化權利的代碼。

	private transient volatile int sizeCtl;
	transient volatile Node<K,V>[] table;

	/**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab;
      	int sc;
      	
      	// 0
        while ((tab = table) == null || tab.length == 0) {
          	// 1
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
          	// 2
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                  	// 3
                    if ((tab = table) == null || tab.length == 0) {
                      	// 4
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                  	// 5
                    sizeCtl = sc;
                }
                break;
              
            }// end if
          
        }// end while
        return tab;
    }
  • 1處,這裏把sizeCtl,賦值給局部變量sc。這裏的sizeCtl是一個很重要的field,當我們new完之後,默認這個字段,要麼為0,要麼為準備創建的底層數組的長度。

    這裏去判斷是否小於0,那肯定不滿足,小於0,會是什麼意思?當某個線程,搶到了這個initTable中的底層數組的創建權利時,就會把sizeCtl改為 -1。

    所以,這裏的意思是,看看是否已經有其他線程在初始化了,如果已經有了,則直接調用:

    Thread.yield();

    這個方法的意思是,暗示操作系統,自己準備放棄cpu;但操作系統,自有它自己的線程調度規則,所以,這個方法可能沒什麼效果;我們業務代碼,這裏一般可以修改為Thread.sleep。

    這個方法調用完成后,後續也沒有其他代碼,所以會直接跳轉到循環開始處(0處代碼),判斷table是否初始化ok了,如果沒有ok,則會繼續進來。

  • 2處,使用cas,如果此時,sizeCtl的值等於sc的值,就修改sizeCtl為 -1;如果成功,則返回true,進入3處

    否則,會跳轉到0處,繼續循環。

  • 3處,雖然搶到了控制權,但是這裏還是要再判斷一下,不然可能出現重複初始化,即,不加這一行,4處的代碼,會被重複執行

  • 4處開始,這裏去執行真正的初始化邏輯。

    // 
    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
    @SuppressWarnings("unchecked")
    // 1
    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
    // 2
    table = tab = nt;
    sc = n - (n >>> 2);
    

    這裏的1處,new數組;2處,賦值給field:table;此時,因為table 這個field是volatile修飾的,所以其他線程會馬上感知到。0處代碼就不會為true了,就不會繼續循環了。

  • 5處,修改sizeCtl為正數。

這裏說下,為啥要加3處的那個判斷。

現在,假設線程A,在初始化完成后,走到了5處,修改了sizeCtl為正數;而線程B,剛好執行1處代碼:

// 1
if ((sc = sizeCtl) < 0)

那肯定,1處就不滿足了;然後就會進到2處,cas修改成功,進行初始化。沒有3處判斷的話,就會重複初始化。

基於concurrentHashmap,實現我們的緩存雪崩方案

我這裏的方案,還是比較簡單那種,就是,n個線程同時爭奪構建緩存的權利;winner線程,構建緩存后,會把緩存設置到redis;其他線程則是一直在while(true)里sleep一段時間,然後檢查redis里的數據是否不為空。

這個方案中,redis掛了這種情況,是沒在考慮中的,但是一個方案,沒辦法立馬各方面全部到位,後續我再完善一下。

不考慮緩存雪崩的代碼

@Override
public Users getUser(long userId) {
    ValueOperations<String, Users> ops = redisTemplate.opsForValue();
  	// 1
    Users s = ops.get(String.valueOf(userId));
    if (s == null) {
        /**
         * 2 這裏要去查庫獲取值
         */
        Users users = getUsersFromDB(userId);
		// 3
        ops.set(String.valueOf(users.getUserId()),users);

        return users;
    }

    return s;
}

private Users getUsersFromDB(long userId) {
    Users users = new Users();

    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("spent 1s to get user from db");
    users.setUserId(userId);
    users.setUserName("zhangsan");

    return users;
}

直接看上面的1,2,3處。就是檢查、構建緩存,設置到緩存的過程。

考慮緩存雪崩的代碼

	// 1
	private volatile int initControl;

	@Override
    public Users getUser(long userId) {
        ValueOperations<String, Users> ops = redisTemplate.opsForValue();

        Users users;
        while (true) {
          	// 2
            users = ops.get(String.valueOf(userId));
            if (users != null) {
              	// 3 
                break;
            }
			
          	// 4
            int initControlLocal = initControl;
            /**
             * 5 如果已經有線程在進行獲取了,則直接放棄cpu
             */
            if (initControlLocal < 0) {
//                log.info("initControlLocal < 0,just yield and wait");
//                Thread.yield();
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    log.warn("e:{}", e);
                }
                continue;
            }


            /**
             * 6 爭奪控制權
             */
            boolean bGotChanceToInit = U.compareAndSwapInt(this,
                    INIT_CONTROL, initControlLocal, -1);
          	// 7
            if (bGotChanceToInit) {
                try {
                  	// 8
                    users = ops.get(String.valueOf(userId));
                    if (users == null) {
                        log.info("got change to init");
                        /**
                         * 9 這裏要去查庫獲取值
                         */
                        users = getUsersFromDB(userId);
                        ops.set(String.valueOf(users.getUserId()), users);
                        log.info("init over");
                    }
                } finally {
                  	// 10
                    initControl = 0;
                }

                break;
            }// end if (bGotChanceToInit)
        }// end while


        return users;
    }
  • 1處,定義了一個field,initControl;默認為0.線程們會去使用cas,修改為-1,成功的線程,即獲得初始化緩存的權利。

    注意,要定義為volatile,保證線程間的可見性

  • 2處,去redis獲取緩存,如果不為null,直接返回

  • 4處,如果沒取到緩存,則進入此處;此處,將field:initControl賦值給局部變量

  • 5處,判斷局部變量initControlLocal,是否小於0;小於0,說明已經有線程在進行初始化了,直接contine,繼續下一次循環

  • 6處,如果當前還沒有線程在初始化,則開始競爭初始化的權利,誰成功地用cas,修改field:initControl為-1,誰就獲得這個權利

  • 7處,如果當前線程獲得了權利,則進入8處,否則,會繼續下一次循環

  • 8處,再次去redis,獲取緩存,如果不為空,則進入9處

  • 9處,查庫,設置緩存

  • 10處,修改field:initControl為0,表示退出初始化

這裏的代碼,整體和hashmap中的initTable是一模一樣的。

如何測試

上面的方案,怎麼測試沒問題呢?我寫了一段測試代碼。

    ThreadPoolExecutor executor = new ThreadPoolExecutor(100, 100,
            60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new RejectedExecutionHandler() 	{
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            log.info("discard:{}",r);
        }
    });
	
	@RequestMapping("/test.do")
    public void test() {
      	// 0
        iUsersService.deleteUser(111L);

        CyclicBarrier barrier = new CyclicBarrier(100);

        for (int i = 0; i < 100; i++) {

            executor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        barrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    long start = System.currentTimeMillis();
                  	// 1
                    Users users = iUsersService.getUser(111L);
                    log.info("result:{},spent {} ms", users, System.currentTimeMillis() - start);
                }
            });
        }

    }

上面模擬100併發下,獲取緩存。

0處,把緩存刪了,模擬緩存失效

1處,調用方法,獲取緩存。

效果如下:

可以看到,只有一個線程拿到了初始化權利。

源碼位置

https://gitee.com/ckl111/all-simple-demo-in-work-1/tree/master/redis-cache-avalanche

總結

jdk的併發包,寫得真是有水平,大家仔細研究的話,必有收穫。

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

Spring系列.依賴注入配置

依賴注入的配置

Spring的依賴注入分為基於構造函數的依賴注入基於setter方法的依賴注入

基於構造函數的依賴注入

        <!-- 通過構造器參數索引方式依賴注入 -->
	<bean id="byIndex" class="cn.javass.spring.chapter3.HelloImpl3">
		<constructor-arg index="0" value="Hello World!"/>
		<constructor-arg index="1" value="1"/>
	</bean>
	<!-- 通過構造器參數類型方式依賴注入 -->
	<bean id="byType" class="cn.javass.spring.chapter3.HelloImpl3">
		<constructor-arg type="java.lang.String" value="Hello World!"/>
		<constructor-arg type="int" value="2"/>
	</bean>
	<!-- 通過構造器參數名稱方式依賴注入 -->
	<bean id="byName" class="cn.javass.spring.chapter3.HelloImpl3">
		<constructor-arg name="message" value="Hello World!"/>
		<constructor-arg name="index" value="3"/>
	</bean>
	<!-- 通過靜態的工廠方法注入 -->
	<bean id="byName" class="cn.javass.spring.chapter3.HelloImpl3" factory-method="getBean">
		<constructor-arg name="message" value="Hello World!"/>
		<constructor-arg name="index" value="3"/>
	</bean>

基於setter方法的依賴注入

    <bean class="...HelloImpl4">
	    <property name="message" value="Hello"/>
	    <property name="index" value="1"/>  //value中的值全部是字符串形式,如果轉換出錯會報異常
	</bean>
	<bean id="Hello2" class="com.csx.personal.web.services.HelloImpl2">
		<property name="msg" ref="message"/>  //msg屬性是一個類對象
	</bean>

循環依賴:創建Bean A需要Bean B,創建Bean B需要Bean C,創建Bean C需要Bean A 這樣就形成了循環依賴。 Spring的解決方案:Spring創建Bean的時候會維護一個池,在創建A的時候會去池中查找A是否在池子中,假如發現就拋出循環依賴異常。

避免依賴注入時的循環依賴:可以使用setter方式注入,不要使用構造器形式的注入。

依賴配置常見列子

常量值注入配置

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <!-- results in a setDriverClassName(String) call -->
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
        <property name="username" value="root"/>
        <property name="password" value="masterkaoli"/>
    </bean>

注入其他Bean

這邊分別給出了一個引用當前容器和引用父容器中Bean的列子。

   <bean id="Hello2" class="com.csx.personal.web.services.HelloImpl2">
	<property name="msg">        //msg屬性是一個類對象
            <ref  bean="message"/>   //引用同一個容器中id="message"的Bean
       </property>
   </bean>
    
    <!-- 引用父容器中的Bean -->
    <!-- in the parent context -->
    <bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required as here -->
    </bean>
 
    <!-- in the child (descendant) context -->
    <bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
    </bean>

注入內部Bean

內部bean:這種bean一般只讓某個外部bean使用(和內部類相似),不讓容器中的其他Bean使用。

    <bean id="outer" class="...">
        <property name="target">
             <!-- this is the inner bean -->
            <bean class="com.example.Person"> 
                <property name="name" value="Fiona Apple"/>
                <property name="age" value="25"/>
            </bean>
        </property>
   </bean>

集合的注入

集合類的注入建議使用util命名空間

    <util:map id="myMap" key-type="java.lang.String" value-type="java.lang.String">
        <entry key="key1" value="chen"/>
        <entry key="key2" value="zhao"/>
    </util:map>

    <util:list id="myList" value-type="java.lang.String">
        <value>chen</value>
        <value>zhao</value>
    </util:list>
    
    <util:set id="mySet" value-type="java.lang.String" scope="singleton">
        <value>chen</value>
        <value>zhao</value>
    </util:set>
    
    <util:properties id="myProp" location="classpath:xx.properties"/>

null值和空字符串的注入

   <bean class="...HelloImpl4">
        <property name="message"><null/></property> //null值
        <property name="index" value=""/>  //空字符串
  </bean>

使用depends-on屬性

depends-on屬性用來指定bean的初始化順序。這個屬性只對scope是單列的bean生效

    <!--在實例化beanOne之前先實例化manager和accountDao這兩個bean-->
    <bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
	<property name="manager" ref="manager" />
    </bean>
    <bean id="manager" class="ManagerBean" />
    <bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

懶加載

bean的定義中有一個lazy-init這個屬性,用來設置單列bean在容器初始化后是否實例化這個bean。默認情況下容器是會實例化所有單例bean的,我們也建議這麼做,因為這樣能在容器初始化階段就發現bean配置是否正確。如果一個Bean按照下面的設置,lazy-init被設置為true那麼它不會被容器預初始化,只有在被使用的時候才被初始化。但是如果有一個單列類依賴了這個Bean,那麼這個被設置成懶加載的Bean還是會被預初始化。

   <bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>

如果想設置全局的單例Bean都不要預先初始化,那麼可以在xml中做如下設置:

    <beans default-lazy-init="true">
        <!-- no beans will be pre-instantiated... -->
    </beans>

Autowiring相關

當我們要往一個bean的某個屬性里注入另外一個bean,我們會使用property +ref標籤的形式。但是對於大型項目,假設有一個bean A被多個bean引用注入,如果A的id因為某種原因修改了,那麼所有引用了A的bean的ref標籤內容都得修改,這時候如果使用autowire=”byType”,那麼引用了A的bean就完全不用修改了。

 <!--autowire的用法如下,對某個Bean配置autowire模式 -->
 <!--和給Bean的屬性添加@AutoWired註解的效果一致-->
 <bean id="auto" class="example.autoBean" autowire="byType"/>

autowire的幾種模式:

  • no模式:也是默認模式,這種模式下不會進行自動屬性注入,需要我們自己通過value或者ref屬性進行配置;
  • byName模式:通過屬性的名稱自動裝配,Spring會在容器中查找名稱與bean屬性名稱一致的bean,並自動注入到bean屬性中。當然bean的屬性需要有setter方法。例如:bean A有個屬性master,master的setter方法就是setMaster,A設置了autowire=”byName”,那麼Spring就會在容器中查找名為master的bean通過setMaster方法注入到A中;
  • byType:通過類型自動裝配(注入)。Spring會在容器中查找類(Class)與bean屬性類一致的bean,並自動注入到bean屬性中,如果容器中包含多個這個類型的bean,Spring將拋出異常。如果沒有找到這個類型的bean,那麼注入動作將不會執行;
  • constructor:類似於byType,但是是通過構造函數的參數類型來匹配。假設bean A有構造函數A(B b, C c),那麼Spring會在容器中查找類型為B和C的bean通過構造函數A(B b, C c)注入到A中。與byType一樣,如果存在多個bean類型為B或者C,則會拋出異常。但時與byType不同的是,如果在容器中找不到匹配的類的bean,將拋出異常,因為Spring無法調用構造函數實例化這個bean;
  • default : 採用父級標籤(即beans標籤的default-autowire屬性)的配置。

需要注意的是上面這5中方式注入都需要我們提供相應的setter方法,通過@Autowired的方式不需要提供相應的setter方法。

自動裝配的缺點

  • 屬性和構造函數參數設置中的顯式依賴項會覆蓋自動裝配;

將Bean排除自動裝配

如果按照下面的方式配置了Bean,那麼這個Bean將不會作為自動裝配的候選Bean。但是autowire-candidate自會對byType形式的自動注入生效,如果我們是通過byName的形式進行自動注入,那麼還是能注入這個Bean。

    <bean id="auto" class="example.autoBean" autowire="byType" autowire-candidate="false"/>

如果我們只想讓某些Bean作為自動裝配的候選Bean,那麼可以進行全局設置。如果做了下面的配置,那麼只有id為bean1和bean2的Bean才會成為自動裝配的候選Bean。同時default-autowire-candidates的值支持正則表達式形式。但是強烈建議不要配置這個選項的值,使用默認的配置就行。

    <beans default-autowire-candidates="bean1,bean2">
    </beans>

方法注入(單例依賴原型Bean)

我們在配置bean的時候首先要考慮這個bean是要配置成單例模式還是其他模式。 在我們的應用中,絕大多數類都是單例類。當單例類引用單例類,或者原型類引用原型類、單例類時,我們只要像普通的配置方式就行了。但是當一個單例類引用原型類時,就會出現問題。這種情況可以使用下面的方式進行注入。

   @Service
   @Scope("prototype")
   public class MyService1 {
public void service() {
   System.out.println(this.toString() + ":id");
}
	}
	
	@Service
public abstract class MyService {

    private MyService1 service1;
    public void useService(){
     service1 = createService1();
     service1.service();
    }

    @Lookup("myService1")
    public abstract MyService1 createService1();

}
	
//也可以這樣配置bean
<bean id="serviceC" class="com.csx.demo.springdemo.service.ServiceC">
    <lookup-method bean="serviceD" name="createService"/>
</bean>
<bean id="serviceD" class="com.csx.demo.springdemo.service.ServiceD"     scope="prototype"/>

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

【其他文章推薦】

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

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

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

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

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

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

WinForm通用自動更新AutoUpdater項目實戰

目前我們做的上位機項目還是以Winform為主,在實際應用過程中,可能還會出現一些細節的修改。對於這種情況,如果上位機帶有自動更新功能,我們只需要將更新后的應用程序打包放在指定的路徑下,可以讓用戶自己來進行更新使用,會大大增加項目的便捷性。

 

01.自動更新整體思路

今天給大家介紹一下如何基於C#實現WinForm自動更新的一種方式,這種方式長期應用在項目中,提供了很多幫助,也節約了大量的時間成本,並且也使用在CMSPro軟件中,整體流程如下圖所示:

圖表 1自動更新流程

 

02.實現說明

通過上圖,可以發現這種方式是基於打包文件的方式實現的,好處在於整體打包下載,即使中途出現網絡中斷也不會有任何影響,當然相比於那種單個文件更新的方式,可能每次耗時會多一些,但是由於更新並不是一個頻繁操作的過程,這個時間是可以接受的。

(1)首先對於項目是否啟用自動更新,是通過配置的方式實現的,在實際開發中,可以使用手動更新和自動更新兩種方式,當啟用自動更新時,每次啟動應用程序都會與服務器版本號做下比較,判斷是否執行自動更新的流程。

圖表 2自動更新界面

 

(2)對於手動更新,可以通過點擊,彈出一個手動更新窗體,如下圖所示:

圖表 3手動更新界面

 

(3)對於服務器路徑、本地版本號等信息都是通過本地配置文件存儲的,因此本地需要有一個LocalVersion的配置文件,具體用什麼形式,可以自由選擇,Ini、Txt、Xml、Json都可以,如下圖所示:

圖表 4本地配置文件參考

 

(4)服務器側也會有一個配置文件,形式自由選擇,應該包含以下信息:當前服務器版本號、最新版本的程序包、該版本是否更新、該版本更新內容等信息,同時如果有新版本,應該將新版本的文件放到指定路徑下,保證最新版本包的這個路徑是有效路徑。

圖表 5服務器配置文件參考

 

(5)上位機通過將服務器的最新版本號與本地的版本號做對比,如果服務器的版本號較大,說明服務器有更新版本,因此,會根據最新版本包的地址進行下載,這裏採用的是zip文件,下載過程根據網絡及實際情況可能會耗時,因此上位機側應該做個進度條,讓用戶知道下載的進度情況,同時對於每一步的狀態也應該通過圖標的方式來進行显示,讓用戶明確更新的進度情況。

圖表 6自動更新流程

 

(6)更新完成后,系統會自動重啟新的應用程序,可以看到軟件從之前的5.3.5版本升級到最新的6.0.0版本。

圖表 7更新結果

03.整體總結

本文主要工控上位機進行自動更新的流程做了一個整體介紹,主要是介紹流程為主,給大家分享一下實現的整體思路,畢竟每個人的實現方式都可能有所不同,大家也可以在此基礎上增加一個新的功能,給自己的上位機軟件增加一點特色的同時,也給自己提供了便捷一下升級的過程的話,可以通過關注本公眾號:dotNet工控上位機,併發送關鍵詞:CMSPro,下載之後安裝運行,便會直接進入版本升級的過程。

公眾號:thinger_swj

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

【其他文章推薦】

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

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

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

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

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

[computer graphics]簡單光照模型(Phong和Blinn-Phong)和明暗處理

簡單光照模型(Phong和Blinn-Phong)和明暗處理

支持點光源和平行光,是一種簡單光照模型,它將光照分解成了三個部分,分別為

  • 漫反射
  • 鏡面反射
  • 環境光

如圖所示,是一個簡單的幾何模型。

  • \(L\)是光源方向
  • \(N\)是法線方向
  • \(R\)是反射方向
  • \(V\)是視線方向
  • \(H\)\(L\)\(V\)的平分
  • 所有向量都是單位向量

理想漫反射

當光源來自一個方向時,漫反射均勻地向各個方向傳播,與視點無關,是由物體表面粗糙不平引起的,漫反射的空間分佈是均勻的,也就是說不論從哪個方向看去,同一個點的漫反射光強都是一樣的。物體上的點\(P\),法向量為\(N\),入射光強度為\(I_p\)\(L\)\(P\)指向光源的方向。如果所有所有的向量都是單位向量,那麼有

\[I_d = I_pK_d\cdot(L\cdot N) \]

其中\(K_d=(K_{dr},K_{dg},K_{db})\)這三個分量分別是RGB三原色的漫反射係數,可以反應物體的顏色。同樣的\(I_p=(I_r,I_g,I_b)\)可以通過分量來設置光源的顏色。

鏡面反射

對於理想鏡面,反射光集中在一個方向,並遵守反射定律。對於一般的光滑表面,反射光則集中在一個範圍內,且反射定律決定的方向光強最大。所以從不同位置觀察到的鏡面反射光強不同。鏡面反射光可表示為

\[I_s = I_pK_s(R\cdot V)^{n} \]

\(R \cdot V\)計算的是反射方向和視線方向的夾角,夾角越小,強度越大。\(n\)是反射指數,反應了物體的表面的光滑程度,一般1-2000。\(n\)越大約光滑,因為n越大,例如2000,那麼當夾角很小時,例如很接近1,如0.9,但是經過2000乘方,就變得很小了,這意味着只有無限接近反射方向,才能看到高光,其他方向不行,這就表示物體很光滑。反過來,\(n\)很小那麼移動一點角度,也能看到衰弱的高光,所以光斑會比較明顯。

在鏡面反射模型中,最終要的是計算R的方向,\(R\)可以通過入射方向和法線方向計算出來

因為這裏的向量都是單位向量,只有方向不一致

\[\begin{aligned} ||L||\cos\theta &= ||M||=\cos\theta\\ &M和N的方向一致\\ R &= -L+2M \\ &=2N\cos\theta-L\\ &=2N\cdot(N\cdot L)-L \end{aligned} \]

高光區域只反映光源的顏色,漫反射才能設定物體的顏色。

環境光

光源間接對物體施加的明暗影響,在物體和環境之間多次反射。在簡單光照模型中進行了簡化,通常用一個常數來模擬環境光

\[I = I_aK_a \]

\(I_a\)是環境光強,\(K_a\)為物體對環境光的反射係數。

Phong模型

\[I = I_aK_a +I_pK_d\cdot(L\cdot N)+I_pK_s(R\cdot V)^{n} \]

Phong模型是上述三種因素的疊加,其中\(R\)的計算比較費時,需要對每一點計算一次\(R\)的值。

Blinn-Phong模型

由於Phong模型計算較為耗時,後來提出了一種對Phong模型的修改,Blinn-Phong模型。
假設:

  1. 光源在無窮遠處,光線的方向L為常數(這就意味着,對物體上所有點來說,光線的方向都是一致的,正常情況應該是光源到點的向量,每個點的光照方向都不一致)
  2. 視點在無窮遠處,視線的防線V為常數(這個同理)
  3. 此模型針對高光部分進行了修改,\(R\cdot V\)的計算用\(H\cdot N\)近似,其中\(H=(L+V)/||L+V||\),也就是\(L\)\(V\)的平分向量。當\(V\)接近\(R\)的時候,\(H\)也接近\(N\),符號高光的規律。對於所有點,\(H\)只需計算一次。

所以Blinn-Phong模型的可以表示成:

\[I = I_aK_a +I_pK_d\cdot(L\cdot N)+I_pK_s(H\cdot N)^{n} \]

(圖片中應該採用了明暗處理,不僅是光照模型)

明暗處理

如今的物體大多數用多邊形表示,一個多邊形的法線方向一致,因此一個多邊形內部的像素相同,而在鄰接出可能會有突變,感覺不連續。為了讓過度平滑,基本思想是:對多邊形的頂點計算合適的光強度,在內部進行均勻插值。其中有兩種主要的做法:

  • 計算物體表面多邊形頂點的光強,然後插值,求多邊形內部光強。
  • 對內部點的法向量進行插值,而頂點的法向量用相鄰多邊形的法向量的平均值得到。

Gouraud明暗處理(雙線性光強插值)

基本算法

  1. 計算多邊形頂點的平均法向量
  2. 用Phong模型計算頂點的平均強度
  3. 插值計算離散邊上的各點光強
  4. 插值計算多邊形區域內的各點光強

計算速度比簡單光照模型有了很大的提高,解決了顏色突變問題,但是鏡面反射效果不理想。

Phong明暗處理(雙線性法向量插值)

和Gouraud方法基本類似,只不過是對法向量插值。多邊形頂點的法向量用相鄰多邊形的法向量的平均值。而內部每個點都要計算法向量,用頂點的法向量插值得到。
這種做法效果好,可以產生正確的高光,但是計算量很大。

  • [1]維基百科
  • [2]計算機圖形學基礎教程 胡事民

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

【其他文章推薦】

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

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

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

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

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

AntD框架的upload組件上傳圖片時使用customRequest方法自定義上傳行為

本次做後台管理系統,採用的是 AntD 框架。涉及到圖片的上傳,用的是AntD的 upload 組件。

我在上一篇文章《前端AntD框架的upload組件上傳圖片時遇到的一些坑》中講到:AntD 的 upload 組件有很多坑,引起了很多人的關注。折騰過的人,自然明白其中的苦楚。

今天這篇文章,我們繼續來研究 AntD 的 upload 組件的另一個坑。

備註:本文寫於2020-06-11,使用的 antd 版本是 3.13.6。

使用 AntD 的 upload 組件做圖片的上傳,效果演示

因為需要上傳多張圖片,所以採用的是照片牆的形式。上傳成功后的界面如下:

(1)上傳中:

(2)上傳成功:

(3)圖片預覽:

代碼實現

首先,你需要讓後台同學提供好圖片上傳的接口。上一篇文章中,我們是把接口調用直接寫在了 <Upload> 標籤的 action 屬性當中。但如果你在調接口的時候,動作很複雜(比如根據業務要求,需要連續調兩個接口才能上傳圖片,或者在調接口時還要做其他的事情),這個 action 方法就無法滿足需求了。那該怎麼做呢?

好在 AntD 的 upload 組件給我們提供了 customRequest這個方法:

關於customRequest 這個方法, AntD 官方並沒有給出示例,他們只是在 GitHub 上給出了這樣一個簡短的介紹:

但這個方法怎麼用呢?用的時候,會遇到什麼問題呢?AntD 官方沒有說。我在網上搜了半天,也沒看到比較完整的、切實可行的 Demo。我天朝地大物博,網絡資料浩如煙海,AntD 可是口口聲聲被人們號稱是天朝最好用的管理後台的樣式框架。可如今,卻面臨這樣的局面。我看着你們,滿懷羡慕。

既然如此,那我就自己研究吧。折騰了一天,總算是把 customRequest 的坑踩得差不多了。

啥也不說了,直接上代碼。

採用 AntD框架的 upload 組件的 customRequest 方法,自定義上傳行為。核心代碼如下:

import React, { PureComponent } from 'react';
import { Button, Card, Form, message, Upload, Icon, Modal, Row, Col } from 'antd';
import { connect } from 'dva';
import { queryMyData, submitData } from '../api';
import { uploadImage } from '../../utils/wq.img.upload';

import styles from '../../utils/form.less';

const FormItem = Form.Item;

@Form.create()
export default class PicturesWall extends PureComponent {
  constructor(props) {
    super(props);
    const { id } = this.props.match.params;
    this.state = {
      id,
      img: undefined, // 從接口拿到的圖片字段
      imgList: [], // 展示在 antd圖片組件上的數據
      previewVisible: false,
      previewImage: '',
    };
  }

  componentDidMount() {
    const { id } = this.state;
    id && this.queryData();
  }

  // 調接口,查詢已有的數據
  queryData() {
    const { id } = this.state;
    queryMyData({
      id,
    })
      .then(({ ret, data }) => {
        if (ret == 0 && data && data.list && data.list.length) {
          const item = data.list[0];

          const img = data.img;
          const imgList = item.img
            ? [
              {
                uid: '1', // 注意,這個uid一定不能少,否則展示失敗
                name: 'hehe.png',
                status: 'done',
                url: img,
              },
            ]
            : [];

          this.setState({
            img,
            imgList,
          });
        } else {
          return Promise.reject();
        }
      })
      .catch(() => {
        message.error('查詢出錯,請重試');
      });
  }

  handleCancel = () => this.setState({ previewVisible: false });

  // 方法:圖片預覽
  handlePreview = (file) => {
    console.log('smyhvae handlePreview:' + JSON.stringify(file));
    this.setState({
      previewImage: file.url || file.thumbUrl,
      previewVisible: true,
    });
  };

  // 參考鏈接:https://www.jianshu.com/p/f356f050b3c9
  handleBeforeUpload = (file) => {
    console.log('smyhvae handleBeforeUpload file:' + JSON.stringify(file));
    console.log('smyhvae handleBeforeUpload file.file:' + JSON.stringify(file.file));
    console.log('smyhvae handleBeforeUpload file type:' + JSON.stringify(file.type));

    //限製圖片 格式、size、分辨率
    const isJPG = file.type === 'image/jpeg';
    const isJPEG = file.type === 'image/jpeg';
    const isGIF = file.type === 'image/gif';
    const isPNG = file.type === 'image/png';
    const isLt2M = file.size / 1024 / 1024 < 1;
    if (!(isJPG || isJPEG || isPNG)) {
      Modal.error({
        title: '只能上傳JPG、JPEG、PNG格式的圖片~',
      });
    } else if (!isLt2M) {
      Modal.error({
        title: '圖片超過1M限制,不允許上傳~',
      });
    }
    return (isJPG || isJPEG || isPNG) && isLt2M;
  };

  // checkImageWH  返回一個promise  檢測通過返回resolve  失敗返回reject阻止圖片上傳
  checkImageWH(file) {
    return new Promise(function (resolve, reject) {
      let filereader = new FileReader();
      filereader.onload = (e) => {
        let src = e.target.result;
        const image = new Image();
        image.onload = function () {
          // 獲取圖片的寬高
          file.width = this.width;
          file.height = this.height;
          resolve();
        };
        image.onerror = reject;
        image.src = src;
      };
      filereader.readAsDataURL(file);
    });
  }

  // 圖片上傳
  doImgUpload = (options) => {
    const { onSuccess, onError, file, onProgress } = options;

    // start:進度條相關
    // 偽裝成 handleChange裏面的圖片上傳狀態
    const imgItem = {
      uid: '1', // 注意,這個uid一定不能少,否則上傳失敗
      name: 'hehe.png',
      status: 'uploading',
      url: '',
      percent: 99, // 注意不要寫100。100表示上傳完成
    };

    this.setState({
      imgList: [imgItem],
    }); // 更新 imgList
    // end:進度條相關

    const reader = new FileReader();
    reader.readAsDataURL(file); // 讀取圖片文件

    reader.onload = (file) => {
      const params = {
        myBase64: file.target.result, // 把 本地圖片的base64編碼傳給後台,調接口,生成圖片的url
      };

      // 上傳圖片的base64編碼,調接口后,返回 imageId
      uploadImage(params)
        .then((res) => {
          console.log('smyhvae doImgUpload:' + JSON.stringify(res));
          console.log('smyhvae 圖片上傳成功:' + res.imageUrl);

          const imgItem = {
            uid: '1', // 注意,這個uid一定不能少,否則上傳失敗
            name: 'hehe.png',
            status: 'done',
            url: res.imageUrl, // url 是展示在頁面上的絕對鏈接
            imgUrl: res.imageUrl, // imgUrl 是存到 db 里的相對鏈接
            // response: '{"status": "success"}',
          };

          this.setState({
            imgList: [imgItem],
          }); // 更新 imgList
        })
        .catch((e) => {
          console.log('smyhvae 圖片上傳失敗:' + JSON.stringify(e || ''));
          message.error('圖片上傳失敗,請重試');
        });
    };
  };

  handleChange = ({ file, fileList }) => {
    console.log('smyhvae handleChange file:' + JSON.stringify(file));
    console.log('smyhvae handleChange fileList:' + JSON.stringify(fileList));

    if (file.status == 'removed') {
      this.setState({
        imgList: [],
      });
    }
  };

  submit = (e) => {
    e.preventDefault();

    this.props.form.validateFields((err, fieldsValue) => {
      if (err) {
        return;
      }

      const { id, imgList } = this.state;

      const tempImgList = imgList.filter((item) => item.status == 'done'); // 篩選出 status = done 的圖片
      const imgArr = [];
      tempImgList.forEach((item) => {
        imgArr.push(item.imgUrl);
        // imgArr.push(item.url);
      });

      submitData({
        id,
        img: imgArr[0] || '', // 1、暫時只傳一張圖片給後台。如果傳多張圖片,那麼,upload組件需要進一步完善,比較麻煩,以後有需求再優化。2、如果圖片字段是選填,那就用空字符串兜底
      })
        .then((res) => {
          if (res.ret == 0) {
            message.success(`${id ? '修改' : '新增'}成功,自動跳轉中...`);

          } else if (res.ret == 201 || res.ret == 202 || res.ret == 203 || res.ret == 6) {
            return Promise.reject(res.msg);
          } else {
            return Promise.reject();
          }
        })
        .catch((e) => {
          message.error(e || '提交失敗,請重試');
        });
    });
  };

  render() {
    const { id, imgList } = this.state;
    console.log('smyhvae render imgList:' + JSON.stringify(imgList));
    const { getFieldDecorator } = this.props.form;
    const formItemLayout = {
      labelCol: { span: 3 },
      wrapperCol: { span: 10 },
    };
    const buttonItemLayout = {
      wrapperCol: { span: 10, offset: 3 },
    };

    const uploadButton = (
      <div>
        <Icon type="plus" />
        <div className="ant-upload-text">Upload</div>
      </div>
    );

    return (
      <Card title={id ? '修改信息' : '新增信息'}>
        <Form onSubmit={this.submit} layout="horizontal">

          {/* 新建圖片、編輯圖片 */}
          <FormItem label="圖片" {...formItemLayout}>
            {getFieldDecorator('img', {
              rules: [{ required: false, message: '請上傳圖片' }],
            })(
              <Upload
                action="2"
                customRequest={this.doImgUpload}
                listType="picture-card"
                fileList={imgList}
                onPreview={this.handlePreview}
                beforeUpload={this.handleBeforeUpload}
                onChange={this.handleChange}
              >
                {imgList.length >= 1 ? null : uploadButton}
              </Upload>
            )}
          </FormItem>
          <Row>
            <Col span={3} />
            <Col span={18} className={styles.graytext}>
              注:圖片支持JPG、JPEG、PNG格式,小於1M,最多上傳1張
            </Col>
          </Row>

          <FormItem {...buttonItemLayout}>
            <Button type="primary" htmlType="submit">
              提交
            </Button>
          </FormItem>
        </Form>

        {/* 圖片點開預覽 */}
        <Modal visible={this.state.previewVisible} footer={null} onCancel={this.handleCancel}>
          <img alt="example" style={{ width: '100%' }} src={this.state.previewImage} />
        </Modal>
      </Card>
    );
  }
}

參考鏈接

注意file的格式:https://www.lmonkey.com/t/oREQA5XE1

Demo在線演示:

  • https://stackoverflow.com/questions/58128062/using-customrequest-in-ant-design-file-upload

fileList 格式在線演示:

  • https://stackoverflow.com/questions/51514757/action-function-is-required-with-antd-upload-control-but-i-dont-need-it

ant design Upload組件的使用總結:https://www.jianshu.com/p/0aa4612af987

antd上傳功能的CustomRequest:https://mlog.club/article/3832743

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

大話性能測試系列(1)- 性能測試的基本概念

如果你對性能測試感興趣,但是又不熟悉理論知識,可以看下面的系列文章

https://www.cnblogs.com/poloyy/category/1620792.html

 

學習前的認知

我們在學習性能測試之前,需要有個新的認識:性能測試,不再是像功能測試一樣單純的找 Bug,而是去找性能指標

 

轉變思維

  • 在做功能測試、自動化測試的時候,我們基本都是依託界面進行測試,也稱 GUI 測試,我們的目的就是為了跑通功能、程序,並成功找到 Bug
  • 但在做性能測試的時候,我們大部分是 headless 模式(所謂的:無頭,無界面模式),目的不再是單純的為了找到 Bug,而是要分析性能指標等等(後續講到)

 

性能測試的時間一般會比自動化、功能測試長,為啥?

  • 因為性能測試的步驟跟自動化、功能測試的步驟不一樣,比如說前期的準備(了解系統,環境搭建),後期的壓力測試(7*24h)等等
  • 在後面,我們通過講述性能測試步驟來仔細了解

 

性能測試一定要工具,手工不行嗎?

  • 性能測試是模擬系統在被很多很多用戶同時使用時,系統能不能正常使用和提供服務
  • 重點:很多很多用戶
  • 功能測試:一個人點點點就知道功能通不通,有沒有 Bug 了
  • 性能測試:用手工的話,可以模擬幾個、十幾個用戶,但是當需要模擬上千萬個用戶時,手工又怎麼模擬數據量多的場景呢?
  • 類比,吃飯場景:一個人可以吃好幾碗,但是叫你吃幾百碗是不可能的
  • 結論:工具就可以模擬大數據量的場景,可以做到人做不到的事情

 

大數據量測試是性能測試嗎?

大數據量測試

簡單理解:一個接口返回的數據比較多(假設:不使用分頁,把所有數據同時返回)

 

結論

  • 返回大數據量的接口的響應時間會變長
  • 這麼大的數據量,我們需要考慮:網絡傳輸數據、服務器查詢這些數據、服務器處理這些數據等等分別需要多少時間
  • 這已經跟響應時間掛鈎,所以已經屬於性能測試的範圍,但不歸納於性能分析範圍

 

大數據測試是性能測試嗎?

大數據測試的功能屬於功能測試哦

 

性能測試過程發現問題需要立即提交嗎?

在性能測試過程中發現一些問題,假設定位到某一段代碼有問題,可以截圖提交 Bug 給開發,但這並不是我們性能測試的最終目的,最終目的是找出性能指標

 

有哪些性能指標?

  • 比如說響應時間:10個人、100個人 、1000個人 、10000個人向服務器發起請求,服務器響應請求的平均響應時間是多少,這就是一個指標
  • 又好比TPS:服務器在當前的配置下,不同用戶數發起請求,服務器的 TPS 處理能力是多少,這也是一個指標
  • 後續詳細介紹

 

性能測試中發現的 Bug 

  • 性能測試過程中發現的 Bug 屬於一個衍生品,並不是最終得到的結果
  • 但像功能測試,最終目的就是為了找出 Bug

 

關於這個問題的總結

  • 做性能測試,當數據量變大后,會出現連接超時、連接拒絕、500、502異常問題;在性能測試中,這些異常問題基本都會出現的,但不會去立即提 Bug
  • 對於性能測試工程師,我們要做的是分析為什麼在當前數據量下會出現連接超時、連接拒絕,響應時間超時、服務器異常等異常問題
  • 這就需要我們去分析性能瓶頸,並不會單獨去某個異常問題出現在哪裡,而是分析為什麼會出現這個異常問題,分析的是服務器或者是代碼,而不是讓開發人員馬上來修復這些異常問題

 

我們常說的壓測是指壓力測試嗎?

  • 並不是,而是指負載測試,一般都是為了找出系統的最大負載量
  • 就好像你老闆說:你去壓測下,看看系統能支撐多少用戶同時訪問我們的系統

 

什麼是性能測試?

狹義理解

  • 通過工具,找出或獲得系統在不同工況下的性能指標值
  • 性能測試過程中,重點是找出性能指標,而不再是找出 Bug,
  • 性能測試的產出絕對不只是 Bug

 

場景類比

跑步100米,用時多少?運動員的心跳、步伐頻率是多少?

  1. 跑步100米:業務場景
  2. 用時多少:響應時間
  3. 運動員的心跳、步伐:性能指標值

性能指標值和響應時間是否滿足當前業務場景的最低要求(合格線)

 

什麼時候能找出性能指標值

假設當前有一個業務

電商系統,下單業務,目前還不知道系統支持多少人同時下單,那麼我們需要找到服務器能正常支持多少人同時下單

 

性能測試初始階段(第一次做)

  • 先把基礎的性能指標值找出來(第一次性能測試也叫做基準測試)
  • 比如:100個人同時下單系統正常,但120個人同時下單就會出現部分請求的響應時間超長,連接異常
  • 那麼100-120範圍內的某個值就是當前服務器能達到的性能指標值(基準值)

 

版本迭代,進行第二次做性能測試,重新跑一遍之前的性能腳本

  • 又會得到一些性能指標值,對比上個版本的性能指標值,看是否有優化(性能變化)
  • 假設這個時候120個人同時下單是正常的,150個人才有異常,那麼接口已經有優化了

 

假設公司是從0開始做性能測試

  • 第一階段:做好性能測試,得到性能指標值
  • 第二階段:假設性能比之前差,哪些性能指標值不滿足預期值,就需要分析是哪裡有問題

 

廣義理解

  • 只要與服務器性能指標相關的測試都屬於性能測試
  • 比如:響應時間、併發用戶數、服務器處理能力、吞吐量等性能指標
  • 負載測試、壓力測試、容量測試、可靠性測試都屬於性能測試
  • 通常嘴巴上說的做性能測試就是廣義的性能測試,它包括了很多內容,並不只是針對某一個測試類型

 

“官方”解釋

以下含義來源高老的解釋,比較“官方”的術語

  1. 性能測試針對系統的性能指標,建立性能測試模型
  2. 制定性能測試方案
  3. 制定監控策略
  4. 在場景條件下執行性能場景
  5. 分析判斷性能瓶頸並調優
  6. 最終得出性能結果來評估系統的性能指標是否滿足既定值

其實也算是一個簡潔描述的性測試流程了

 

注意

  • 性能測試不像自動化測試那樣很多東西大家都是公認的,性能測試沒有一套標準的知識體系,只能說是相似的
  • 基本每個人都有自己的一套知識體系,就好像高老也會說他給性能測試的定義很大可能會被轟炸一樣
  • 只要屬於自己的知識體系建立起來了,那麼就能助力你正確的完成性能測試
  • 不用太過糾結於哪個人對性能測試概念的解釋是最準確的

目前博主是正在學習性能測試的小白一枚,希望通過通俗簡單的術語來學懂性能測試,打造屬於自己的知識體系,歡迎大家進群與我溝通(870155189)

 

 什麼是負載測試?

概念

  • 逐步增加系統負載,測試系統性能變化,並最終確定系統所能承受的最大負載量
  • 通俗理解:看看你幾斤幾兩

 

如何增加負載

通過增加“用戶數”,就是常說的併發數

 

場景類比

天平秤,稱東西的時候,需要逐步加砝碼,最終達到砝碼和物品重量的平衡點,因為它不可能一下子就達到平衡點【好比不可能一下子找到系統能承受的最大負載量】

  • 稱東西:業務場景
  • 加砝碼:逐步加壓
  • 達到平衡點:找到最大負載量

 

實際場景

  • 有一個業務,增加到40個人的時候,服務器還能正常使用,沒有異常
  • 當你增加到50個人的時候,服務器已經開始有異常了,那麼就能確定40-50之間某個值就是系統所能承受的最大負載量【出現性能拐點,找到了服務器性能瓶頸的範圍值】
  • 最後減小加壓梯度(比如:從40個人開始每次增加1個人、2個人),確認最大負載量【確認性能拐點】

 

服務器又有哪些可能會出現的異常呢

  • 響應時間超長:正常服務器處理請求時間是 1s,但現在變成3s – 5s
  • 服務報錯:無法同時正常響應多個請求
  • 服務器宕機:系統完全用不了

 

什麼是壓力測試?

概念

  • 在較大的性能壓力下,持續運行一個比較長的時間,看看系統服務是否正常及系統資源的利用率情況
  • 通俗理解:鴨梨山大!
  • 關鍵字:較大壓力 + 較長時間
  • 注意:不是滿負荷壓力哦

 

場景類比

問:大家什麼時候會覺得工作壓力大?

答:996、007;因為你不會覺得955壓力山大吧

結論:所以在我們日常工作中,長時間工作強度高,才會覺得壓力大;如果你一周就加班一天也說壓力大…(那就別干這一行了)

 

壓力測試用來幹嘛的

測試系統的穩定性

 

類比

工作壓力大,你還能堅持下去(那穩定性肯定好吧),那如果你很快就離職了(那穩定性肯定差,都宕機罷工了)

 

什麼時候會做壓力測試

  • 生產環境下,系統隔三差五的出現不穩定的情況
  • 這個時候,就需要通過壓力測試去測試系統的穩定性情況

 

啥情況算不穩定?穩定性差?

隔三差五的出現下面的情況

  • 服務異常:響應錯誤、響應時間超時等
  • 服務器出現異常:宕機

 

怎麼分析是服務異常還是服務器異常 

  • 如果所有請求都是一片紅,應用程序發送的所有請求都報紅,就是服務器出現了異常
  • 如果有些請求偶爾成功響應,偶爾又失敗,則是服務異常,出現不穩定的情況

 

如何取壓力值

  • 在負載測試中,我們確認了系統所能承受的最大負載量
  • 壓力值 < 最大負載量,一般取80%左右

 

靈魂拷問

負載測試一般時間比較短,壓力測試時間比較長,持續運行時間短就能正常使用,但持續運行時間長就可能崩掉了,這是什麼原因呢?

 

場景類比

  • 栗子一:電腦保持開機狀態很長時間,會逐漸變卡,因為內存的東西會越來越多,得不到有效的回收, 就會越來越卡
  • 栗子二:當你經常工作壓力很大,且你的心理所能承受的壓力逐漸達到最大值時,你就可能會選擇離職

 

總結

壓力測試長時間運行,可能會逐漸增加系統的內存佔用空間,若得不到有效的內存回收,當達到內存最大值時,系統就會崩掉

 

壓力測試持續運行時間要多久?

  • 標準性能測試裏面,一般是7*24小時,或者是它的倍數
  • 但是實際工作中,並不會這麼久,否則成本太高
  • 所以會以小時為單位,比如:4個小時、8個小時…晚上下班之後做,第二天早上上班看結果

 

先負載測試還是壓力測試?

  • 先負載測試
  • 負載測試可以找到服務器性能瓶頸的範圍值,若生產環境中系統穩定性較差,再做壓力測試
  • 所以壓力測試是可做可不做的

 

什麼是可靠性測試?

概念

  • 在給定的一定的業務壓力下,持續運行一段時間,查看系統是否穩定
  • 關鍵字:是否穩定,一定業務壓力
  • 注意:不是較大壓力哦

 

業務場景栗子

電商秒殺場景,幾十個商品幾十萬個人同時秒殺搶購

 

如何理解可靠性測試

  1. 編寫性能腳本:假設一秒內有一萬個人同時發起請求
  2. 有壓力嗎?,一萬個人同時發起請求
  3. 但是持續時間,不像壓力測試一樣需要持續一段時間
  4. 目的是為了驗證當這麼多人同時發起請求時,成功秒殺的用戶能否繼續完成後續下單付款等操作【一定業務壓力下,系統是否穩定運行】

 

什麼是容量測試?

概念

  • 在一定的軟、硬件條件下,在數據庫不同數據量級數據量的情況下,對系統中讀/寫比較多的業務進行測試,從而獲得不同數據量級下的性能指標值
  • 關鍵字:不同數據量級

 

數據庫數據量對性能測試結果有沒有影響?

肯定有

  • 比如數據庫已經有幾百條數據和幾百萬條數據,查詢的速度肯定不一樣,所以肯定會影響性能測試結果
  • 數據量級的差異,會影響TPS、響應時間、網絡等

 

場景類比

從一袋米中找一個綠豆,和一碗米中找一個綠豆,找的時間肯定是千差萬別的

 

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

Flink 如何分流數據

  • 場景
  • 分流方式
  • 如何分流
    • 使用Filter分流
    • 使用Split分流
    • 使用Side Output分流

場景

獲取流數據的時候,通常需要根據所需把流拆分出其他多個流,根據不同的流再去作相應的處理。

舉個例子:創建一個商品實時流,商品有季節標籤,需要對不同標籤的商品做統計處理,這個時候就需要把商品數據流根據季節標籤分流。

分流方式

  • 使用Filter分流
  • 使用Split分流
  • 使用Side Output分流

如何分流

先模擬一個實時的數據流

import lombok.Data;
@Data
public class Product {
    public Integer id;
    public String seasonType;
}

自定義Source

import common.Product;
import org.apache.flink.streaming.api.functions.source.SourceFunction;

import java.util.ArrayList;
import java.util.Random;

public class ProductStremingSource implements SourceFunction<Product> {
    private boolean isRunning = true;

    @Override
    public void run(SourceContext<Product> ctx) throws Exception {
        while (isRunning){
            // 每一秒鐘產生一條數據
            Product product = generateProduct();
            ctx.collect(product);
            Thread.sleep(1000);
        }
    }

    private Product generateProduct(){
        int i = new Random().nextInt(100);
        ArrayList<String> list = new ArrayList();
        list.add("spring");
        list.add("summer");
        list.add("autumn");
        list.add("winter");
        Product product = new Product();
        product.setSeasonType(list.get(new Random().nextInt(4)));
        product.setId(i);
        return product;
    }
    @Override
    public void cancel() {

    }
}

輸出:

使用Filter分流

使用 filter 算子根據數據的字段進行過濾。

import common.Product;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import source.ProductStremingSource;

public class OutputStremingDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Product> source = env.addSource(new ProductStremingSource());

        // 使用Filter分流
        SingleOutputStreamOperator<Product> spring = source.filter(product -> "spring".equals(product.getSeasonType()));
        SingleOutputStreamOperator<Product> summer = source.filter(product -> "summer".equals(product.getSeasonType()));
        SingleOutputStreamOperator<Product> autumn  = source.filter(product -> "autumn".equals(product.getSeasonType()));
        SingleOutputStreamOperator<Product> winter  = source.filter(product -> "winter".equals(product.getSeasonType()));
        source.print();
        winter.printToErr();

        env.execute("output");
    }
}

結果輸出(紅色為季節標籤是winter的分流輸出):

使用Split分流

重寫OutputSelector內部類的select()方法,根據數據所需要分流的類型反正不同的標籤下,返回SplitStream,通過SplitStream的select()方法去選擇相應的數據流。

只分流一次是沒有問題的,但是不能使用它來做連續的分流。

SplitStream已經標記過時了

public class OutputStremingDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Product> source = env.addSource(new ProductStremingSource());

        // 使用Split分流
        SplitStream<Product> dataSelect = source.split(new OutputSelector<Product>() {
            @Override
            public Iterable<String> select(Product product) {
                List<String> seasonTypes = new ArrayList<>();
                String seasonType = product.getSeasonType();
                switch (seasonType){
                    case "spring":
                        seasonTypes.add(seasonType);
                        break;
                    case "summer":
                        seasonTypes.add(seasonType);
                        break;
                    case "autumn":
                        seasonTypes.add(seasonType);
                        break;
                    case "winter":
                        seasonTypes.add(seasonType);
                        break;
                    default:
                        break;
                }
                return seasonTypes;
            }
        });
        DataStream<Product> spring = dataSelect.select("machine");
        DataStream<Product> summer = dataSelect.select("docker");
        DataStream<Product> autumn = dataSelect.select("application");
        DataStream<Product> winter = dataSelect.select("middleware");
        source.print();
        winter.printToErr();

        env.execute("output");
    }
}

使用Side Output分流

推薦使用這種方式

首先需要定義一個OutputTag用於標識不同流

可以使用下面的幾種函數處理流發送到分流中:

  • ProcessFunction
  • KeyedProcessFunction
  • CoProcessFunction
  • KeyedCoProcessFunction
  • ProcessWindowFunction
  • ProcessAllWindowFunction

之後再用getSideOutput(OutputTag)選擇流。

public class OutputStremingDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStreamSource<Product> source = env.addSource(new ProductStremingSource());

        // 使用Side Output分流
        final OutputTag<Product> spring = new OutputTag<Product>("spring");
        final OutputTag<Product> summer = new OutputTag<Product>("summer");
        final OutputTag<Product> autumn = new OutputTag<Product>("autumn");
        final OutputTag<Product> winter = new OutputTag<Product>("winter");
        SingleOutputStreamOperator<Product> sideOutputData = source.process(new ProcessFunction<Product, Product>() {
            @Override
            public void processElement(Product product, Context ctx, Collector<Product> out) throws Exception {
                String seasonType = product.getSeasonType();
                switch (seasonType){
                    case "spring":
                        ctx.output(spring,product);
                        break;
                    case "summer":
                        ctx.output(summer,product);
                        break;
                    case "autumn":
                        ctx.output(autumn,product);
                        break;
                    case "winter":
                        ctx.output(winter,product);
                        break;
                    default:
                        out.collect(product);
                }
            }
        });

        DataStream<Product> springStream = sideOutputData.getSideOutput(spring);
        DataStream<Product> summerStream = sideOutputData.getSideOutput(summer);
        DataStream<Product> autumnStream = sideOutputData.getSideOutput(autumn);
        DataStream<Product> winterStream = sideOutputData.getSideOutput(winter);

        // 輸出標籤為:winter 的數據流
        winterStream.print();

        env.execute("output");
    }
}

結果輸出:

更多文章:www.ipooli.com

掃碼關注公眾號《ipoo》

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

時間序列神器之爭:Prophet VS LSTM

一、需求背景

我們福祿網絡致力於為廣大用戶提供智能化充值服務,包括各類通信充值卡(比如移動、聯通、電信的話費及流量充值)、遊戲類充值卡(比如王者榮耀、吃雞類點券、AppleStore充值、Q幣、鬥魚幣等)、生活服務類(比如肯德基、小鹿茶等),網娛類(比如QQ各類鑽等),作為一個服務提供商,商品質量的穩定、持續及充值過程的便捷一直是我們在業內的口碑。
在整個商品流通過程中,如何做好庫存的管理,以充分提高庫存運轉周期和資金使用效率,一直是個難題。基於此,我們提出了智能化的庫存管理服務,根據訂單數據及商品數據,來預測不同商品隨着時間推移的日常消耗情況。

二、算法選擇

目前成熟的時間序列預測算法很多,但商業領域性能優越的卻不多,經過多種嘗試,給大家推薦2種時間序列算法:facebook開源的Prophet算法和LSTM深度學習算法。
現將個人理解的2種算法特性予以簡要說明:

  • (1)、在訓練時間上,prophet幾十秒就能出結果,而lstm往往需要1個半小時,更是隨着網絡層數和特徵數量的增加而增加。
  • (2)、Prophet是一個為商業預測而生的時間序列預測模型,因此在很多方便都有針對性的優化,而lstm的初衷是nlp。
  • (3)、Prophet無需特徵處理即可使用,參數調優也明確簡單。而lstm則需要先進行必要的特徵處理,其次要進行正確的網絡結構設計,因此lstm相對prophet更為複雜。
  • (4)、Lstm需要更多的數據進行學習,否則無法消除欠擬合的情形。而prophet不同,prophet基於統計學,有完整的數學理論支撐,因此更容易從少量的數據中完成學習。
  • (5)、傳統的時間序列預測算法只支持單緯度,但LSTM能支持多緯度,也就是說LSTM能考慮促銷活動,目標用戶特性,產品特性等

三、數據來源

  • (1)、訂單數據
  • (2)、產品分類數據

四、數據形式

time,product,cnt
2019-10-01 00,**充值,6
2019-10-01 00,***遊戲,368
2019-10-01 00,***,1
2019-10-01 00,***,11
2019-10-01 00,***遊戲,17
2019-10-01 00
,三網***,39
2019-10-01 00,**網,6
2019-10-01 00,***,2

字段說明:

  • Time:小時級時間
  • Product:產品名稱或產品的分類名稱,目前使用的是產品2級分類,名稱
  • Cnt:成功訂單數量
    目前的時間序列是由以上time和cnt組成,product是用於區分不同時間序列的字段。

五、特徵處理

時間序列一般不進行特徵處理,當然可以根據具體情況進行歸一化處理或是取對數處理等。

六、算法選擇

目前待選的算法主要有2種:

  • (1)、Prophet
    Facebook開源的時間序列預測算法,考慮了節假日因素。
  • (2)、LSTM
    優化后的RNN深度學習算法。

七、算法說明

7.1 prophet

7.1.1Prophet的核心是調參,步驟如下:
  • 1、首先我們去除數據中的異常點(outlier),直接賦值為none就可以,因為Prophet的設計中可以通過插值處理缺失值,但是對異常值比較敏感。
  • 2、選擇趨勢模型,默認使用分段線性的趨勢,但是如果認為模型的趨勢是按照log函數方式增長的,可設置growth=’logistic’從而使用分段log的增長方式
  • 3、 設置趨勢轉折點(changepoint),如果我們知道時間序列的趨勢會在某些位置發現轉變,可以進行人工設置,比如某一天有新產品上線會影響我們的走勢,我們可以將這個時刻設置為轉折點。如果自己不設置,算法會自己總結changepoint。
  • 4、 設置周期性,模型默認是帶有年和星期以及天的周期性,其他月、小時的周期性需要自己根據數據的特徵進行設置,或者設置將年和星期等周期關閉。
    設置節假日特徵,如果我們的數據存在節假日的突增或者突降,我們可以設置holiday參數來進行調節,可以設置不同的holiday,例如五一一種,國慶一種,影響大小不一樣,時間段也不一樣。
  • 5、 此時可以簡單的進行作圖觀察,然後可以根據經驗繼續調節上述模型參數,同時根據模型是否過擬合以及對什麼成分過擬合,我們可以對應調節seasonality_prior_scale、holidays_prior_scale、changepoint_prior_scale參數。

以上是理論上的調參步驟,但我們在實際情況下在建議使用grid_search(網格尋參)方式,直接簡單效果好。當機器性能不佳時網格調參配合理論調參方法可以加快調參速度。建議初學者使用手動調參方式以理解每個參數對模型效果的影響。

holiday.csv

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from fbprophet import Prophet

data = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')


def get_product_data(name, rule=None):
    product = data[data['product'] == name][['cnt']]
    product.plot()
    
if rule is not None:
        product = product.resample(rule).sum()
    product.reset_index(inplace=True)
    product.columns = ['ds', 'y']
    return product


holidays = pd.read_csv('holiday.csv', parse_dates=['ds'])
holidays['lower_window'] = -1

holidays = holidays.append(pd.DataFrame({
    'holiday': '雙11',
    'ds': pd.to_datetime(['2019-11-11', '2020-11-11']),
    'lower_window': -1,
    'upper_window': 1,
})).append(pd.DataFrame({
    'holiday': '雙12',
    'ds': pd.to_datetime(['2019-12-12', '2020-12-12']),
    'lower_window': -1,
    'upper_window': 1,
})
)

def predict(name, rule='1d', freq='d', periods=1, show=False):
    ds = get_product_data(name, rule=rule)
    if ds.shape[0] < 7:
        return None
    m = Prophet(holidays=holidays)
    m.fit(ds)
    future = m.make_future_dataframe(freq=freq, periods=periods)  # 建立數據預測框架,數據粒度為天,預測步長為一年
    forecast = m.predict(future)
    if show:
        m.plot(forecast).show()  # 繪製預測效果圖
        m.plot_components(forecast).show()  # 繪製成分趨勢圖
    mse = forecast['yhat'].iloc[ds.shape[0]] - ds['y'].values
    mse = np.abs(mse) / (ds['y'].values + 1)
    return [name, mse.mean(), mse.max(), mse.min(), np.quantile(mse, 0.9), np.quantile(mse, 0.8), mse[-7:].mean(),
            ds['y'].iloc[-7:].mean()]
if __name__ == '__main__':
    products = set(data['product'])
    p = []
    for i in products:
        y = predict(i)
        if y is not None:
            p.append(y)
    df = pd.DataFrame(p, columns=['product', 'total_mean', 'total_max', 'total_min', '0.9', '0.8', '7_mean',
       '7_real_value_mean'])
    df.set_index('product', inplace=True)
    product_sum: pd.DataFrame = data.groupby('product').sum()
    df = df.join(product_sum)
    df.sort_values('cnt', ascending=False, inplace=True)
    df.to_csv('result.csv', index=False)

結果如下:由於行數較多這裏只展示前1行

根據結果,對比原生數據,可以得出如下結論:
就算法與產品的匹配性可分為3個類型:

  • (1)與算法較為匹配,算法的歷史誤差8分為數<=0.2的
  • (2)與算法不太匹配的,算法的歷史誤差8分為數>0.2的
  • (3)數據過少的,無法正常預測的。目前僅top10就能佔到整體訂單數的90%以上。
7.1.2 部分成果展示

A. 因素分解圖

上圖中主要分為3個部分,分別對應prophet 3大要素,趨勢、節假日或特殊日期、周期性(包括年周期、月周期、week周期、天周期以及用戶自定義的周期)
下面依照上面因素分解圖的順序依次對圖進行說明:

  • (1)、Trend:
    即趨勢因素圖。描述時間序列的趨勢。Prophet支持線性趨勢和logist趨勢。通過growth參數設置,當然模型能自己根據時間序列的走勢判斷growth類型。這也是prophet實現的比較智能的一點。
  • (2)、Holidays
    即節假日及特殊日期因素圖。描述了節假日及用戶自定義的特殊日期對時間序列的影響。正值為正影響,負值為負影響。從圖中可以看出這個商品對節假日比較敏感。節假日是根據holidays參數設置的。
  • (3)、weekly
    星期周期性因素圖。正常情況下,如果是小時級別數據將會有天周期圖。有1年以上完整數據並且時間序列有典型的年周期性會有年周期圖。如果你覺得這個有年周期,但模型並不這麼認為,你可以通過設置yearly_seasonality設置一個具體的數值。這個數值默認情況下為10(weekly_seasonality默認為3),這個值代表的是傅里恭弘=叶 恭弘級數的項數,越大模型越容易過擬合,過小則會導致欠擬合,一般配合seasonality_prior_scale使用。
    B.預測曲線與實際值對比

7.2 lstm

LSTM(長短記憶網絡)主要用於有先後順序的序列類型的數據的深度學習網絡。是RNN的優化版本。一般用於自然語言處理,也可用於時間序列的預測。

簡單來說就是,LSTM一共有三個門,輸入門,遺忘門,輸出門, i 、o、 f 分別為三個門的程度參數, g 與RNN中的概念一致。公式里可以看到LSTM的輸出有兩個,細胞狀態c 和隱狀態 h,c是經輸入、遺忘門的產物,也就是當前cell本身的內容,經過輸出門得到h,就是想輸出什麼內容給下一單元。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
from torch import nn

from sklearn.preprocessing import MinMaxScaler

ts_data = pd.read_csv('../data/data2.csv', parse_dates=['time'], index_col='time')


def series_to_supervised(data, n_in=1, n_out=1, dropnan=True):
    n_vars = 1 if type(data) is list else data.shape[1]
    df = pd.DataFrame(data)
    cols, names = list(), list()
    # input sequence (t-n, ... t-1)
    for i in range(n_in, 0, -1):
        cols.append(df.shift(i))
        names += [('var%d(t-%d)' % (j + 1, i)) for j in range(n_vars)]
    # forecast sequence (t, t+1, ... t+n)
    for i in range(0, n_out):
        cols.append(df.shift(-i))
        if i == 0:
            names += [('var%d(t)' % (j + 1)) for j in range(n_vars)]
        else:
            names += [('var%d(t+%d)' % (j + 1, i)) for j in range(n_vars)]
    # put it all together
    agg = pd.concat(cols, axis=1)
    agg.columns = names
    # drop rows with NaN values
    if dropnan:
        agg.dropna(inplace=True)
    return agg


def transform_data(feature_cnt=2):
    yd = ts_data[ts_data['product'] == '移動話費'][['cnt']]
    scaler = MinMaxScaler(feature_range=(0, 1))
    yd_scaled = scaler.fit_transform(yd.values)
    yd_renamed = series_to_supervised(yd_scaled
, n_in=feature_cnt).values.astype('float32')

    n_row = yd_renamed.shape[0]

    n_train = int(n_row * 0.7)

    train_X, train_y = yd_renamed[:n_train, :-1], yd_renamed[:n_train, -1]
    test_X, test_y = yd_renamed[n_train:, :-1], yd_renamed[n_train:, -1]

    # 最後,我們需要將數據改變一下形狀,因為 RNN 讀入的數據維度是 (seq, batch, feature),所以要重新改變一下數據的維度,這裏只有一個序列,所以 batch 是 1,而輸入的 feature 就是我們希望依據的幾天,這裏我們定的是兩個天,所以 feature 就是 2.
    train_X = train_X.reshape((-1, 1, feature_cnt))
    test_X = test_X.reshape((-1, 1, feature_cnt))
    print(train_X.shape, train_y.shape, test_X.shape, test_y.shape)

    # 轉化成torch 的張量
    train_x = torch.from_numpy(train_X)
    train_y = torch.from_numpy(train_y)
    test_x = torch.from_numpy(test_X)
    test_y = torch.from_numpy(test_y)
    return scaler, train_x, train_y, test_x, test_y


scaler, train_x, train_y, test_x, test_y = transform_data(24)


# lstm 網絡
class lstm_reg(nn.Module):  # 括號中的是python的類繼承語法,父類是nn.Module類 不是參數的意思
    def __init__(self, input_size, hidden_size, output_size=1, num_layers=2):  # 構造函數
        # inpu_size 是輸入的樣本的特徵維度, hidden_size 是LSTM層的神經元個數,
        # output_size是輸出的特徵維度
        super(lstm_reg, self).__init__()  # super用於多層繼承使用,必須要有的操作

        self.rnn = nn.LSTM(input_size, hidden_size, num_layers)  # 兩層LSTM網絡,
        self.reg = nn.Linear(hidden_size, output_size)  # 把上一層總共hidden_size個的神經元的輸出向量作為輸入向量,然後回歸到output_size維度的輸出向量中

    
def forward(self, x):  # x是輸入的數據
        x, _ = self.rnn(x)  # 單個下劃線表示不在意的變量,這裡是LSTM網絡輸出的兩個隱藏層狀態
        s, b, h = x.shape
        x = x.view(s * b, h)
        x = self.reg(x)
        x = x.view(s, b, -1)  # 使用-1表示第三個維度自動根據原來的shape 和已經定了的s,b來確定
        return x


def train(feature_cnt, hidden_size, round, save_path='model.pkl'):
    # 我使用了GPU加速,如果不用的話需要把.cuda()給註釋掉
    net = lstm_reg(feature_cnt, hidden_size)
    criterion = nn.MSELoss()
    optimizer = torch.optim.Adam(net.parameters(), lr=1e-2)
    for e in range(round):
        # 新版本中可以不使用Variable了
        #     var_x = Variable(train_x).cuda()
        #     var_y = Variable(train_y).cuda()

        # 將tensor放在GPU上面進行運算
        var_x = train_x
        var_y = train_y

        out = net(var_x)
        loss = criterion(out, var_y)

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (e + 1) % 100 == 0:
            print('Epoch: {}, Loss:{:.5f}'.format(e + 1, loss.item()))
    # 存儲訓練好的模型參數
    torch.save(net.state_dict(), save_path)
    return net


if __name__ == '__main__':
    net = train(24, 8, 5000)
    # criterion = nn.MSELoss()
    # optimizer = torch.optim.Adam(net.parameters(), lr=1e-2)
    pred_test = net(test_x)  # 測試集的預測結果

    pred_test = pred_test.view(-1).data.numpy()  # 先轉移到cpu上才能轉換為numpy

    # 乘以原來歸一化的刻度放縮回到原來的值域
    origin_test_Y = scaler.inverse_transform(test_y.reshape((-1,1)))
    origin_pred_test = scaler.inverse_transform(pred_test.reshape((-1,1)))

    # 畫圖
    plt.plot(origin_pred_test, 'r', label='prediction')
    plt.plot(origin_test_Y, 'b', label='real')
    plt.legend(loc='best')
    plt.show()

    # 計算MSE
    # loss = criterion(out, var_y)?
    true_data = origin_test_Y
    true_data = np.array(true_data)
    true_data = np.squeeze(true_data)  # 從二維變成一維
    
MSE = true_data - origin_pred_test
    MSE = MSE * MSE
    MSE_loss = sum(MSE) / len(MSE)
    print(MSE_loss)

八、兩種算法的比較

  • (1)在訓練時間上,prophet幾十秒就能出結果,而lstm往往需要1個半小時,更是隨着網絡層數和特徵數量的增加而增加。
  • (2)Prophet是一個為商業預測而生的時間序列預測模型,因此在很多方便都有針對性的優化,而lstm的初衷是nlp。
  • (3)Prophet無需特徵處理即可使用,參數調優也明確簡單。而lstm則需要先進行必要的特徵處理,其次要進行正確的網絡結構設計,因此lstm相對prophet更為複雜。
  • (4)Lstm需要更多的數據進行學習,否則無法消除欠擬合的情形。而prophet不同,prophet基於統計學,有完整的數學理論支撐,因此更容易從少量的數據中完成學習。
    參考文獻:
    【1】Prophet官方文檔:https://facebook.github.io/prophet/
    【2】Prophet論文:https://peerj.com/preprints/3190/
    【3】Prophet-github:https://github.com/facebook/prophet
    【4】LSTM http://colah.github.io/posts/2015-08-Understanding-LSTMs/
    【5】基於LSTM的關聯時間序列預測方法研究 尹康 《北京交通大學》 2019年 cnki地址:http://cdmd.cnki.com.cn/Article/CDMD-10004-1019209125.htm

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

新加坡銷毀重達9公噸走私象牙 打擊非法交易

摘錄自2020年8月11日中央社報導

新加坡今天(11日)銷毀重達9公噸的非法走私象牙,並透過網路直播,預計數天才能完成銷毀。當局表示,這是全球近年最大的銷毀非法象牙行動,展現星國打擊非法野生動物交易的決心。

根據動保人士估計,每天約有100隻非洲象被意圖盜取象牙等大象身體部位的盜獵者所殺,目前僅存約40萬隻非洲象。

新加坡是非洲與亞洲之間運送非法動物商品的海上航路點。除了對非法運輸的商品採取強硬立場,新加坡去年也宣示,自2021年9月起,將全面禁止國內象牙及其製品銷售。

生物多樣性
國際新聞
新加坡
象牙
野生動物
非洲象
非法盜獵
象牙走私

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

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