如何使用偽類選擇器

偽類選擇器介紹

  • 偽類選擇器就是用來給超級鏈接設置不同的狀態樣式。
  • 超級鏈接分為4種狀態如:正常狀態、訪問過後狀態、鼠標放上狀態、激活狀態。

偽類選擇器說明表

選擇器 描述
:link 向未被訪問的超級鏈接添加樣式,正常狀態。
:visited 向已經被訪問的超級鏈接添加樣式,訪問過後狀態。
:hover 當鼠標懸浮在超級鏈接上方時,向超級鏈接添加樣式,鼠標放上狀態。
:active 鼠標放在超級鏈接上並且點擊的一瞬間,向超級鏈接添加樣式,激活狀態。

偽類選擇器實踐

  • 讓我們進入偽類選擇器實踐,實踐內容將超級鏈接4種狀態進行演示,演示效果如:將向未被訪問的超級鏈接文本顏色設置為紅色、已經被訪問的超級鏈接文本顏色設置為綠色、當鼠標懸浮在超級鏈接上文本顏色設置為紫色、用鼠標點擊超級鏈接的一瞬間文本顏色設置為藍色

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>偽類選擇器</title>
    <style>
        a:link{
            color:red;
        }
        a:visited{
            color: lime;
        }
        a:hover{
            color: purple;
        }
        a:active{
            color: blue;
        }
    </style>
</head>
  
<body>
    <a href="https://www.cnblogs.com/lq000122/">微笑是最初的信仰</a>
</body>
</html>
  • 正常狀態結果圖

  • 鼠標放上狀態結果圖

  • 激活狀態結果圖

  • 訪問過後狀態

總結

  • 超級鏈接的不同狀態他其實是由順序,也就是說偽類選擇器設置其實是順序的,如果按照偽類選擇器的順序,那麼設置的樣式就不會被渲染。
  • 順序:linkvisitedhoveractive

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

高併發編程學習(1)——併發基礎

為更良好的閱讀體驗,請訪問原文:

一、前言

當我們使用計算機時,可以同時做許多事情,例如一邊打遊戲一邊聽音樂。這是因為操作系統支持併發任務,從而使得這些工作得以同時進行。

  • 那麼提出一個問題:如果我們要實現一個程序能一邊聽音樂一邊玩遊戲怎麼實現呢?
public class Tester {

    public static void main(String[] args) {
        System.out.println("開始....");
        playGame();
        playMusic();
        System.out.println("結束....");
    }

    private static void playGame() {
        for (int i = 0; i < 50; i++) {
            System.out.println("玩遊戲" + i);
        }
    }

    private static void playMusic() {
        for (int i = 0; i < 50; i++) {
            System.out.println("播放音樂" + i);
        }
    }
}

我們使用了循環來模擬過程,因為播放音樂和打遊戲都是連續的,但是結果卻不盡人意,因為函數體總是要執行完之後才能返回。那麼到底怎麼解決這個問題?

并行與併發

并行性和併發性是既相似又有區別的兩個概念。

并行性是指兩個或多個事件在同一時刻發生。而併發性是指兩個或多個事件在同一時間間隔內發生。

在多道程序環境下,併發性是指在一段時間內宏觀上有多個程序在同時運行,但在單處理機環境下(一個處理器),每一時刻卻僅能有一道程序執行,故微觀上這些程序只能是分時地交替執行。例如,在 1 秒鐘時間內,0 – 15 ms 程序 A 運行;15 – 30 ms 程序 B 運行;30 – 45 ms 程序 C 運行;45 – 60 ms 程序 D 運行,因此可以說,在 1 秒鐘時間間隔內,宏觀上有四道程序在同時運行,但微觀上,程序 A、B、C、D 是分時地交替執行的。

如果在計算機系統中有多個處理機,這些可以併發執行的程序就可以被分配到多個處理機上,實現併發執行,即利用每個處理機愛處理一個可併發執行的程序。這樣,多個程序便可以同時執行。以此就能提高系統中的資源利用率,增加系統的吞吐量。

進程和線程

進程是指一個內存中運行的應用程序。一個應用程序可以同時啟動多個進程,那麼上面的問題就有了解決的思路:我們啟動兩個進程,一個用來打遊戲,一個用來播放音樂。這當然是一種解決方案,但是想象一下,如果一個應用程序需要執行的任務非常多,例如 LOL 遊戲吧,光是需要播放的音樂就有非常多,人物本身的語音,技能的音效,遊戲的背景音樂,塔攻擊的聲音等等等,還不用說遊戲本身,就光播放音樂就需要創建許多許多的進程,而進程本身是一種非常消耗資源的東西,這樣的設計顯然是不合理的。更何況大多數的操作系統都不需要一個進程訪問其他進程的內存空間,也就是說,進程之間的通信很不方便,此時我們就得引入“線程”這門技術,來解決這個問題。

線程是指進程中的一個執行任務(控制單元),一個進程可以同時併發運行多個線程。我們可以打開任務管理器,觀察到幾乎所有的進程都擁有着許多的「線程」(在 WINDOWS 中線程是默認隱藏的,需要在「查看」裏面點擊「選擇列」,有一個線程數的勾選項,找到並勾選就可以了)。

進程和線程的區別

進程:有獨立的內存空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。

線程:堆空間是共享的,棧空間是獨立的,線程消耗的資源也比進程小,相互之間可以影響的,又稱為輕型進程或進程元。

因為一個進程中的多個線程是併發運行的,那麼從微觀角度上考慮也是有先後順序的,那麼哪個線程執行完全取決於 CPU 調度器(JVM 來調度),程序員是控制不了的。我們可以把多線程併發性看作是多個線程在瞬間搶 CPU 資源,誰搶到資源誰就運行,這也造就了多線程的隨機性。下面我們將看到更生動的例子。

Java 程序的進程(Java 的一個程序運行在系統中)里至少包含主線程和垃圾回收線程(後台線程),你可以簡單的這樣認為,但實際上有四個線程(了解就好):

  • [1] main——main 線程,用戶程序入口
  • [2] Reference Handler——清除 Reference 的線程
  • [3] Finalizer——調用對象 finalize 方法的線程
  • [4] Signal Dispatcher——分發處理髮送給 JVM 信號的線程

多線程和單線程的區別和聯繫?

  1. 單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執行,是一種微觀上輪流佔用 CPU 的機制。

  2. 多線程會存在線程上下文切換,會導致程序執行速度變慢,即採用一個擁有兩個線程的進程執行所需要的時間比一個線程的進程執行兩次所需要的時間要多一些。

結論:即採用多線程不會提高程序的執行速度,反而會降低速度,但是對於用戶來說,可以減少用戶的響應時間。

多線程的優勢

儘管面臨很多挑戰,多線程有一些優點仍然使得它一直被使用,而這些優點我們應該了解。

優勢一:資源利用率更好

想象一下,一個應用程序需要從本地文件系統中讀取和處理文件的情景。比方說,從磁盤讀取一個文件需要 5 秒,處理一個文件需要 2 秒。處理兩個文件則需要:

1| 5秒讀取文件A
2| 2秒處理文件A
3| 5秒讀取文件B
4| 2秒處理文件B
5| ---------------------
6| 總共需要14秒

從磁盤中讀取文件的時候,大部分的 CPU 時間用於等待磁盤去讀取數據。在這段時間里,CPU 非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用 CPU 資源。看下面的順序:

1| 5秒讀取文件A
2| 5秒讀取文件B + 2秒處理文件A
3| 2秒處理文件B
4| ---------------------
5| 總共需要12秒

CPU 等待第一個文件被讀取完。然後開始讀取第二個文件。當第二文件在被讀取的時候,CPU 會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU 大部分時間是空閑的。

總的說來,CPU 能夠在等待 IO 的時候做一些其他的事情。這個不一定就是磁盤 IO。它也可以是網絡的 IO,或者用戶輸入。通常情況下,網絡和磁盤的 IO 比 CPU 和內存的 IO 慢的多。

優勢二:程序設計在某些情況下更簡單

在單線程應用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態。相反,你可以啟動兩個線程,每個線程處理一個文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用 CPU 去處理已經讀取完的文件。其結果就是,磁盤總是在繁忙地讀取不同的文件到內存中。這會帶來磁盤和 CPU 利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現。

優勢三:程序響應更快

有時我們會編寫一些較為複雜的代碼(這裏的複雜不是說複雜的算法,而是複雜的業務邏輯),例如,一筆訂單的創建,它包括插入訂單數據、生成訂單趕快找、發送郵件通知賣家和記錄貨品銷售數量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結果。但是這麼多業務操作,如何能夠讓其更快地完成呢?

在上面的場景中,可以使用多線程技術,即將數據一致性不強的操作派發給其他線程處理(也可以使用消息隊列),如生成訂單快照、發送郵件等。這樣做的好處是響應用戶請求的線程能夠盡可能快地處理完成,縮短了響應時間,提升了用戶體驗。

其他優勢

多線程還有一些優勢也顯而易見:

  • 進程之前不能共享內存,而線程之間共享內存(堆內存)則很簡單。
  • 系統創建進程時需要為該進程重新分配系統資源,創建線程則代價小很多,因此實現多任務併發時,多線程效率更高.
  • Java 語言本身內置多線程功能的支持,而不是單純地作為底層系統的調度方式,從而簡化了多線程編程.

上下文切換

即使是單核處理器也支持多線程執行代碼,CPU 通過給每個線程分配 CPU 時間片來實現這個機制。時間片是 CPU 分配給各個線程的時間,因為時間片非常短,所以 CPU 通過不停地切換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。

CPU 通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務的時候,可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。

這就像我們同時讀兩本書,當我們在讀一本英文的技術書時,發現某個單詞不認識,於是打開中英文字典,但是在放下英文技術書之前,大腦必須先記住這本書獨到了多少頁的多少行,等查完單詞之後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。

二、創建線程的兩種方式

繼承 Thread 類

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread extends Thread {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式1:繼承 Thread 類
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                PlayMusicThread musicThread = new PlayMusicThread();
                musicThread.start();
            }
        }
    }
}

運行結果發現打遊戲和播放音樂交替出現,說明已經成功了。

實現 Runnable 接口

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread implements Runnable {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式2:實現 Runnable 方法
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                Thread musicThread = new Thread(new PlayMusicThread());
                musicThread.start();
            }
        }
    }
}

也能完成效果。

以上就是傳統的兩種創建線程的方式,事實上還有第三種,我們後邊再講。

多線程一定快嗎?

先來一段代碼,通過并行和串行來分別執行累加操作,分析:下面的代碼併發執行一定比串行執行快嗎?

import org.springframework.util.StopWatch;

// 比較并行和串行執行累加操作的速度
public class Tester {

    // 執行次數
    private static final long COUNT = 100000000;
    private static final StopWatch TIMER = new StopWatch();

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
        // 打印比較測試結果
        System.out.println(TIMER.prettyPrint());
    }

    private static void serial() {
        TIMER.start("串行執行" + COUNT + "條數據");

        int a = 0;
        for (long i = 0; i < COUNT; i++) {
            a += 5;
        }
        // 串行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }

        TIMER.stop();
    }

    private static void concurrency() throws InterruptedException {
        TIMER.start("并行執行" + COUNT + "條數據");

        // 通過匿名內部類來創建線程
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < COUNT; i++) {
                a += 5;
            }
        });
        thread.start();

        // 并行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }
        // 等待線程結束
        thread.join();
        TIMER.stop();
    }
}

大家可以自己測試一下,每一台機器 CPU 不同測試結果可能也會不同,之前在 WINDOWS 本兒上測試的時候,多線程的優勢從 1 千萬數據的時候才開始體現出來,但是現在換了 MAC,1 億條數據時間也差不多,到 10 億的時候明顯串行就比并行快了… 總之,為什麼併發執行的速度會比串行慢呢?就是因為線程有創建和上下文切換的開銷。

繼承 Thread 類還是實現 Runnable 接口?

想象一個這樣的例子:給出一共 50 個蘋果,讓三個同學一起來吃,並且給蘋果編上號碼,讓他們吃的時候順便要說出蘋果的編號:

運行結果可以看到,使用繼承方式實現,每一個線程都吃了 50 個蘋果。這樣的結果顯而易見:是因為顯式地創建了三個不同的 Person 對象,而每個對象在堆空間中有獨立的區域來保存定義好的 50 個蘋果。

而使用實現方式則滿足要求,這是因為三個線程共享了同一個 Apple 對象,而對象中的 num 數量是一定的。

所以可以簡單總結出繼承方式和實現方式的區別:

繼承方式:

  1. Java 中類是單繼承的,如果繼承了 Thread 了,該類就不能再有其他的直接父類了;
  2. 從操作上分析,繼承方式更簡單,獲取線程名字也簡單..(操作上,更簡單)
  3. 從多線程共享同一個資源上分析,繼承方式不能做到…

實現方式:

  1. Java 中類可以實現多個接口,此時該類還可以繼承其他類,並且還可以實現其他接口(設計上,更優雅)..
  2. 從操作上分析,實現方式稍微複雜點,獲取線程名字也比較複雜,需要使用 Thread.currentThread() 來獲取當前線程的引用..
  3. 從多線程共享同一個資源上分析,實現方式可以做到..

在這裏,三個同學完成搶蘋果的例子,使用實現方式才是更合理的方式。

對於這兩種方式哪種好並沒有一個確定的答案,它們都能滿足要求。就我個人意見,我更傾向於實現 Runnable 接口這種方法。因為線程池可以有效的管理實現了 Runnable 接口的線程,如果線程池滿了,新的線程就會排隊等候執行,直到線程池空閑出來為止。而如果線程是通過實現 Thread 子類實現的,這將會複雜一些。

有時我們要同時融合實現 Runnable 接口和 Thread 子類兩種方式。例如,實現了 Thread 子類的實例可以執行多個實現了 Runnable 接口的線程。一個典型的應用就是線程池。

常見錯誤:調用 run() 方法而非 start() 方法

創建並運行一個線程所犯的常見錯誤是調用線程的 run() 方法而非 start() 方法,如下所示:

1| Thread newThread = new Thread(MyRunnable());
2| newThread.run();  //should be start();

起初你並不會感覺到有什麼不妥,因為 run() 方法的確如你所願的被調用了。但是,事實上,run() 方法並非是由剛創建的新線程所執行的,而是被創建新線程的當前線程所執行了。也就是被執行上面兩行代碼的線程所執行的。想要讓創建的新線程執行 run() 方法,必須調用新線程的 start() 方法。

三、線程的安全問題

吃蘋果遊戲的不安全問題

我們來考慮一下上面吃蘋果的例子,會有什麼問題?

儘管,Java 並不保證線程的順序執行,具有隨機性,但吃蘋果比賽的案例運行多次也並沒有發現什麼太大的問題。這並不是因為程序沒有問題,而只是問題出現的不夠明顯,為了讓問題更加明顯,我們使用 Thread.sleep() 方法(經常用來模擬網絡延遲)來讓線程休息 10 ms,讓其他線程去搶資源。(注意:在程序中並不是使用 Thread.sleep(10)之後,程序才出現問題,而是使用之後,問題更明顯.)

為什麼會出現這樣的錯誤呢?

先來分析第一種錯誤:為什麼會吃重複的蘋果呢?就拿 B 和 C 都吃了編號為 47 的蘋果為例吧:

  • A 線程拿到了編號為 48 的蘋果,打印輸出然後讓 num 減 1,睡眠 10 ms,此時 num 為 47。
  • 這時 B 和 C 同時都拿到了編號為 47 的蘋果,打印輸出,在其中一個線程作出了減一操作的時候,A 線程從睡眠中醒過來,拿到了編號為 46 的蘋果,然後輸出。在這期間並沒有任何操作不允許 B 和 C 線程不能拿到同一個編號的蘋果,之前沒有明顯的錯誤僅僅可能只是因為運行速度太快了。

再來分析第二種錯誤:照理來說只應該存在 1-50 編號的蘋果,可是 0 和-1 是怎麼出現的呢?

  • 當 num = 1 的時候,A,B,C 三個線程同時進入了 try 語句進行睡眠。
  • C 線程先醒過來,輸出了編號為 1 的蘋果,然後讓 num 減一,當 C 線程醒過來的時候發現 num 為 0 了。
  • A 線程醒過來一看,0 都沒有了,只有 -1 了。

歸根結底是因為沒有任何操作來限制線程來獲取相同的資源並對他們進行操作,這就造成了線程安全性問題。

如果我們把打印和減一的操作分成兩個步驟,會更加明顯:

ABC 三個線程同時打印了 50 的蘋果,然後同時做出減一操作。

像這樣的原子操作,是不允許分步驟進行的,必須保證同步進行,不然可能會引發不可設想的後果。

要解決上述多線程併發訪問一個資源的安全性問題,就需要引入線程同步的概念。

線程同步

多個執行線程共享一個資源的情景,是最常見的併發編程情景之一。為了解決訪問共享資源錯誤或數據不一致的問題,人們引入了臨界區的概念:用以訪問共享資源的代碼塊,這個代碼塊在同一時間內只允許一個線程執行。

為了幫助編程人員實現這個臨界區,Java(以及大多數編程語言)提供了同步機制,當一個線程試圖訪問一個臨界區時,它將使用一種同步機制來查看是不是已經有其他線程進入臨界區。如果沒有其他線程進入臨界區,他就可以進入臨界區。如果已經有線程進入了臨界區,它就被同步機制掛起,直到進入的線程離開這個臨界區。如果在等待進入臨界區的線程不止一個,JVM 會選擇其中的一個,其餘的將繼續等待。

synchronized 關鍵字

如果一個對象已用 synchronized 關鍵字聲明,那麼只有一個執行線程被允許訪問它。使用 synchronized 的好處顯而易見:保證了多線程併發訪問時的同步操作,避免線程的安全性問題。但是壞處是:使用 synchronized 的方法/代碼塊的性能比不用要低一些。所以好的做法是:盡量減小 synchronized 的作用域。

我們還是先來解決吃蘋果的問題,考慮一下 synchronized 關鍵字應該加在哪裡呢?

發現如果還再把 synchronized 關鍵字加在 if 裏面的話,0 和 -1 又會出來了。這其實是因為當 ABC 同是進入到 if 語句中,等待臨界區釋放的時,拿到 1 編號的線程已經又把 num 減一操作了,而此時最後一個等待臨界區的進程拿到的就會是 -1 了。

同步鎖 Lock

Lock 機制提供了比 synchronized 代碼塊和 synchronized 方法更廣泛的鎖定操作,同步代碼塊/ 同步方法具有的功能 Lock 都有,除此之外更強大,更體現面向對象。在併發包的類族中,Lock 是 JUC 包的頂層接口,它的實現邏輯並未用到 synchronized,而是利用了 volatile 的可見性。

使用 Lock 最典型的代碼如下:

class X {

    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();
        try {
            // ..... method body
        } finally {
            lock.unlock();
        }
    }
}

線程安全問題

線程安全問題只在多線程環境下才會出現,單線程串行執行不存在此類問題。保證高併發場景下的線程安全,可以從以下四個維度考量:

維度一:數據單線程可見

單線程總是安全的。通過限制數據僅在單線程內可見,可以避免數據被其他線程篡改。最典型的就是線程局部變量,它存儲在獨立虛擬機棧幀的局部變量表中,與其他線程毫無瓜葛。TreadLocal 就是採用這種方式來實現線程安全的。

維度二:只讀對象

只讀對象總是安全的。它的特性是允許複製、拒絕寫入。最典型的只讀對象有 String、Integer 等。一個對象想要拒絕任何寫入,必須要滿足以下條件:

  • 使用 final 關鍵字修飾類,避免被繼承;
  • 使用 private final 關鍵字避免屬性被中途修改;
  • 沒有任何更新方法;
  • 返回值不能為可變對象。

維度三:線程安全類

某些線程安全類的內部有非常明確的線程安全機制。比如 StringBuffer 就是一個線程安全類,它採用 synchronized 關鍵字來修飾相關方法。

維度四:同步與鎖機制

如果想要對某個對象進行併發更新操作,但又不屬於上述三類,需要開發工程師在代碼中實現安全的同步機制。雖然這個機制支持的併發場景很有價值,但非常複雜且容易出現問題。

處理線程安全的核心理念

要麼只讀,要麼加鎖。

合理利用好 JDK 提供的併發包,往往能化腐朽為神奇。Java 併發包(java.util.concurrent,JUC)中大多數類註釋都寫有:@author Doug Lea。如果說 Java 是一本史書,那麼 Doug Lea 絕對是開疆拓土的偉大人物。Doug Lea 在當大學老師時,專攻併發編程和併發數據結構設計,主導設計了 JUC 併發包,提高了 Java 併發編程的易用性,大大推進了 Java 的商用進程。

參考資料

  • 《Java 零基礎入門教程》 –
  • 《Java 併發編程的藝術》
  • 《Java 7 併發編程實戰手冊》
  • 《碼出高效 Java 開發手冊》 – 楊冠寶(孤盡) 高海慧(鳴莎)著

按照慣例黏一個尾巴:

歡迎轉載,轉載請註明出處!
獨立域名博客:wmyskxz.com
簡書 ID:
github:
歡迎關注公眾微信號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加 qq 群:3382693

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

帶你漲姿勢的認識一下 Kafka 消費者

之前我們介紹過了 Kafka 整體架構,Kafka 生產者,Kafka 生產的消息最終流向哪裡呢?當然是需要消費了,要不只產生一系列數據沒有任何作用啊,如果把 Kafka 比作餐廳的話,那麼生產者就是廚師的角色,消費者就是客人,只有廚師的話,那麼炒出來的菜沒有人吃也沒有意義,如果只有客人沒有廚師的話,誰會去這個店吃飯呢?!所以如果你看完前面的文章意猶未盡的話,可以繼續讓你爽一爽。如果你沒看過前面的文章,那就從現在開始讓你爽。

Kafka 消費者概念

應用程序使用 KafkaConsumer 從 Kafka 中訂閱主題並接收來自這些主題的消息,然後再把他們保存起來。應用程序首先需要創建一個 KafkaConsumer 對象,訂閱主題並開始接受消息,驗證消息並保存結果。一段時間后,生產者往主題寫入的速度超過了應用程序驗證數據的速度,這時候該如何處理?如果只使用單個消費者的話,應用程序會跟不上消息生成的速度,就像多個生產者像相同的主題寫入消息一樣,這時候就需要多個消費者共同參与消費主題中的消息,對消息進行分流處理。

Kafka 消費者從屬於消費者群組。一個群組中的消費者訂閱的都是相同的主題,每個消費者接收主題一部分分區的消息。下面是一個 Kafka 分區消費示意圖

上圖中的主題 T1 有四個分區,分別是分區0、分區1、分區2、分區3,我們創建一個消費者群組1,消費者群組中只有一個消費者,它訂閱主題T1,接收到 T1 中的全部消息。由於一個消費者處理四個生產者發送到分區的消息,壓力有些大,需要幫手來幫忙分擔任務,於是就演變為下圖

這樣一來,消費者的消費能力就大大提高了,但是在某些環境下比如用戶產生消息特別多的時候,生產者產生的消息仍舊讓消費者吃不消,那就繼續增加消費者。

如上圖所示,每個分區所產生的消息能夠被每個消費者群組中的消費者消費,如果向消費者群組中增加更多的消費者,那麼多餘的消費者將會閑置,如下圖所示

向群組中增加消費者是橫向伸縮消費能力的主要方式。總而言之,我們可以通過增加消費組的消費者來進行水平擴展提升消費能力。這也是為什麼建議創建主題時使用比較多的分區數,這樣可以在消費負載高的情況下增加消費者來提升性能。另外,消費者的數量不應該比分區數多,因為多出來的消費者是空閑的,沒有任何幫助。

Kafka 一個很重要的特性就是,只需寫入一次消息,可以支持任意多的應用讀取這個消息。換句話說,每個應用都可以讀到全量的消息。為了使得每個應用都能讀到全量消息,應用需要有不同的消費組。對於上面的例子,假如我們新增了一個新的消費組 G2,而這個消費組有兩個消費者,那麼就演變為下圖這樣

在這個場景中,消費組 G1 和消費組 G2 都能收到 T1 主題的全量消息,在邏輯意義上來說它們屬於不同的應用。

總結起來就是如果應用需要讀取全量消息,那麼請為該應用設置一個消費組;如果該應用消費能力不足,那麼可以考慮在這個消費組裡增加消費者

消費者組和分區重平衡

消費者組是什麼

消費者組(Consumer Group)是由一個或多個消費者實例(Consumer Instance)組成的群組,具有可擴展性和可容錯性的一種機制。消費者組內的消費者共享一個消費者組ID,這個ID 也叫做 Group ID,組內的消費者共同對一個主題進行訂閱和消費,同一個組中的消費者只能消費一個分區的消息,多餘的消費者會閑置,派不上用場。

我們在上面提到了兩種消費方式

  • 一個消費者群組消費一個主題中的消息,這種消費模式又稱為點對點的消費方式,點對點的消費方式又被稱為消息隊列
  • 一個主題中的消息被多個消費者群組共同消費,這種消費模式又稱為發布-訂閱模式

消費者重平衡

我們從上面的消費者演變圖中可以知道這麼一個過程:最初是一個消費者訂閱一個主題並消費其全部分區的消息,後來有一個消費者加入群組,隨後又有更多的消費者加入群組,而新加入的消費者實例分攤了最初消費者的部分消息,這種把分區的所有權通過一個消費者轉到其他消費者的行為稱為重平衡,英文名也叫做 Rebalance 。如下圖所示

重平衡非常重要,它為消費者群組帶來了高可用性伸縮性,我們可以放心的添加消費者或移除消費者,不過在正常情況下我們並不希望發生這樣的行為。在重平衡期間,消費者無法讀取消息,造成整個消費者組在重平衡的期間都不可用。另外,當分區被重新分配給另一個消費者時,消息當前的讀取狀態會丟失,它有可能還需要去刷新緩存,在它重新恢復狀態之前會拖慢應用程序。

消費者通過向組織協調者(Kafka Broker)發送心跳來維護自己是消費者組的一員並確認其擁有的分區。對於不同不的消費群體來說,其組織協調者可以是不同的。只要消費者定期發送心跳,就會認為消費者是存活的並處理其分區中的消息。當消費者檢索記錄或者提交它所消費的記錄時就會發送心跳。

如果過了一段時間 Kafka 停止發送心跳了,會話(Session)就會過期,組織協調者就會認為這個 Consumer 已經死亡,就會觸發一次重平衡。如果消費者宕機並且停止發送消息,組織協調者會等待幾秒鐘,確認它死亡了才會觸發重平衡。在這段時間里,死亡的消費者將不處理任何消息。在清理消費者時,消費者將通知協調者它要離開群組,組織協調者會觸發一次重平衡,盡量降低處理停頓。

重平衡是一把雙刃劍,它為消費者群組帶來高可用性和伸縮性的同時,還有有一些明顯的缺點(bug),而這些 bug 到現在社區還無法修改。

重平衡的過程對消費者組有極大的影響。因為每次重平衡過程中都會導致萬物靜止,參考 JVM 中的垃圾回收機制,也就是 Stop The World ,STW,(引用自《深入理解 Java 虛擬機》中 p76 關於 Serial 收集器的描述):

更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程。直到它收集結束。Stop The World 這個名字聽起來很帥,但這項工作實際上是由虛擬機在後台自動發起並完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。

也就是說,在重平衡期間,消費者組中的消費者實例都會停止消費,等待重平衡的完成。而且重平衡這個過程很慢……

創建消費者

上面的理論說的有點多,下面就通過代碼來講解一下消費者是如何消費的

在讀取消息之前,需要先創建一個 KafkaConsumer 對象。創建 KafkaConsumer 對象與創建 KafkaProducer 對象十分相似 — 把需要傳遞給消費者的屬性放在 properties 對象中,後面我們會着重討論 Kafka 的一些配置,這裏我們先簡單的創建一下,使用3個屬性就足矣,分別是 bootstrap.serverkey.deserializervalue.deserializer

這三個屬性我們已經用過很多次了,如果你還不是很清楚的話,可以參考

還有一個屬性是 group.id 這個屬性不是必須的,它指定了 KafkaConsumer 是屬於哪個消費者群組。創建不屬於任何一個群組的消費者也是可以的

Properties properties = new Properties();
        properties.put("bootstrap.server","192.168.1.9:9092");     properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");   properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(properties);

主題訂閱

創建好消費者之後,下一步就開始訂閱主題了。subscribe() 方法接受一個主題列表作為參數,使用起來比較簡單

consumer.subscribe(Collections.singletonList("customerTopic"));

為了簡單我們只訂閱了一個主題 customerTopic,參數傳入的是一個正則表達式,正則表達式可以匹配多個主題,如果有人創建了新的主題,並且主題的名字與正則表達式相匹配,那麼會立即觸發一次重平衡,消費者就可以讀取新的主題。

要訂閱所有與 test 相關的主題,可以這樣做

consumer.subscribe("test.*");

輪詢

我們知道,Kafka 是支持訂閱/發布模式的,生產者發送數據給 Kafka Broker,那麼消費者是如何知道生產者發送了數據呢?其實生產者產生的數據消費者是不知道的,KafkaConsumer 採用輪詢的方式定期去 Kafka Broker 中進行數據的檢索,如果有數據就用來消費,如果沒有就再繼續輪詢等待,下面是輪詢等待的具體實現

try {
  while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(100));
    for (ConsumerRecord<String, String> record : records) {
      int updateCount = 1;
      if (map.containsKey(record.value())) {
        updateCount = (int) map.get(record.value() + 1);
      }
      map.put(record.value(), updateCount);
    }
  }
}finally {
  consumer.close();
}
  • 這是一個無限循環。消費者實際上是一個長期運行的應用程序,它通過輪詢的方式向 Kafka 請求數據。
  • 第三行代碼非常重要,Kafka 必須定期循環請求數據,否則就會認為該 Consumer 已經掛了,會觸發重平衡,它的分區會移交給群組中的其它消費者。傳給 poll() 方法的是一個超市時間,用 java.time.Duration 類來表示,如果該參數被設置為 0 ,poll() 方法會立刻返回,否則就會在指定的毫秒數內一直等待 broker 返回數據。
  • poll() 方法會返回一個記錄列表。每條記錄都包含了記錄所屬主題的信息,記錄所在分區的信息、記錄在分區中的偏移量,以及記錄的鍵值對。我們一般會遍歷這個列表,逐條處理每條記錄。
  • 在退出應用程序之前使用 close() 方法關閉消費者。網絡連接和 socket 也會隨之關閉,並立即觸發一次重平衡,而不是等待群組協調器發現它不再發送心跳並認定它已經死亡。

線程安全性

在同一個群組中,我們無法讓一個線程運行多個消費者,也無法讓多個線程安全的共享一個消費者。按照規則,一個消費者使用一個線程,如果一個消費者群組中多個消費者都想要運行的話,那麼必須讓每個消費者在自己的線程中運行,可以使用 Java 中的 ExecutorService 啟動多個消費者進行進行處理。

消費者配置

到目前為止,我們學習了如何使用消費者 API,不過只介紹了幾個最基本的屬性,Kafka 文檔列出了所有與消費者相關的配置說明。大部分參數都有合理的默認值,一般不需要修改它們,下面我們就來介紹一下這些參數。

  • fetch.min.bytes

該屬性指定了消費者從服務器獲取記錄的最小字節數。broker 在收到消費者的數據請求時,如果可用的數據量小於 fetch.min.bytes 指定的大小,那麼它會等到有足夠的可用數據時才把它返回給消費者。這樣可以降低消費者和 broker 的工作負載,因為它們在主題使用頻率不是很高的時候就不用來回處理消息。如果沒有很多可用數據,但消費者的 CPU 使用率很高,那麼就需要把該屬性的值設得比默認值大。如果消費者的數量比較多,把該屬性的值調大可以降低 broker 的工作負載。

  • fetch.max.wait.ms

我們通過上面的 fetch.min.bytes 告訴 Kafka,等到有足夠的數據時才會把它返回給消費者。而 fetch.max.wait.ms 則用於指定 broker 的等待時間,默認是 500 毫秒。如果沒有足夠的數據流入 kafka 的話,消費者獲取的最小數據量要求就得不到滿足,最終導致 500 毫秒的延遲。如果要降低潛在的延遲,就可以把參數值設置的小一些。如果 fetch.max.wait.ms 被設置為 100 毫秒的延遲,而 fetch.min.bytes 的值設置為 1MB,那麼 Kafka 在收到消費者請求后,要麼返回 1MB 的數據,要麼在 100 ms 后返回所有可用的數據。就看哪個條件首先被滿足。

  • max.partition.fetch.bytes

該屬性指定了服務器從每個分區里返回給消費者的最大字節數。它的默認值時 1MB,也就是說,KafkaConsumer.poll() 方法從每個分區里返回的記錄最多不超過 max.partition.fetch.bytes 指定的字節。如果一個主題有20個分區和5個消費者,那麼每個消費者需要至少4 MB的可用內存來接收記錄。在為消費者分配內存時,可以給它們多分配一些,因為如果群組裡有消費者發生崩潰,剩下的消費者需要處理更多的分區。max.partition.fetch.bytes 的值必須比 broker 能夠接收的最大消息的字節數(通過 max.message.size 屬性配置大),否則消費者可能無法讀取這些消息,導致消費者一直掛起重試。 在設置該屬性時,另外一個考量的因素是消費者處理數據的時間。消費者需要頻繁的調用 poll() 方法來避免會話過期和發生分區再平衡,如果單次調用poll() 返回的數據太多,消費者需要更多的時間進行處理,可能無法及時進行下一個輪詢來避免會話過期。如果出現這種情況,可以把 max.partition.fetch.bytes 值改小,或者延長會話過期時間。

  • session.timeout.ms

這個屬性指定了消費者在被認為死亡之前可以與服務器斷開連接的時間,默認是 3s。如果消費者沒有在 session.timeout.ms 指定的時間內發送心跳給群組協調器,就會被認定為死亡,協調器就會觸發重平衡。把它的分區分配給消費者群組中的其它消費者,此屬性與 heartbeat.interval.ms 緊密相關。heartbeat.interval.ms 指定了 poll() 方法向群組協調器發送心跳的頻率,session.timeout.ms 則指定了消費者可以多久不發送心跳。所以,這兩個屬性一般需要同時修改,heartbeat.interval.ms 必須比 session.timeout.ms 小,一般是 session.timeout.ms 的三分之一。如果 session.timeout.ms 是 3s,那麼 heartbeat.interval.ms 應該是 1s。把 session.timeout.ms 值設置的比默認值小,可以更快地檢測和恢復崩憤的節點,不過長時間的輪詢或垃圾收集可能導致非預期的重平衡。把該屬性的值設置得大一些,可以減少意外的重平衡,不過檢測節點崩潰需要更長的時間。

  • auto.offset.reset

該屬性指定了消費者在讀取一個沒有偏移量的分區或者偏移量無效的情況下的該如何處理。它的默認值是 latest,意思指的是,在偏移量無效的情況下,消費者將從最新的記錄開始讀取數據。另一個值是 earliest,意思指的是在偏移量無效的情況下,消費者將從起始位置處開始讀取分區的記錄。

  • enable.auto.commit

我們稍後將介紹幾種不同的提交偏移量的方式。該屬性指定了消費者是否自動提交偏移量,默認值是 true,為了盡量避免出現重複數據和數據丟失,可以把它設置為 false,由自己控制何時提交偏移量。如果把它設置為 true,還可以通過 auto.commit.interval.ms 屬性來控制提交的頻率

  • partition.assignment.strategy

我們知道,分區會分配給群組中的消費者。PartitionAssignor 會根據給定的消費者和主題,決定哪些分區應該被分配給哪個消費者,Kafka 有兩個默認的分配策略RangeRoundRobin

  • client.id

該屬性可以是任意字符串,broker 用他來標識從客戶端發送過來的消息,通常被用在日誌、度量指標和配額中

  • max.poll.records

該屬性用於控制單次調用 call() 方法能夠返回的記錄數量,可以幫你控制在輪詢中需要處理的數據量。

  • receive.buffer.bytes 和 send.buffer.bytes

socket 在讀寫數據時用到的 TCP 緩衝區也可以設置大小。如果它們被設置為 -1,就使用操作系統默認值。如果生產者或消費者與 broker 處於不同的數據中心內,可以適當增大這些值,因為跨數據中心的網絡一般都有比較高的延遲和比較低的帶寬。

提交和偏移量的概念

特殊偏移

我們上面提到,消費者在每次調用poll() 方法進行定時輪詢的時候,會返回由生產者寫入 Kafka 但是還沒有被消費者消費的記錄,因此我們可以追蹤到哪些記錄是被群組裡的哪個消費者讀取的。消費者可以使用 Kafka 來追蹤消息在分區中的位置(偏移量)

消費者會向一個叫做 _consumer_offset 的特殊主題中發送消息,這個主題會保存每次所發送消息中的分區偏移量,這個主題的主要作用就是消費者觸發重平衡後記錄偏移使用的,消費者每次向這個主題發送消息,正常情況下不觸發重平衡,這個主題是不起作用的,當觸發重平衡后,消費者停止工作,每個消費者可能會分到對應的分區,這個主題就是讓消費者能夠繼續處理消息所設置的。

如果提交的偏移量小於客戶端最後一次處理的偏移量,那麼位於兩個偏移量之間的消息就會被重複處理

如果提交的偏移量大於最後一次消費時的偏移量,那麼處於兩個偏移量中間的消息將會丟失

既然_consumer_offset 如此重要,那麼它的提交方式是怎樣的呢?下面我們就來說一下

提交方式

KafkaConsumer API 提供了多種方式來提交偏移量

自動提交

最簡單的方式就是讓消費者自動提交偏移量。如果 enable.auto.commit 被設置為true,那麼每過 5s,消費者會自動把從 poll() 方法輪詢到的最大偏移量提交上去。提交時間間隔由 auto.commit.interval.ms 控制,默認是 5s。與消費者里的其他東西一樣,自動提交也是在輪詢中進行的。消費者在每次輪詢中會檢查是否提交該偏移量了,如果是,那麼就會提交從上一次輪詢中返回的偏移量。

提交當前偏移量

auto.commit.offset 設置為 false,可以讓應用程序決定何時提交偏移量。使用 commitSync() 提交偏移量。這個 API 會提交由 poll() 方法返回的最新偏移量,提交成功后馬上返回,如果提交失敗就拋出異常。

commitSync() 將會提交由 poll() 返回的最新偏移量,如果處理完所有記錄后要確保調用了 commitSync(),否則還是會有丟失消息的風險,如果發生了在均衡,從最近一批消息到發生在均衡之間的所有消息都將被重複處理。

異步提交

異步提交 commitAsync() 與同步提交 commitSync() 最大的區別在於異步提交不會進行重試,同步提交會一致進行重試。

同步和異步組合提交

一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大的問題,因為如果提交失敗是因為臨時問題導致的,那麼後續的提交總會有成功的。但是如果在關閉消費者或再均衡前的最後一次提交,就要確保提交成功。

因此,在消費者關閉之前一般會組合使用commitAsync和commitSync提交偏移量

提交特定的偏移量

消費者API允許調用 commitSync() 和 commitAsync() 方法時傳入希望提交的 partition 和 offset 的 map,即提交特定的偏移量。

文章參考:

《極客時間-Kafka核心技術與實戰》

《Kafka 權威指南》

關注公眾號獲取更多優質电子書,關注一下你就知道資源是有多好了

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

SpringSecurity動態加載用戶角色權限實現登錄及鑒權

很多人覺得Spring Security實現登錄驗證很難,我最開始學習的時候也這樣覺得。因為我好久都沒看懂我該怎麼樣將自己寫的用於接收用戶名密碼的Controller與Spring Security結合使用,這是一個先入為主的誤區。後來我搞懂了:根本不用你自己去寫Controller。你只需要告訴Spring Security用戶信息、角色信息、權限信息、登錄頁是什麼?登陸成功頁是什麼?或者其他有關登錄的一切信息。具體的登錄驗證邏輯它來幫你實現。

一、動態數據登錄驗證的基礎知識

在本號之前的文章中,已經介紹了Spring Security的formLogin登錄認證模式,RBAC的權限控制管理模型,並且針對Spring Security的登錄認證邏輯源碼進行了解析等等。我們所有的用戶、角色、權限信息都是在配置文件裏面寫死的,然而在實際的業務系統中,這些信息通常是存放在RBAC權限模型的數據庫表中的。下面我們來回顧一下其中的核心概念:

  • RBAC的權限模型可以從用戶獲取為用戶分配的一個或多個角色,從用戶的角色又可以獲取該角色的多種權限。通過關聯查詢可以獲取某個用戶的角色信息和權限信息。
  • 在源碼解析的文章中,我們知道如果我們不希望用戶、角色、權限信息寫死在配置裏面。我們應該實現UserDetails與UserDetailsService接口,從而從數據庫或者其他的存儲上動態的加載這些信息。

以上是對一些核心的基礎知識的總結,如果您對這些知識還不是很清晰,建議您先往下讀本文。如果看完本文仍然理解困難,建議您翻看本號之前的文章。

二、UserDetails與UserDetailsService接口

  • UserDetailsService接口有一個方法叫做loadUserByUsername,我們實現動態加載用戶、角色、權限信息就是通過實現該方法。函數見名知義:通過用戶名加載用戶。該方法的返回值就是UserDetails。
  • UserDetails就是用戶信息,即:用戶名、密碼、該用戶所具有的權限。

下面我們來看一下UserDetails接口都有哪些方法。

public interface UserDetails extends Serializable {
    //獲取用戶的權限集合
    Collection<? extends GrantedAuthority> getAuthorities();

    //獲取密碼
    String getPassword();

    //獲取用戶名
    String getUsername();

    //賬號是否沒過期
    boolean isAccountNonExpired();

    //賬號是否沒被鎖定
    boolean isAccountNonLocked();

    //密碼是否沒過期
    boolean isCredentialsNonExpired();

    //賬戶是否可用
    boolean isEnabled();
}

現在,我們明白了,只要我們把這些信息提供給Spring Security,Spring Security就知道怎麼做登錄驗證了,根本不需要我們自己寫Controller實現登錄驗證邏輯。

三、實現UserDetails 接口

public class SysUser implements UserDetails{
    
    String password();  //密碼
    String username();  //用戶名
    boolean accountNonExpired;   //是否沒過期
    boolean accountNonLocked;   //是否沒被鎖定
    boolean credentialsNonExpired;  //是否沒過期
    boolean enabled;  //賬號是否可用
    Collection<? extends GrantedAuthority> authorities;  //用戶的權限集合

    //省略構造方法
    //省略set方法
    //省略get方法(即接口UserDetails的方法)
}

我們就是寫了一個適應於UserDetails的java POJO類,所謂的 UserDetails接口實現就是一些get方法。get方法由Spring Security調用,我們通過set方法或構造函數為 Spring Security提供UserDetails數據。

四、實現UserDetailsService接口

@Component
public class MyUserDetailsService implements UserDetailsService{

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            
       //這裏從數據庫sys_user表裡面查詢實體類對象。loadUser方法可使用Mybatis或JDBC或JPA自行實現。
       SysUser sysUser =  loadUser(username);   

        // 判斷用戶是否存在 
       if(user == null)  {  throw  new  UsernameNotFoundException("用戶名不存在");  }

       //從數據庫該用戶所有的角色信息,所有的權限標誌
       //遍歷所有的ROLE角色及所有的Authority權限(菜單、按鈕)。
       //用逗號分隔他們的唯一標誌,具體過程自行實現。
       sysUser.setAuthorities(
               AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_AMIN , system:user:delete"));
        
        //sysUser.setAccountNonLocked(true或false);
        return sysUser;
    }
}
  • 通常數據庫表sys_user字段要和SysUser屬性一一對應,比如username、password、enabled。但是比如accountNonLocked字段用於登錄多次錯誤鎖定,但我們一般不會在表裡存是否鎖定,而是存一個鎖定時間字段。通過鎖定時間是否大於當前時間判斷賬號是否鎖定,所以實現過程中可以靈活做判斷並用好set方法,不必拘泥於一一對應的形式。
  • 角色是一種特殊的權限,在Spring Security我們可以使用hasRole(角色標識)表達式判斷用戶是否具有某個角色,決定他是否可以做某個操作;通過hasAuthority(權限標識)表達式判斷是否具有某個操作權限。

五、最後說明

至此,我們將系統裏面的所有的用戶、角色、權限信息都通過UserDetailsService和UserDetails告知了Spring Security。但是多數朋友可能仍然不知道該怎樣實現登錄的功能,其實剩下的事情很簡單了:

  • 寫一個登錄界面,寫一個登錄表單,表單使用post方法提交到默認的/login路徑
  • 表單的用戶名、密碼字段名稱默認是username、password。
  • 寫一個登錄成功之後的跳轉頁面,比如index.html

然後把這些信息通過配置方式告知Spring Security ,以上的配置信息名稱都可以靈活修改。如果您不知道如何配置請參考本號之前的文章《formLogin登錄認證模式》。

期待您的關注

  • 向您推薦博主的系列文檔:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

特斯拉 Cybertruck 的 6 個設計原則與 5 個未公開的疑問

特斯拉上週發表了最新車種 Cybertruck,破格的外型與性能讓全球傻眼,本篇文章將告訴你 Cybertruck 設計成這樣的 6 個理由,以及 5 個它尚未說清楚的疑點。

老派科幻電影的線條、全車冷鑄不鏽鋼、跑得比保時捷還快、拉得比福特還給力,特斯拉 Cybertruck 一出場就奪走全世界目光,如同特斯拉執行長馬斯克(Elon Musk)說的,他要改變皮卡車一成不變的外觀。改變歸改變,改成這樣確實是有點誇張了,但 Cybertruck 在設計時,是遵循著以下 6 個原則,才會長成這樣。

  1. 原則一:強悍外表
  2. 原則二:將低風阻,提高效率
  3. 原則三:內在舒適大空間
  4. 原則四:適應不同路面駕駛
  5. 原則五:大載貨量與拉力
  6. 原則六:大電池容量

這些原則(或說是目標),部分來自於馬斯克個人的期待,一部分來自於市場調查的回饋,相信收到這個提案要求的專案,當下應該很想離職。但從發表會的成果來看,他們確實做到了。

為了看起來更強悍,Cybertruck 全車採用 301 不鏽鋼包覆。

皮卡車外表強悍,幾乎是一種固定形象,方方正正的大塊頭是百年傳統。但這樣的外表牴觸了原則二的低風阻要求,同時別忘了,低風阻的跑車,可不會配備原則三的車內大空間。

最終,結合這三項要求的最大公約數,原來就存在於 80 年代的科幻電影跟電玩中,當然這一切若沒有電動馬達是不可能達成的。燃油車的引擎通常配置於車頭,這是為了讓後方留出寬敞的乘坐空間,犧牲了車頭的流線設計,也在傳輸過程中浪費了部分動力。因此追求速度的跑車,通常將車頭壓低,引擎後置,這樣子減低了風阻,也讓動力傳導更直接,但是乘坐空間就非常狹窄,更別提燃油皮卡車後面的大型貨斗了。

Cybertruck 和目前越來越多的電動車一樣,將馬達、電池組整合在底盤,離輪胎更近,也讓上部空間更有彈性,Cybertruck 進一步運用了大眾對皮卡車車身較高的印象,讓它的底盤空間能塞進更多電池,還有其他黑科技,來滿足這些設計原則,像是適應性氣壓懸吊、龐大的電池容量以及可容納六人乘坐的車廂和一個可開閉的貨斗。

從試乘影片中,可以看到後座乘客(身高約 180 公分)在入座後,頭頂上仍然有兩個拳頭左右的空間,算是相當舒適,然而腿部空間似乎就比較侷促,後座寬度看起來和一般房車差不多,塞進兩位大漢剛剛好,整體來說,算是勉強達成了原則三的大空間要求。(官方數據,能容納 6 個成人)

但是,在完成這樣的設計之後,我們會發現,這種外觀線條看起來並不是那麼強悍,因為斜切的車頭讓人聯想到的是跑車,不是皮卡車或悍馬車這種硬漢風格。解決的辦法,就如同我們現在看到的,捨棄一般車輛的鈑金與烤漆,改用與姊妹公司 SpaceX 的太空船同樣材料的 301 不鏽鋼板,而且厚度達到 3 公厘,足以抵擋 9mm 口徑手槍的射擊。代價是,每一扇車門重達 27 公斤,萬一被車門夾到,後果可是不得了。

Cybertruck 內部空間比想像中寬闊,前方只有一塊 17 吋觸控螢幕作為主控台。

5 個等待揭開的疑問

發表會後隔天,馬斯克宣布訂單數量已經突破 14 萬,換句話說,這個發表會已經幫特斯拉賺進了 1,400 萬美元,然而這位愛做夢、愛說大話的老闆,心中也明白最後成交的數量可能不到一半,因為這台科幻裝甲車,仍然有幾個問題需要釐清。

問題一:後照鏡

許多人都發現了,發表會上的 Cybertruck 並沒有裝備車側後照鏡,官網上的影片與照片也都沒有看到,推測是使用攝影機來取代了傳統後照鏡,然而並非所有國家都開放電子後照鏡上路,包含美國跟台灣都還沒開放。在 Cybertruck 試乘的過程中,也沒看到螢幕上有播放左右後側的畫面,只有裝設數位正後方後照鏡,究竟 Cybertruck 怎麼解決車側後照鏡的問題,尚有待解答。

問題二:外觀塗裝

全不鏽鋼車身肯定會吸引到一部分的客群,但是正式販售時會不會提供不同款式的彩繪或是塗裝,並未說明,畢竟許多車主仍然希望自己的愛車能保有一些特殊性,至少花了 7 萬美元購買頂級款的車主,會希望自己的車看起來跟 4 萬美元的有些不同。

而提到外觀就不能不提那一整片乾乾淨淨的車頭,一般車輛會有車頭 Logo、進氣壩等,即使特斯拉其他車款,也都留有進氣孔,或許 Cybertruck 可以不用 Logo 來辨識,但是令人好奇的是它怎麼解決進氣的問題,又或者 Cybertruck 有了不同的方式來處理散熱和車內空氣流通問題

問題三:上下車

一般的皮卡車,車身都較高,對於家有老小的家庭來說,上下車會比較辛苦。從 Cybertruck 的照片中來看,底盤高度約在成年女子的膝蓋,算是有一點點難度。

在發表會上,Cybertruck 展示了它的適應性氣壓懸吊系統,在貨斗載重時,能夠自動調整車身高度;特斯拉官方也表示,當行駛於不同路面時,Cybertruck 會自動調整懸吊,在高速公路時會降低車身,在路面崎嶇時,會提高車身。這讓人不免想知道,當車主或乘客要上下車的時候,車身是否也會自動降低,方便進出呢?

問題四:貨斗操縱方式

市面上的皮卡車,後方貨斗大都是開放式的,也有車主會加裝棚罩,除了保護行李外,最重要是不讓路人亂丟垃圾。Cybertruck 的貨斗是機械式的,在發表會中,可以看到機車騎士走到車尾按了一些隱藏的開關,開啟貨斗,接著再用手拉開尾門,放下斜板。

特斯拉是否會將貨斗開閉設計得更容易使用呢?

對於一般卡車司機來說,這些動作稀鬆平常,但對於標榜高科技的 Cybertruck 來說,似乎不太「性感」,因此我們不免好奇,正式上市後,特斯拉是否會將操作貨斗開閉的功能,放到他們的手機 App 裡,讓車主可以更輕鬆地開關?

問題五:交車時間

最後的問題,也是特斯拉在過去幾年最為人詬病的問題,真的能夠如期交車嗎?特斯拉官網上寫得非常保守,「生產日期接近 2021 年底」,最高規格的三馬達版本,更是標明「預計在 2022 年底開始生產」。這等於是在說,我沒有說什麼時候交車,我只說了什麼時候開始製造,從生產到交車要多久,沒有人知道。

(合作媒體:。圖片來源:)

延伸閱讀:

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

【其他文章推薦】

※公開收購3c價格,不怕被賤賣!

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

自己動手實現分佈式任務調度框架(續)

  之前寫過一篇:本來是用來閑來分享一下自己的思維方式,時至今日發現居然有些人正在使用了,本着對代碼負責人的態度,對代碼部分已知bug進行了修改,並增加了若干功能,如立即啟動,實時停止等功能,新增加的功能會在這一篇做詳細的說明。

  提到分佈式任務調度,市面上本身已經有一些框架工具可以使用,但是個人覺得功能做的都太豐富,架構都過於複雜,所以才有了我重複造輪子。個人喜歡把複雜的問題簡單化,利用有限的資源實現竟可能多的功能。因為有幾個朋友問部署方式,這裏再次強調下:我的這個服務可以直接打成jar放在自己本地倉庫,然後依賴進去,或者直接copy代碼過去,當成自己項目的一部分就可以了。也就是說跟隨你們自己的項目啟動,所以我這裏也沒有寫界面。下面先談談怎麼基於上次的代碼實現任務立即啟動吧!

  調度和自己服務整合後部署圖抽象成如下:

  

 

 

   用戶在前端點擊立即請求按鈕,通過各種負載均衡軟件或者設備,到達某台機器的某個帶有本調度框架的服務,然後進行具體的執行,也就是說這個立即啟動就是一個最常見最簡單的請求,沒有過多複雜的問題(比如多節點會不會重複執行這些)。最簡單的辦法,當用戶請求過來直接用一個線程或者線程池執行用戶點的那個任務的邏輯代碼就行了,當然我這裏沒有那麼粗暴,現有的調度代碼資源如下:

package com.rdpaas.task.scheduler;

import com.rdpaas.task.common.Invocation;
import com.rdpaas.task.common.Node;
import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.common.Task;
import com.rdpaas.task.common.TaskDetail;
import com.rdpaas.task.common.TaskStatus;
import com.rdpaas.task.config.EasyJobConfig;
import com.rdpaas.task.repository.NodeRepository;
import com.rdpaas.task.repository.TaskRepository;
import com.rdpaas.task.strategy.Strategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 任務調度器
 * @author rongdi
 * @date 2019-03-13 21:15
 */
@Component
public class TaskExecutor {

    private static final Logger logger = LoggerFactory.getLogger(TaskExecutor.class);

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private EasyJobConfig config;

    /**
     * 創建任務到期延時隊列
      */
    private DelayQueue<DelayItem<Task>> taskQueue = new DelayQueue<>();

    /**
     * 可以明確知道最多只會運行2個線程,直接使用系統自帶工具就可以了
     */
    private ExecutorService bossPool = Executors.newFixedThreadPool(2);

    /**
     * 正在執行的任務的Future
     */
    private Map<Long,Future> doingFutures = new HashMap<>();

    /**
     * 聲明工作線程池
     */
    private ThreadPoolExecutor workerPool;
    
    /**
     * 獲取任務的策略
     */
    private Strategy strategy;


    @PostConstruct
    public void init() {
        /**
         * 根據配置選擇一個節點獲取任務的策略
         */
        strategy = Strategy.choose(config.getNodeStrategy());
        /**
         * 自定義線程池,初始線程數量corePoolSize,線程池等待隊列大小queueSize,當初始線程都有任務,並且等待隊列滿后
         * 線程數量會自動擴充最大線程數maxSize,當新擴充的線程空閑60s后自動回收.自定義線程池是因為Executors那幾個線程工具
         * 各有各的弊端,不適合生產使用
         */
        workerPool = new ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(config.getQueueSize()));
        /**
         * 執行待處理任務加載線程
         */
        bossPool.execute(new Loader());
        /**
         * 執行任務調度線程
         */
        bossPool.execute(new Boss());
    
    }

    class Loader implements Runnable {

        @Override
        public void run() {
            for(;;) {
                try { 
                    /**
                     * 先獲取可用的節點列表
                     */
                    List<Node> nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
                    if(nodes == null || nodes.isEmpty()) {
                        continue;
                    }
                    /**
                     * 查找還有指定時間(單位秒)才開始的主任務列表
                     */
                    List<Task> tasks = taskRepository.listNotStartedTasks(config.getFetchDuration());
                    if(tasks == null || tasks.isEmpty()) {
                        continue;
                    }
                    for(Task task:tasks) {
                        
                        boolean accept = strategy.accept(nodes, task, config.getNodeId());
                        /**
                         * 不該自己拿就不要搶
                         */
                        if(!accept) {
                            continue;
                        }
                        /**
                         * 先設置成待執行
                         */
                        task.setStatus(TaskStatus.PENDING);
                        task.setNodeId(config.getNodeId());
                        /**
                         * 使用樂觀鎖嘗試更新狀態,如果更新成功,其他節點就不會更新成功。如果其它節點也正在查詢未完成的
                         * 任務列表和當前這段時間有節點已經更新了這個任務,version必然和查出來時候的version不一樣了,這裏更新
                         * 必然會返回0了
                         */
                        int n = taskRepository.updateWithVersion(task);
                        Date nextStartTime = task.getNextStartTime();
                        if(n == 0 || nextStartTime == null) {
                            continue;
                        }
                        /**
                         * 封裝成延時對象放入延時隊列,這裏再查一次是因為上面樂觀鎖已經更新了版本,會導致後面結束任務更新不成功
                         */
                        task = taskRepository.get(task.getId());
                        DelayItem<Task> delayItem = new DelayItem<Task>(nextStartTime.getTime() - new Date().getTime(), task);
                        taskQueue.offer(delayItem);
                        
                    }
                    Thread.sleep(config.getFetchPeriod());
                } catch(Exception e) {
                    logger.error("fetch task list failed,cause by:{}", e);
                }
            }
        }
        
    }
    
    class Boss implements Runnable {
        @Override
        public void run() {
            for (;;) {
                try {
                     /**
                     * 時間到了就可以從延時隊列拿出任務對象,然後交給worker線程池去執行
                     */
                    DelayItem<Task> item = taskQueue.take();
                    if(item != null && item.getItem() != null) {
                        Task task = item.getItem();
                        /**
                         * 真正開始執行了設置成執行中
                         */
                        task.setStatus(TaskStatus.DOING);
                        /**
                         * loader線程中已經使用樂觀鎖控制了,這裏沒必要了
                         */
                        taskRepository.update(task);
                        /**
                         * 提交到線程池
                         */
                        Future future = workerPool.submit(new Worker(task));
                        /**
                         * 暫存在doingFutures
                         */
                        doingFutures.put(task.getId(),future);
                    }
                     
                } catch (Exception e) {
                    logger.error("fetch task failed,cause by:{}", e);
                }
            }
        }

    }

    class Worker implements Callable<String> {

        private Task task;

        public Worker(Task task) {
            this.task = task;
        }

        @Override
        public String call() {
            logger.info("Begin to execute task:{}",task.getId());
            TaskDetail detail = null;
            try {
                //開始任務
                detail = taskRepository.start(task);
                if(detail == null) return null;
                //執行任務
                task.getInvokor().invoke();
                //完成任務
                finish(task,detail);
                logger.info("finished execute task:{}",task.getId());
                /**
                 * 執行完后刪了
                 */
                doingFutures.remove(task.getId());
            } catch (Exception e) {
                logger.error("execute task:{} error,cause by:{}",task.getId(), e);
                try {
                    taskRepository.fail(task,detail,e.getCause().getMessage());
                } catch(Exception e1) {
                    logger.error("fail task:{} error,cause by:{}",task.getId(), e);
                }
            }
            return null;
        }

    }

    /**
     * 完成子任務,如果父任務失敗了,子任務不會執行
     * @param task
     * @param detail
     * @throws Exception
     */
    private void finish(Task task,TaskDetail detail) throws Exception {

        //查看是否有子類任務
        List<Task> childTasks = taskRepository.getChilds(task.getId());
        if(childTasks == null || childTasks.isEmpty()) {
            //當沒有子任務時完成父任務
            taskRepository.finish(task,detail);
            return;
        } else {
            for (Task childTask : childTasks) {
                //開始任務
                TaskDetail childDetail = null;
                try {
                    //將子任務狀態改成執行中
                    childTask.setStatus(TaskStatus.DOING);
                    childTask.setNodeId(config.getNodeId());
                    //開始子任務
                    childDetail = taskRepository.startChild(childTask,detail);
                    //使用樂觀鎖更新下狀態,不然這裏可能和恢複線程產生併發問題
                    int n = taskRepository.updateWithVersion(childTask);
                    if (n > 0) {
                        //再從數據庫取一下,避免上面update修改后version不同步
                        childTask = taskRepository.get(childTask.getId());
                        //執行子任務
                        childTask.getInvokor().invoke();
                        //完成子任務
                        finish(childTask, childDetail);
                    }
                } catch (Exception e) {
                    logger.error("execute child task error,cause by:{}", e);
                    try {
                        taskRepository.fail(childTask, childDetail, e.getCause().getMessage());
                    } catch (Exception e1) {
                        logger.error("fail child task error,cause by:{}", e);
                    }
                }
            }
            /**
             * 當有子任務時完成子任務后再完成父任務
             */
            taskRepository.finish(task,detail);

        }

    }

    /**
     * 添加任務
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addTask(String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        return taskRepository.insert(task);
    }

    /**
     * 添加子任務
     * @param pid
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addChildTask(Long pid,String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        task.setPid(pid);
        return taskRepository.insert(task);
    }

   
}

  上面主要就是三組線程,Loader負責加載將要執行的任務放入本地的任務隊列,Boss線程負責取出任務隊列的任務,然後分配Worker線程池的一個線程去執行。由上面的代碼可以看到如果要立即執行,其實只需要把一個延時為0的任務放入任務隊列,等着Boss線程去取然後分配給worker執行就可以實現了,代碼如下:

    /**
     * 立即執行任務,就是設置一下延時為0加入任務隊列就好了,這個可以外部直接調用
     * @param taskId
     * @return
     */
    public boolean startNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        task.setStatus(TaskStatus.DOING);
        taskRepository.update(task);
        DelayItem<Task> delayItem = new DelayItem<Task>(0L, task);
        return taskQueue.offer(delayItem);
    }

  啟動不用再多說,下面介紹一下停止任務,根據面向對象的思維,用戶要想停止一個任務,最終執行停止任務的就是正在執行任務的那個節點。停止任務有兩種情況,第一種任務沒有正在運行如何停止,第二種是任務正在運行如何停止。第一種其實直接改變一下任務對象的狀態為停止就行了,不必多說。下面主要考慮如何停止正在運行的任務,細心的朋友可能已經發現上面代碼和之前那一篇代碼有點區別,之前用的Runnble作為線程實現接口,這個用了Callable,其實在java中停止線程池中正在運行的線程最常用的就是直接調用future的cancel方法了,要想獲取到這個future對象就需要將以前實現Runnbale改成實現Callable,然後提交到線程池由execute改成submit就可以了,然後每次提交到線程池得到的future對象使用taskId一起保存在一個map中,方便根據taskId隨時找到。當然任務執行完后要及時刪除這個map里的任務,以免常駐其中導致內存溢出。停止任務的請求流程如下

  

 

 

  圖還是原來的圖,但是這時候情況不一樣了,因為停止任務的時候假如當前正在執行這個任務的節點處於服務1,負載均衡是不知道要去把你引到服務1的,他可能會引入到服務2,那就悲劇了,所以通用的做法就是停止請求過來不管落到哪個節點上,那個節點就往一個公用的mq上發一個帶有停止任務業務含義的消息,各個節點訂閱這個消息,然後判斷都判斷任務在不在自己這裏執行,如果在就執行停止操作。但是這樣勢必讓我們的調度服務又要依賴一個外部的消息隊列服務,就算很方便的就可以引入一個外部的消息隊列,但是你真的可以駕馭的了嗎,消息丟了咋辦,重複發送了咋辦,消息服務掛了咋辦,網絡斷了咋辦,又引入了一大堆問題,那我是不是又要寫n篇文章來分別解決這些問題。往往現實卻是就是這麼殘酷,你解決了一個問題,引入了更多的問題,這就是為什麼bug永遠改不完的道理了。當然這不是我的風格,我的風格是利用有限的資源做盡可能多的事情(可能是由於我工作的企業都是那種資源貧瘠的,養成了我這種習慣,土豪公司的程序員請繞道,哈哈)。

  簡化一下問題:目前的問題就是如何讓正在執行任務的節點知道,然後停止正在執行的這個任務,其實就是這個停止通知如何實現。這不免讓我想起了12306網站上買票,其實我們作為老百姓多麼希望12306可以在有票的時候發個短信通知一下我們,然後我們上去搶,但是現實卻是,你要麼使用軟件一直刷,要麼是自己隔一段時間上去瞄一下有沒有票。如果把有票了給我們發短信通知定義為異步通知,那麼這種我們要隔一段時間自己去瞄一下的方式就是同步輪訓。這兩種方式都能達到告知的目的,關鍵的區別在於你到底有沒有時間去一直去瞄,不過相比於可以回家,這些時間都是值得的。個人認為軟件的設計其實就是一個權衡是否值得的過程。如果約定了不使用外部消息隊列這種異步通知的方式,那麼我們只能使用同步輪訓的方式了。不過正好我們的任務調度本身已經有一個心跳機制,沒隔一段時間就去更新一下節點狀態,如果我們把用戶的停止請求作為命令信息更新到每個節點的上,然後隨着心跳獲取到這個節點的信息,然後判斷這個命令,做相應的處理是不是就可以完美解決這個問題。值得嗎?很明顯是值得的,我們只是在心跳邏輯上加一個小小的副作用就實現了通知功能了。代碼如下

package com.rdpaas.task.common;

/**
 * @author rongdi
 * @date 2019/11/26
 */
public enum NotifyCmd {

    //沒有通知,默認狀態
    NO_NOTIFY(0),
    //開啟任務(Task)
    START_TASK(1),
    //修改任務(Task)
    EDIT_TASK(2),
    //停止任務(Task)
    STOP_TASK(3);

    int id;

    NotifyCmd(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public static NotifyCmd valueOf(int id) {
        switch (id) {
            case 1:
                return START_TASK;
            case 2:
                return EDIT_TASK;
            case 3:
                return STOP_TASK;
            default:
                return NO_NOTIFY;
        }
    }

}
package com.rdpaas.task.handles;

import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.utils.SpringContextUtil;

/**
 * @author: rongdi
 * @date:
 */
public interface NotifyHandler<T> {

    static NotifyHandler chooseHandler(NotifyCmd notifyCmd) {
        return SpringContextUtil.getByTypeAndName(NotifyHandler.class,notifyCmd.toString());
    }

    public void update(T t);

}
package com.rdpaas.task.handles;

import com.rdpaas.task.scheduler.TaskExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author: rongdi
 * @date:
 */
@Component("STOP_TASK")
public class StopTaskHandler implements NotifyHandler<Long> {

    @Autowired
    private TaskExecutor taskExecutor;

    @Override
    public void update(Long taskId) {
        taskExecutor.stop(taskId);
    }

}
class HeartBeat implements Runnable {
        @Override
        public void run() {
            for(;;) {
                try {
                    /**
                     * 時間到了就可以從延時隊列拿出節點對象,然後更新時間和序號,
                     * 最後再新建一個超時時間為心跳時間的節點對象放入延時隊列,形成循環的心跳
                     */
                    DelayItem<Node> item = heartBeatQueue.take();
                    if(item != null && item.getItem() != null) {
                        Node node = item.getItem();
                        handHeartBeat(node);
                    }
                    heartBeatQueue.offer(new DelayItem<>(config.getHeartBeatSeconds() * 1000,new Node(config.getNodeId())));
                } catch (Exception e) {
                    logger.error("task heart beat error,cause by:{} ",e);
                }
            }
        }
    }

    /**
     * 處理節點心跳
     * @param node
     */
    private void handHeartBeat(Node node) {
        if(node == null) {
            return;
        }
        /**
         * 先看看數據庫是否存在這個節點
         * 如果不存在:先查找下一個序號,然後設置到node對象中,最後插入
         * 如果存在:直接根據nodeId更新當前節點的序號和時間
         */
        Node currNode= nodeRepository.getByNodeId(node.getNodeId());
        if(currNode == null) {
            node.setRownum(nodeRepository.getNextRownum());
            nodeRepository.insert(node);
        } else  {
            nodeRepository.updateHeartBeat(node.getNodeId());
            NotifyCmd cmd = currNode.getNotifyCmd();
            String notifyValue = currNode.getNotifyValue();
            if(cmd != null && cmd != NotifyCmd.NO_NOTIFY) {
                /**
                 * 藉助心跳做一下通知的事情,比如及時停止正在執行的任務
                 * 根據指令名稱查找Handler
                 */
                NotifyHandler handler = NotifyHandler.chooseHandler(currNode.getNotifyCmd());
                if(handler == null || StringUtils.isEmpty(notifyValue)) {
                    return;
                }
                /**
                 * 執行操作
                 */
                handler.update(Long.valueOf(notifyValue));
            }
            
        }


    }

  最終的任務調度代碼如下:

package com.rdpaas.task.scheduler;

import com.rdpaas.task.common.Invocation;
import com.rdpaas.task.common.Node;
import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.common.Task;
import com.rdpaas.task.common.TaskDetail;
import com.rdpaas.task.common.TaskStatus;
import com.rdpaas.task.config.EasyJobConfig;
import com.rdpaas.task.repository.NodeRepository;
import com.rdpaas.task.repository.TaskRepository;
import com.rdpaas.task.strategy.Strategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 任務調度器
 * @author rongdi
 * @date 2019-03-13 21:15
 */
@Component
public class TaskExecutor {

    private static final Logger logger = LoggerFactory.getLogger(TaskExecutor.class);

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private EasyJobConfig config;

    /**
     * 創建任務到期延時隊列
      */
    private DelayQueue<DelayItem<Task>> taskQueue = new DelayQueue<>();

    /**
     * 可以明確知道最多只會運行2個線程,直接使用系統自帶工具就可以了
     */
    private ExecutorService bossPool = Executors.newFixedThreadPool(2);

    /**
     * 正在執行的任務的Future
     */
    private Map<Long,Future> doingFutures = new HashMap<>();

    /**
     * 聲明工作線程池
     */
    private ThreadPoolExecutor workerPool;
    
    /**
     * 獲取任務的策略
     */
    private Strategy strategy;


    @PostConstruct
    public void init() {
        /**
         * 根據配置選擇一個節點獲取任務的策略
         */
        strategy = Strategy.choose(config.getNodeStrategy());
        /**
         * 自定義線程池,初始線程數量corePoolSize,線程池等待隊列大小queueSize,當初始線程都有任務,並且等待隊列滿后
         * 線程數量會自動擴充最大線程數maxSize,當新擴充的線程空閑60s后自動回收.自定義線程池是因為Executors那幾個線程工具
         * 各有各的弊端,不適合生產使用
         */
        workerPool = new ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(config.getQueueSize()));
        /**
         * 執行待處理任務加載線程
         */
        bossPool.execute(new Loader());
        /**
         * 執行任務調度線程
         */
        bossPool.execute(new Boss());
    
    }

    class Loader implements Runnable {

        @Override
        public void run() {
            for(;;) {
                try { 
                    /**
                     * 先獲取可用的節點列表
                     */
                    List<Node> nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
                    if(nodes == null || nodes.isEmpty()) {
                        continue;
                    }
                    /**
                     * 查找還有指定時間(單位秒)才開始的主任務列表
                     */
                    List<Task> tasks = taskRepository.listNotStartedTasks(config.getFetchDuration());
                    if(tasks == null || tasks.isEmpty()) {
                        continue;
                    }
                    for(Task task:tasks) {
                        
                        boolean accept = strategy.accept(nodes, task, config.getNodeId());
                        /**
                         * 不該自己拿就不要搶
                         */
                        if(!accept) {
                            continue;
                        }
                        /**
                         * 先設置成待執行
                         */
                        task.setStatus(TaskStatus.PENDING);
                        task.setNodeId(config.getNodeId());
                        /**
                         * 使用樂觀鎖嘗試更新狀態,如果更新成功,其他節點就不會更新成功。如果其它節點也正在查詢未完成的
                         * 任務列表和當前這段時間有節點已經更新了這個任務,version必然和查出來時候的version不一樣了,這裏更新
                         * 必然會返回0了
                         */
                        int n = taskRepository.updateWithVersion(task);
                        Date nextStartTime = task.getNextStartTime();
                        if(n == 0 || nextStartTime == null) {
                            continue;
                        }
                        /**
                         * 封裝成延時對象放入延時隊列,這裏再查一次是因為上面樂觀鎖已經更新了版本,會導致後面結束任務更新不成功
                         */
                        task = taskRepository.get(task.getId());
                        DelayItem<Task> delayItem = new DelayItem<Task>(nextStartTime.getTime() - new Date().getTime(), task);
                        taskQueue.offer(delayItem);
                        
                    }
                    Thread.sleep(config.getFetchPeriod());
                } catch(Exception e) {
                    logger.error("fetch task list failed,cause by:{}", e);
                }
            }
        }
        
    }
    
    class Boss implements Runnable {
        @Override
        public void run() {
            for (;;) {
                try {
                     /**
                     * 時間到了就可以從延時隊列拿出任務對象,然後交給worker線程池去執行
                     */
                    DelayItem<Task> item = taskQueue.take();
                    if(item != null && item.getItem() != null) {
                        Task task = item.getItem();
                        /**
                         * 真正開始執行了設置成執行中
                         */
                        task.setStatus(TaskStatus.DOING);
                        /**
                         * loader線程中已經使用樂觀鎖控制了,這裏沒必要了
                         */
                        taskRepository.update(task);
                        /**
                         * 提交到線程池
                         */
                        Future future = workerPool.submit(new Worker(task));
                        /**
                         * 暫存在doingFutures
                         */
                        doingFutures.put(task.getId(),future);
                    }
                     
                } catch (Exception e) {
                    logger.error("fetch task failed,cause by:{}", e);
                }
            }
        }

    }

    class Worker implements Callable<String> {

        private Task task;

        public Worker(Task task) {
            this.task = task;
        }

        @Override
        public String call() {
            logger.info("Begin to execute task:{}",task.getId());
            TaskDetail detail = null;
            try {
                //開始任務
                detail = taskRepository.start(task);
                if(detail == null) return null;
                //執行任務
                task.getInvokor().invoke();
                //完成任務
                finish(task,detail);
                logger.info("finished execute task:{}",task.getId());
                /**
                 * 執行完后刪了
                 */
                doingFutures.remove(task.getId());
            } catch (Exception e) {
                logger.error("execute task:{} error,cause by:{}",task.getId(), e);
                try {
                    taskRepository.fail(task,detail,e.getCause().getMessage());
                } catch(Exception e1) {
                    logger.error("fail task:{} error,cause by:{}",task.getId(), e);
                }
            }
            return null;
        }

    }

    /**
     * 完成子任務,如果父任務失敗了,子任務不會執行
     * @param task
     * @param detail
     * @throws Exception
     */
    private void finish(Task task,TaskDetail detail) throws Exception {

        //查看是否有子類任務
        List<Task> childTasks = taskRepository.getChilds(task.getId());
        if(childTasks == null || childTasks.isEmpty()) {
            //當沒有子任務時完成父任務
            taskRepository.finish(task,detail);
            return;
        } else {
            for (Task childTask : childTasks) {
                //開始任務
                TaskDetail childDetail = null;
                try {
                    //將子任務狀態改成執行中
                    childTask.setStatus(TaskStatus.DOING);
                    childTask.setNodeId(config.getNodeId());
                    //開始子任務
                    childDetail = taskRepository.startChild(childTask,detail);
                    //使用樂觀鎖更新下狀態,不然這裏可能和恢複線程產生併發問題
                    int n = taskRepository.updateWithVersion(childTask);
                    if (n > 0) {
                        //再從數據庫取一下,避免上面update修改后version不同步
                        childTask = taskRepository.get(childTask.getId());
                        //執行子任務
                        childTask.getInvokor().invoke();
                        //完成子任務
                        finish(childTask, childDetail);
                    }
                } catch (Exception e) {
                    logger.error("execute child task error,cause by:{}", e);
                    try {
                        taskRepository.fail(childTask, childDetail, e.getCause().getMessage());
                    } catch (Exception e1) {
                        logger.error("fail child task error,cause by:{}", e);
                    }
                }
            }
            /**
             * 當有子任務時完成子任務后再完成父任務
             */
            taskRepository.finish(task,detail);

        }

    }

    /**
     * 添加任務
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addTask(String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        return taskRepository.insert(task);
    }

    /**
     * 添加子任務
     * @param pid
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addChildTask(Long pid,String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        task.setPid(pid);
        return taskRepository.insert(task);
    }

    /**
     * 立即執行任務,就是設置一下延時為0加入任務隊列就好了,這個可以外部直接調用
     * @param taskId
     * @return
     */
    public boolean startNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        task.setStatus(TaskStatus.DOING);
        taskRepository.update(task);
        DelayItem<Task> delayItem = new DelayItem<Task>(0L, task);
        return taskQueue.offer(delayItem);
    }

    /**
     * 立即停止正在執行的任務,留給外部調用的方法
     * @param taskId
     * @return
     */
    public boolean stopNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        if(task == null) {
            return false;
        }
        /**
         * 該任務不是正在執行,直接修改task狀態為已完成即可
         */
        if(task.getStatus() != TaskStatus.DOING) {
            task.setStatus(TaskStatus.STOP);
            taskRepository.update(task);
            return true;
        }
        /**
         * 該任務正在執行,使用節點配合心跳發布停用通知
         */
        int n = nodeRepository.updateNotifyInfo(NotifyCmd.STOP_TASK,String.valueOf(taskId));
        return n > 0;
    }

    /**
     * 立即停止正在執行的任務,這個不需要自己調用,是給心跳線程調用
     * @param taskId
     * @return
     */
    public boolean stop(Long taskId) {
        Task task = taskRepository.get(taskId);
        /**
         * 不是自己節點的任務,本節點不能執行停用
         */
        if(task == null || !config.getNodeId().equals(task.getNodeId())) {
            return false;
        }
        /**
         * 拿到正在執行任務的future,然後強制停用,並刪除doingFutures的任務
         */
        Future future = doingFutures.get(taskId);
        boolean flag =  future.cancel(true);
        if(flag) {
            doingFutures.remove(taskId);
            /**
             * 修改狀態為已停用
             */
            task.setStatus(TaskStatus.STOP);
            taskRepository.update(task);
        }
        /**
         * 重置通知信息,避免重複執行停用通知
         */
        nodeRepository.resetNotifyInfo(NotifyCmd.STOP_TASK);
        return flag;
    }
}

  好吧,其實實現很簡單,關鍵在於思路,不BB了,詳細代碼見: 在下告辭!

  

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

嵌入式、C語言位操作的一些技巧匯總

下面分享關於位操作的一些筆記:

一、位操作簡單介紹

首先,以下是按位運算符:

嵌入式編程中,常常需要對一些寄存器進行配置,有的情況下需要改變一個字節中的某一位或者幾位,但是又不想改變其它位原有的值,這時就可以使用按位運算符進行操作。下面進行舉例說明,假如有一個8位的TEST寄存器:

當我們要設置第0位bit0的值為1時,可能會這樣進行設置:

TEST = 0x01;

但是,這樣設置是不夠準確的,因為這時候已經同時操作到了高7位:bit1~bit7,如果這高7位沒有用到的話,這麼設置沒有什麼影響;但是,如果這7位正在被使用,結果就不是我們想要的了。

在這種情況下,我們就可以借用按位操作運算符進行配置。

對於二進制位操作來說,不管該位原來的值是0還是1,它跟0進行&運算,得到的結果都是0,而跟1進行&運算,將保持原來的值不變;不管該位原來的值是0還是1,它跟1進行|運算,得到的結果都是1,而跟0進行|運算,將保持原來的值不變。

所以,此時可以設置為:

TEST = TEST | 0x01;

其意義為:TEST寄存器的高7位均不變,最低位變成1了。在實際編程中,常改寫為:

TEST |= 0x01;

這種寫法可以一定程度上簡化代碼,是 C 語言常用的一種編程風格。設置寄存器的某一位還有另一種操作方法,以上的等價方法如:

TEST |= (0x01 << 0);

第幾位要置1就左移幾位。

同樣的,要給TEST的低4位清0,高4位保持不變,可以進行如下配置:

TEST &= 0xF0;

二、嵌入式中位操作一些常見用法

1、一個32bit數據的位、字節讀取操作

(1)獲取單字節:

#define GET_LOW_BYTE0(x)    ((x >>  0) & 0x000000ff)    /* 獲取第0個字節 */
#define GET_LOW_BYTE1(x)    ((x >>  8) & 0x000000ff)    /* 獲取第1個字節 */
#define GET_LOW_BYTE2(x)    ((x >> 16) & 0x000000ff)    /* 獲取第2個字節 */
#define GET_LOW_BYTE3(x)    ((x >> 24) & 0x000000ff)    /* 獲取第3個字節 */

示例:

(2)獲取某一位:

#define GET_BIT(x, bit) ((x & (1 << bit)) >> bit)   /* 獲取第bit位 */

示例:

2、一個32bit數據的位、字節清零操作

(1)清零某個字節:

#define CLEAR_LOW_BYTE0(x)  (x &= 0xffffff00)   /* 清零第0個字節 */
#define CLEAR_LOW_BYTE1(x)  (x &= 0xffff00ff)   /* 清零第1個字節 */
#define CLEAR_LOW_BYTE2(x)  (x &= 0xff00ffff)   /* 清零第2個字節 */
#define CLEAR_LOW_BYTE3(x)  (x &= 0x00ffffff)   /* 清零第3個字節 */

示例:

(2)清零某一位:

#define CLEAR_BIT(x, bit)   (x &= ~(1 << bit))  /* 清零第bit位 */

示例:

3、一個32bit數據的位、字節置1操作

(1)置某個字節為1:

#define SET_LOW_BYTE0(x)    (x |= 0x000000ff)   /* 第0個字節置1 */   
#define SET_LOW_BYTE1(x)    (x |= 0x0000ff00)   /* 第1個字節置1 */   
#define SET_LOW_BYTE2(x)    (x |= 0x00ff0000)   /* 第2個字節置1 */   
#define SET_LOW_BYTE3(x)    (x |= 0xff000000)   /* 第3個字節置1 */

示例:

(2)置位某一位:

#define SET_BIT(x, bit) (x |= (1 << bit))   /* 置位第bit位 */

4、判斷某一位或某幾位連續位的值

(1)判斷某一位的值

舉例說明:判斷0x68第3位的值。

也就是說,要判斷第幾位的值,if里就左移幾位(當然別過頭了)。在嵌入式編程中,可通過這樣的方式來判斷寄存器的狀態位是否被置位。

(2)判斷某幾位連續位的值

/* 獲取第[n:m]位的值 */
#define BIT_M_TO_N(x, m, n)  ((unsigned int)(x << (31-(n))) >> ((31 - (n)) + (m)))

示例:

這是一個查詢連續狀態位的例子,因為有些情況不止有0、1兩種狀態,可能會有多種狀態,這種情況下就可以用這種方法來取出狀態位,再去執行相應操作。

以上是對32bit數據的一些操作進行總結,其它位數的數據類似,可根據需要進行修改。

三、STM32寄存器配置

STM32有幾套固件庫,這些固件庫函數以函數的形式進行1層或者多層封裝(軟件開發中很重要的思想之一:分層思想),但是到了最裏面的一層就是對寄存器的配置。我們平時都比較喜歡固件庫來開發,大概是因為固件庫用起來比較簡單,用固件庫寫出來的代碼比較容易閱讀。最近一段時間一直在配置寄存器,越發地發現使用寄存器來進行一些外設的配置也是很容易懂的。使用寄存器的方式編程無非就是往寄存器的某些位置1、清零以及對寄存器一些狀態位進行判斷、讀取寄存器的內容等。

這些基本操作在上面的例子中已經有介紹,我們依舊以實例來鞏固上面的知識點(以STM32F1xx為例):

(1)寄存器配置

看一下GPIO功能的端口輸出數據寄存器 (GPIOx_ODR) (x=A..E) :

假設我們要讓PA10引腳輸出高、輸出低,可以這麼做:

方法一:

GPIOA->ODR |= 1 << 10;      /* PA10輸出高(置1操作) */
GPIOA->ODR &= ~(1 << 10);  /* PA10輸出低(清0操作) */

也可用我們上面的置位、清零的宏定義:

SET_BIT(GPIOA->ODR, 10);    /* PA10輸出高(置1操作) */
CLEAR_BIT(GPIOA->ODR, 10);  /* PA10輸出低(清0操作) */

方法二:

GPIOA->ODR |= (uint16_t)0x0400;   /* PA10輸出高(置1操作) */
GPIOA->ODR &= ~(uint16_t)0x0400;  /* PA10輸出低(清0操作) */

貌似第二種方法更麻煩?還得去細心地去構造一個數據。

但是,其實第二種方法其實是ST推薦我們用的方法,為什麼這麼說呢?因為ST官方已經把這些我們要用到的值給我們配好了,在stm32f10x.h中:

這個頭文件中存放的就是外設寄存器的一些位配置。

所以我們的方法二等價於:

GPIOA->ODR |= GPIO_ODR_ODR10;   /* PA10輸出高(置1操作) */
GPIOA->ODR &= ~GPIO_ODR_ODR10;  /* PA10輸出低(清0操作) */

兩種方法都是很好的方法,但方法一似乎更好理解。

配置連續幾位的方法也是一樣的,就不介紹了。簡單介紹配置不連續位的方法,以TIM1的CR1寄存器為例:

設置CEN位為1、設置CMS[1:0]位為01、設置CKD[1:0]位為10:

TIM1->CR1 |= (0x1 << 1)| (0x1 << 5) |(0x2 << 8);

這是組合的寫法。當然,像上面一樣拆開來寫也是可以的。

(2)判斷標誌位

以狀態寄存器(USART_SR) 為例:

判斷RXNE是否被置位:

/* 數據寄存器非空,RXNE標誌置位 */
if (USART1->SR & (1 << 5))
{
    /* 其它代碼 */
    
    USART1->SR &= ~(1 << 5);  /* 清零RXNE標誌 */
}

或者:

/* 數據寄存器非空,RXNE標誌置位 */
if (USART1->SR & USART_SR_RXNE)
{
    /* 其它代碼 */
    
    USART1->SR &= ~USART_SR_RXNE;  /* 清零RXNE標誌 */
}

四、總結

以上就是本次關於位操作的一點總結筆記,有必要掌握。雖然說在用STM32的時候有庫函數可以用,但是最接近芯片內部原理的還是寄存器。有可能之後有用到其它芯片沒有像ST這樣把寄存器相關配置封裝得那麼好,那就不得不直接操控寄存器了。

此外,使用庫函數的方式代碼佔用空間大,用寄存器的話,代碼佔用空間小。之前有個需求,我們能用的Flash的空間大小隻有4KB,遇到類似這樣的情況就不能那麼隨性的用庫函數了。

最後,應用的時候當然是怎麼簡單就怎麼用。學從“難”處學,用從易處用,與君共勉~

END:以上筆記中如有錯誤,歡迎指出!謝謝

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

JavaScript 關於setTimeout與setInterval的小研究

說明

在開發功能“軌跡播放”時,遇到了一個情況。
原先同事已經開發了一版,這次有個新功能:點擊線上任意一點后可以從點擊處重新播放。
看了一下原來的版本,發現同時使用了setTimeout和setInterval,兩者配合實現點線播放。
簡單結構如下

        function test() {
            setInterval(function () {
                console.log("interval");
                                //省略插值方法得到arr
                                (...)
                play(arr);
            }, 2000);
        }
        function play(arr) {
            setTimeout(function () {
                play(arr);
                console.log("setTimeout");
            }, 40);
        }

我覺得這個結構欠妥,兩個定時器配合必定會出現失誤!因此重構了一版,將兩個定時器改為一個,用setInterval解決。
但是此時我並不知道欠妥欠在什麼地方,缺乏理論支持,現在閑下來仔細研究了一下

找問題

在仔細研究了舊版本后,我先把舊版本結構扒了出來,排除其他因素,自己模擬了一個簡單版(就是上面的代碼)
setTimeout:在執行時,是在載入后延遲指定時間后,去執行一次表達式,僅執行一次
setInterval:在執行時,它從載入后,每隔指定的時間就執行一次表達式

  • 實驗一:在使用setInterval和setTimeout方法上,並沒有什麼問題,決定跑一下,結果如下

從結果得出兩點結論

  1. setTimeout與setInterval並不是50倍速度配合運行着
  2. 兩次interval間,timeout運行的次數越來越多,表明setInterval運行間隔越來越長,延遲越來越大
  • 實驗二:加一點人工干預再執行
        function test() {
            setInterval(function () {
                console.log("interval");
                play();
            }, 2000);
        }
        function play() {
                    //延遲執行
            for (var i = 0; i < 100000000; i++) {
                
             }
            setTimeout(function () {
                play();
                console.log("setTimeout");
            }, 40);
        }

從結果得出兩點結論

  1. setInterval可能會隨函數處理時間,減少間隔
  2. 推測,因為Javascript是單線程的,setInterval和setTimeout是放隊列里執行的,很容易受到回調事件影響
  • 實驗三:拖動縮放瀏覽器

從結果得出結論

  1. 當瀏覽器標籤切換到其他頁面,或者瀏覽器最小化,會影響計時器,兩者會出現間隔減小

涉及知識點

綜上實驗結果,網上搜集了一些資料能說明問題:

  1. JavaScript是單線程,但是瀏覽器是多線程,Javascript是瀏覽器多線程中的一個線程。(圖參考自:)
  1. Javascript會把執行的回調函數、瀏覽器的觸發事件、UI渲染事件,先放到隊列中,隊列根據先進先出的規則,依次執行他們,當執行到隊列中的setInterval時很難保證其與setTimeout同步關係還保持。
  2. setInterval無視代碼錯誤:代碼報錯,但是setInterval依舊會按時執行,不會中斷。
  3. setInterval無視網絡延遲:如果調用ajax或其他服務,他不會管是否返回回調,會繼續按時執行。
  4. setInterval不保證執行:因為setInterval會定時執行,如果函數邏輯很長,間隔時間內執行不完,後續方法會被拋棄。
  5. 會受瀏覽器狀態影響,tab切換、最小化等

解決方案

在做軌跡播放時,setInterval的延遲還在可接受範圍之內,但是網上給出的最佳解決方案是用setTimeout做。
setTimeout只會執行一次,在執行完成后,重新啟動新的Timeout,時間runtime計算設置為差時,減少出現間隔越來越大的情況

        function test() {
                    //runTime,計算差時
                        runTime = 1000 - 執行耗時;
            setTimeout(callback, runTime);
        }
        setTimeout(test, 1000);

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

※高價收購3C產品,價格不怕你比較

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

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

3c收購,鏡頭 收購有可能以全新價回收嗎?

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

關係型數據庫幾大範式的理解總結

範式的定義

  • 關係型數據庫中的關係是需要滿足一定條件的,滿足這些不同程度的規範化就叫做範式。

  • 範式按照規範化程度從低到高排序為第一範式,第二範式,第三範式,BC範式,第四範式,第五範式。

前導知識

函數依賴

R(U)是屬性集U的關係模型,X,Y是U的一個子集,對於R(U)中的任一個關係r,不可能存在兩個元組在X上屬性值相同,而在Y上屬性值不同。則稱X函數確定Y,或Y函數依賴X。

  • 說人話:U是表(可能不止一個表,可以是有關係的多個表)的所有列,X,Y分別是這些屬性列的一個子集,也就是若干個屬性,對於所有在X這些屬性上的值一樣的行,在Y上的屬性上也必須一樣,滿足這樣條件的這若干個屬性 X和Y叫稱其函數依賴。
  • X相同則Y必須相同,但X不同Y可以相同,也可以不同
  • 如果Y是X的子集,就叫平凡的函數依賴,一般不考慮這種,因為就是廢話,X整個都相同,子集肯定相同。
  • 如果Y不是X的子集,叫做非平凡的函數依賴
  • 如果Y函數依賴X,那麼X稱為決定因素。
  • 如果Y函數依賴X,但不依賴X的任何一個真子集,也就是X是極小的,那就稱Y完全函數依賴X,否則稱Y部分函數依賴X
  • 如果X決定Y,Y決定Z,且Y不決定X,那麼稱Z對X傳遞函數依賴

碼(鍵)

  • U是屬性全集,K是U的子集,若U完全函數依賴K,則稱K為候選碼,候選碼若有多個,任意選擇一個都可作為主碼,若U部分函數依賴K,則稱K為超碼。顯然,候選碼當然也是超碼,而且是最小的超碼。
  • 包含在任何一個候選碼的屬性都叫主屬性,其他都叫非主屬性
  • 在本書中主碼和候選碼統稱為,屬性集K不是該關係模式(表)的碼,而是另一個關係模式(表)的碼,則稱K為該關係模式(表)的外碼

求候選碼

例子:

舉例

有這樣一個配件管理表WPE(WNO,PNO,ENO,QNT),其中WNO表示倉庫號,PNO表示配件號,ENO表示職工號,QNT表示數量。

有以下約束要求:

(1)一個倉庫有多名職工;

(2)一個職工僅在一個倉庫工作;

(3)每個倉庫里一種型號的配件由專人負責,但一個人可以管理幾種配件;

(4)同一種型號的配件可以分放在幾個倉庫中。

分析表中的函數依賴關係,可以得到:

(1)ENO->WNO;

(2)(WNO,PNO)->QNT

(3)(WNO,PNO)->ENO

(4)(ENO,PNO)->QNT

觀察法?:

候選碼的定義就是一組能決定所有列(某一個元組)的屬性。

所以根據這4個函數依賴關係,(WNO,PNO)顯然肯定是,因為它可以決定QNT,也可以決定ENO,加上它本身,就是屬性全集U了。

而(ENO,PNO),雖然只有一個決定QNT,但是ENO可以單獨決定WNO,所以顯然(ENO,PNO)也就能一起決定QNT和WNO,因此也是候選碼。

六大範式?

第一範式

定義

滿足最基本的條件,每一個分量都是不可分的數據項。

  • 說人話,每一列對應只有一個值。

第二範式

定義

R屬於第一範式,且每一個非主屬性完全函數依賴於任何一個候選碼,則R屬於第二範式

  • 說人話,除了主碼候選碼之外的其他屬性都要完全函數依賴於主碼。
  • 因為任意一個候選碼都能作為主碼,所以,也就是說,如果存在某個屬性不是完全函數依賴於某一個候選碼,可能是部分函數依賴,那就沒了。
  • 比如主鍵是(學號,課程號),但是現在有一個屬性完全函數依賴於學號,而部分函數依賴於(學號,課程號),那就不滿足第二範式。

第三範式

定義

R屬於第二範式,若R中不存在碼X,屬性子集Y,非主屬性Z,使得X決定Y,Y不決定X,Y決定Z,則R屬於第三範式。

  • 說人話,非主屬性必須直接完全函數依賴於主鍵,中間不能有其他函數,即不能是傳遞函數依賴。

BC範式

定義

R屬於第一範式,若X決定Y,且Y不是X的子集時X必含有碼,即每一個決定因素都包含碼,則R屬於BC範式。

  • 說人話, 若R是第一範式,且每個屬性不部分函數依賴於候選碼也不傳遞函數依賴於候選碼,則R是BC範式,具體以下三點。
    • 所有非主屬性對每一個碼都是完全函數依賴。(也是第二範式要求)
    • 所有主屬性對每一個不包含它的碼也是完全函數依賴。(也就是排除了所有屬性對碼的部分依賴)
    • 沒有任何屬性完全函數依賴於非碼的任何一組屬性。(排除傳遞函數依賴)
  • 實際上,BC範式就是在第三範式的基礎上消除了主屬性的傳遞依賴

第四範式

多值依賴

  • 說人話,多值依賴就是一個表中多對多的關係,如果可以分成兩列,這兩列多對多,這就平凡的多值依賴,如果是分成三列,固定某一列的值,其他兩列多對多,這就是非平凡的多值依賴,第四範式要消除的就是非平凡的多值依賴。

  • 函數依賴是特殊的多值依賴,因為多對多其實也是一對多。

定義

R屬於第一範式,對應R的每一個非平凡多值依賴,X->->Y,X都含有碼,則R屬於第四範式。

  • 說人話,在滿足第三範式的基礎上,關係表中不能含有一個實體的兩個或多個相互獨立的多值因子。
  • 或者說,滿足第四範式即要求每個非平凡的多值依賴都含有碼,也就是實際上是函數依賴。

第五範式

定義

第五範式是指關係模式R依賴均由R候選碼所隱含。

這輩子應該不會用到的內容,就不管了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Spring中常見的設計模式——工廠模式

一、簡單工廠模式

  簡單工廠模式(Simple Factory Pattern)由一個工廠對象決定創建哪一種產品類的實例,簡單工廠模式適用於工廠類負責創建對象較少的情況,且客戶端只需要傳入工廠類的參數,對於如何創建對像不關心。

public  interface IBlog {
     // 寫隨筆
    public  void write();
}
public  class JavaBlog implements IBlog {
    @Override
    public  void write() {
        System.out.println( "寫java隨筆" );
    }
}
public  class WriteBlog {
     public  static  void main(String[] args) {
        IBlog blog = new JavaBlog();
        blog.write();
    }
}

  上述代碼中,父類 IBlog 指向子類JavaBlog 的引用,應用層需要依賴JavaBlog,如果增加PythonBlog等等更多的課程,客戶端就會越來越臃腫。因此要把依賴減弱,把創建細節隱藏。現在我們用簡單工廠優化:

public class BlogFactory {
    public IBlog create(Class<? extends IBlog> clazz) {
        if (null != clazz) {
            try {
                return clazz.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

客戶端改變:

  public static void main(String[] args) {
        BlogFactory blogFactory = new BlogFactory();
        IBlog blog = blogFactory.create(JavaBlog. class );
        blog.write();
    }

  簡單工廠模式在JDK中很常見,如Calender類(感興趣去看源碼),還有logback,LoggerFactory中有很多重載的方法getLogger()。但是簡單工廠也有缺點:工廠類的職責相對過重,不易於擴展過於複雜的產品結構。

二、工廠方法模式

   工廠方法模式(Factory Method Pattern)是指定義一個創建對象的接口,但讓實現這個接口的類來決定實例化哪個類,工廠方法模式讓類的實例化推遲到子類中進行。在工廠方法模式中,用戶只需要關心所需產品對應工廠,無須關心創建的細節,而且加入新產品時符合開閉原則。

  工廠方法模式主要解決產品擴展問題。在簡單工廠模式中,隨著產品的增多,如果不同語言書寫隨筆的邏輯各不相同,工廠職責越來越多,那工廠裏面就會亂搞一氣,狗屁不通。根據單一職責原則,我們將只能進行拆分,不同工廠做不同事,Java隨筆由Java工廠創建,Python隨筆由Python工廠創建,對工廠本身進行抽象。

先創建工廠類:

public  interface IBlogFactory {
    IBlog create();
}

再創建對應工廠:

public  class JavaBlogFactory implements IBlogFactory {
    @Override
    public IBlog create() {
        return new JavaBlog();
    }
}

public class PythonBlogFactory implements IBlogFactory {
    @Override
    public IBlog create() {
        return new PythonBlog();
    }
}

客戶端:

public class CreateBlog {
    public static void main(String[] args) {
        IBlogFactory factory = new PythonBlogFactory();
        IBlog blog = factory.create();
        blog.write();

        factory = new JavaBlogFactory();
        blog = factory.create();
        blog.write();
    }
}

總結來說就是:不同工廠抽像出一個工廠頭子,不同的工廠創建不同的實例。

工廠方法模式適用於以下場景:

1.創建對象需要大量重複代碼。

2.客戶端(應用層)不依賴於產品類實例如何被創建、如何被實現等細節。

3.一個類通過其子類來指定創建哪個對象。

缺點:

1.類的個數容易過多,增加複雜度。

2.增加了系統的抽象性和理解難度。

三、抽象工廠

  抽象工廠(Abstract Factory Pattern)提供一個黃健一系列相關或相互依賴對象的接口,無需指定具體類。客戶端(應用層)不依賴於產品類實例如何被創建、如何被實現等細節,強調的是一系列相關得產品對象(屬於同一產品族)一起使用創建對象需要大量重複代碼。需要提供一個產品類的庫,所有產品以同樣接口出現,從而是客戶端不依賴於具體實現。

產品族:同一家的不同產品,比如小米,華為,蘋果;

產品等級:不同種類的產品,比如手機,電視,電腦。

工廠要做的就是生產我們牌子的所有產品。以博客為例,java分類的博客有隨筆、文章、日記等。

首先創建文章和日記的抽象接口:

public  interface IDocument {
     void write();
}

public  interface INote {
     void make();
}

再創建抽象工廠:

public  interface BlogFactory {
    INote createNote();

    IDocument createDocument();
}

實現Java文章和日記:

public  class JavaDocument implements IDocument {
    @Override
    public  void write() {
        System.out.println( "寫Java文章" );
    }
}

public  class JavaNote implements INote {
    @Override
    public  void make() {
        System.out.println( "寫Java筆記" );
    }
}

實現Java產品族具體工廠:

public  class JavaBlogFactory implements BlogFactory {
    @Override
    public INote createNote() {
         return  new JavaNote();
    }

    @Override
    public IDocument createDocument() {
         return  new JavaDocument();
    }
}

實現Python文章和日記、實現Python具體工廠參考Java的。

客戶端調用:

public  class BlogTest {
     public  static  void main(String[] args) {
        JavaBlogFactory factory = new JavaBlogFactory();
        factory.createDocument().write();
        factory.createNote().make();
    }
}

  上述代碼描述了兩個產品族的工廠,如果想要擴展產品等級(就是再加點評啥的),要調整抽象工廠、具體工廠。由此可見抽象工廠模式的缺點:

1.規定所有可能被創建的產品集合,產品族(Java系列)中擴展新產品很困難,需要修改抽象工廠及實現;

2.增加系統抽象性和理解難度;

  我們可以利用工廠模式創建好數據源連接池並放到容器中,業務需要時再取出。就避免了用一次創建一次的尷尬。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?