部落格

  • 數組與鏈表

    數組與鏈表

    前言

    數組和鏈表是兩種數據結構,數組非常簡單易用但是它有兩個非常大的缺點,一個是數組一旦創建無法擴展,另一個則是數組的查找和刪除的速度很慢.

    鏈表改善了一些數組的缺點,但是同樣的鏈表自身也存在一些自己的缺點.

    本篇博客將為大家介紹一下這數組和鏈表特點及各自的優缺點.

    閱讀前的準備工作

    ,一種粗略的評價計算機算法效率的方法.後面的內容會用到表示效率的方法.

    1. 數組

    我們按數組中的數組是否排序對數組進行劃分,將數組分為無序數組和有序數組.無序數組中的數組是無序的,而有序數組中的數據則是升序或者降序排序的.

    1.1 無序數組

    因為無序數組中的數據是無序的,往數組中添加數據時不用進行比較和移動數據,所以往無序數組裡面添加數據很快.無論是添加第一個數據還是第一萬個數據所需的時間是相同的,效率為O(1).

    至於查找和刪除速度就沒有那麼快了,以數組中有一萬個數據項為例,最少需要比較1次,最多則需要比較一萬次,平均下來需要比較5000次,即N/2次比較,N代表數據量,大O表示法中常數可以忽略,所以效率為O(N).

    結論:

    1. 插入很快,因為總是將數據插入到數組的空餘位置.
    2. 查找和刪除很慢,假設數組的長度為N,那麼平均的查找/刪除的比較次數為N/2,並且還需要移動數據.

    1.2 有序數組

    無序數組中存放的數據是無序的,有序數組裡面存放的數據則是有序的(有可能是升序有可能是降序).

    因為有序數組中的數據是按升序/降序排列的,所以插入的時候需要進行排序並且移動數據項,所有有序數組的插入速度比無序數組慢. 效率為O(N).

    刪除速度和無序數組一樣慢 效率為O(N).

    有序數組的查找速度要比無序數組快,這是因為使用了一個叫做二分查找的算法.

    二分查找: 二分查找也稱折半查找(Binary Search),它是一種效率較高的查找方法。但是,折半查找要求線性表必須採用順序存儲結構,而且表中元素按關鍵字有序排列.

    有一個關於二分查找的形象類比 -> 猜數遊戲

    假設要在0-100之間猜一個數,那麼你第一個要猜的数字就是100的一半50的時候,你的朋友會告訴你這個数字比要猜的数字是大還是小,如果比数字大,你接下來要猜的数字就是50的一半25,你的朋友說比這個数字要大,那麼你下面要猜的数字就是25-50中間的那個數37,以此類推…

    使用二分查找可極大的提高查找的效率,假設一個有序數組有十億個數據,那麼查找到所需的数字,最多只需比較30次.

    有序數組使用二分查找的效率為O(logN).有序數組也可以通過二分查找來新增和刪除數據以提高效率,但是依然需要在新增/刪除后移動數據項,所以效率依然會有影響.

    總結:

    1. 有序數組的查找速度比無序數組高,效率為O(logN)
    2. 有序數組的刪除和新增速度很慢,效率為O(N)

    1.3 數組總結

    數組雖然簡單易用,但是數組有兩個致命的缺點:

    1. 數組存儲的數量有限,創建的過大浪費資源,創建的過小溢出
    2. 數組的效率比其他數據結構低
    • 無序數組插入效率為O(1)時間,但是查找花費O(N)時間
    • 有序數組查找花費O(logN)時間,插入花費O(N)時間
    • 刪除需要移動平均半數的數據項,所以刪除都是O(N)的時間

    2. 鏈表

    數組一經創建大小就固定住了,無法修改,鏈表在這方面做出了改善,只要內存夠用就可以無限制的擴大.

    鏈表是繼數組之後應用最廣泛的數據結構.

    2.1 鏈表的特點

    鏈表為什麼叫鏈表呢? 因為它保存數據的方式就像一條鎖鏈

    鏈表保存數據的方式很像上面的這一條鎖鏈,每一塊鎖鏈就是一個鏈節點,鏈節點保存着自己的數據同時通過自己的next()方法指向下一個鏈節點. 鏈表通過鏈節點不斷地調用next()方法就可以遍歷鏈表中的所有數據.

    在鏈表中,每個數據項都被包含在”鏈節點”(link)中,一個鏈結點是某個類的對象,這個類可以叫做Link.因為一個鏈表中有許多類似的鏈結點,所以有必要用一個不同於鏈表的類來表達鏈結點.

    每個Link對象中都包含一個對下一個鏈結點引用的字段(通常叫做next).

    鏈表本身的對象中有一個字段指向對第一個鏈結點的引用.

    數據與鏈表查找數據的區別: 在數組中查找數據就像在一個大倉庫裏面一樣,一號房間沒有,我們去二號房間,二號房間沒有我們去三號房間,以此類推.. 按照地址找完所有房間就可以了.

    而在鏈表中查找數據就像單線彙報的地下工作者,你是孤狼你想要彙報點情報給你的頂級上司毒蜂,但是你必須先報告給你的接頭人豬剛鬣,豬剛鬣在報告給它的單線接頭人土行孫,最後由土行孫報告給毒蜂.只能一個找一個,這樣最終完成任務.

    2.2 Java代碼

    鏈節點類:

    
    /**
     * @author liuboren
     * @Title: 鏈節點
     * @Description:
     * @date 2019/11/20 19:30
     */
    public class Link {
        //  保存的數據
        public int data;
    
        // 指向的下一個鏈節點
        public Link nextLink;
    
        public Link(int data) {
            this.data = data;
        }
    
        public int getData() {
            return data;
        }
    
        public void setData(int data) {
            this.data = data;
        }
    
        public Link getNextLink() {
            return nextLink;
        }
    
        public void setNextLink(Link nextLink) {
            this.nextLink = nextLink;
        }
    }
    

    鏈表類

    
    /**
     * @author liuboren
     * @Title: 鏈表類
     * @Description:
     * @date 2019/11/20 19:31
     */
    public class LinkList {
        private Link first;
    
        public LinkList() {
            first = null;
        }
    
        // 新增鏈節點方法
        public void insertFirst(int data) {
            Link link = new Link(data);
            link.setNextLink(first);
            first = link;
        }
    }
    

    在新增節點的時候,新增的link的next方法指向原來的first節點,並將鏈表類的first指向新增的節點.

    2.4 其他鏈表

    剛剛介紹的鏈表是單向鏈表,只能從后往前遍歷,其他的鏈表還有雙端鏈表、雙向鏈表、有序鏈表.

    再簡單介紹一下雙端鏈表吧.

    雙端鏈表就是在單向鏈表的基礎上,新增一個成員變量指向鏈表的最後一個對象.

    雙端鏈表代碼:

    /**
     * @author liuboren
     * @Title: 鏈表類
     * @Description:
     * @date 2019/11/20 19:31
     */
    public class LinkList {
        private Link first;
        private Link last;
    
        public LinkList() {
            first = null;
        }
    
        public boolean isEmpty() {
            return first == null;
        }
    
        // 新增鏈節點方法
        public void insertFirst(int data) {
            Link newLink = new Link(data);
            newLink.setNextLink(first);
            if (isEmpty()) {
                last = newLink;
            }
            first = newLink;
    
        }
    }
    

    雙向鏈表則是可以從first和last兩個方向進行遍歷,有序鏈表的數據都是按照關鍵字的順序排列的,本文不再展開了.

    2.5 鏈表的效率

    鏈表的效率:

    • 表頭插入和刪除速度都很快,花費O(1)的時間.
    • 平均起來,查找&刪除&插入在制定鏈節點後面都需要搜索一半的鏈節點需要O(N)次比較,雖然數組也需要O(N)次比較,但是鏈表讓然要快一些,因為不需要移動數據(只需要改變他們的引用)

    3. 總結

    鏈表解決了數組大小不能擴展的問題,但是鏈表自身依然存在一些問題(在鏈表的鏈節點後面查找&刪除&插入的效率不高),那麼有沒有一種數據結構即擁有二者的優點又改善了二者的缺點呢,答案是肯定的,下篇博客將為您介紹這種優秀的數據結構,敬請期待.

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

    【其他文章推薦】

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

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

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

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

    小三通物流營運型態?

    ※快速運回,大陸空運推薦?

  • 大話字符串逆序

    窗外的大廈,桌子上的水杯,手中的筆。

    面試官:“先來一點基礎的吧,用Java寫一個方法,入參是一個字符串,返回逆序后的字符串。”

    我暗想確實很基礎,於是便寫下:

    public static String reverse(String str) {
        StringBuffer sb = new StringBuffer(str);
        return sb.reverse().toString();
    }

    歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

    面試官看了看,說:“寫的很好,用StringBuffer的reverse方法。如果你來實現其中算法,你會怎麼寫?”

    我直接說:“從最後一個字符開始,一直向前添加字符就可以了。”重新寫了一個遍代碼:

    public static String reverse(String str) {
        char[] chars = str.toCharArray();
        StringBuilder sb = new StringBuilder();
        for (int i = chars.length - 1; i >= 0; i--) {
            sb.append(chars[i]);
        }
        return sb.toString();
    }

    歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

    面試官看了看,說:“寫的很好,逆序的功能完成了。不過再想想,有什麼可以優化的地方?”

    我想了想,說:“好像沒有什麼可以優化的?”

    面試官提示了一句:“比如,採用首尾替換的方式呢?是不是可以減少時間複雜度?”

    我恍然大悟,說:“的確是,我再改一下。”又重新寫了一個遍代碼:

    public static String reverse(String str) {
        char[] chars = str.toCharArray();
        int n = chars.length - 1;
        for (int i = 0; i <= n / 2; i++) {
            int j = n - i;
            char temp = chars[i];
            chars[i] = chars[j];
            chars[j] = temp;
        }
        return new String(chars);
    }

    歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

    面試官又看了看,說:“寫的很好,就是這個思想。不過再想想,有什麼可以優化的地方?”

    我左思右想一番,說:“應該沒有吧。”

    面試官說:“確定沒有了嘛?”

    我肯定地回答:“確定沒有了。”

    面試官:“好吧,這個問題先到這。”

    我有點不服氣,搶着問到:“您說說,還有什麼可以優化的地方?”

    面試官微笑了一下,說:“我認為還有兩個地方可以優化。”

    “第一,for循環的布爾表達式里不應該放除2的計算,否則每次循環都會計算一次。”

    “第二,除2的計算可以用右移一位代替,這樣效率更高。”

    面試官在我寫的代碼上改了幾筆,就變成了:

    public static String reverse(String str) {
        char[] chars = str.toCharArray();
        int n = chars.length - 1;
        for (int i = (n - 1) >> 1; i >= 0; i--) {
            int j = n - i;
            char temp = chars[i];
            chars[i] = chars[j];
            chars[j] = temp;
        }
        return new String(chars);
    }

    歡迎關注微信公眾號:萬貓學社,每周一分享Java技術乾貨。

    我茅塞頓開,這次面試真的是學到了。

    本故事純屬虛構,如有雷同實屬巧合。

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

    【其他文章推薦】

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

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

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

    台灣寄大陸海運貨物規則及重量限制?

    大陸寄台灣海運費用試算一覽表

  • Nebula 架構剖析系列(二)圖數據庫的查詢引擎設計

    Nebula 架構剖析系列(二)圖數據庫的查詢引擎設計

    摘要

    上文(存儲篇)說到數據庫重要的兩部分為存儲和計算,本篇內容為你解讀圖數據庫 Nebula 在查詢引擎 Query Engine 方面的設計實踐。

    在 Nebula 中,Query Engine 是用來處理 Nebula 查詢語言語句(nGQL)。本篇文章將帶你了解 Nebula Query Engine 的架構。

    上圖為查詢引擎的架構圖,如果你對 SQL 的執行引擎比較熟悉,那麼對上圖一定不會陌生。Nebula 的 Query Engine 架構圖和現代 SQL 的執行引擎類似,只是在查詢語言解析器和具體的執行計劃有所區別。

    Session Manager

    Nebula 權限管理採用基於角色的權限控制(Role Based Access Control)。客戶端第一次連接到 Query Engine 時需作認證,當認證成功之後 Query Engine 會創建一個新 session,並將該 session ID 返回給客戶端。所有的 session 統一由 Session Manger 管理。session 會記錄當前 graph space 信息及對該 space 的權限。此外,session 還會記錄一些會話相關的配置信息,並臨時保存同一 session 內的跨多個請求的一些信息。

    客戶端連接結束之後 session 會關閉,或者如果長時間沒通信會切為空閑狀態。這個空閑時長是可以配置的。
    客戶端的每個請求都必須帶上此 session ID,否則 Query Engine 會拒絕此請求。

    Storage Engine 不管理 session,Query Engine 在訪問存儲引擎時,會帶上 session 信息。

    Parser

    Query Engine 解析來自客戶端的 nGQL 語句,分析器(parser)主要基於著名的 flex / bison 工具集。字典文件(lexicon)和語法規則(grammar)在 Nebula 源代碼的 src/parser  目錄下。設計上,nGQL 的語法非常接近 SQL,目的是降低學習成本。 圖數據庫目前沒有統一的查詢語言國際標準,一旦 ISO/IEC 的圖查詢語言(GQL)委員會發布 GQL 國際標準,nGQL 會儘快去實現兼容。
    Parser 構建產出的抽象語法樹(Abstrac Syntax Tree,簡稱 AST)會交給下一模塊:Execution Planner。

    Execution Planner

    執行計劃器(Execution Planner)負責將抽象樹 AST 解析成一系列執行動作 action(可執行計劃)。action 為最小可執行單元。例如,典型的 action 可以是獲取某個節點的所有鄰節點,或者獲得某條邊的屬性,或基於特定過濾條件篩選節點或邊。當抽象樹 AST 被轉換成執行計劃時,所有 ID 信息會被抽取出來以便執行計劃的復用。這些 ID 信息會放置在當前請求 context 中,context 也會保存變量和中間結果。

    Optimization

    經由 Execution Planner 產生的執行計劃會交給執行優化框架 Optimization,優化框架中註冊有多個 Optimizer。Optimizer 會依次被調用對執行計劃進行優化,這樣每個 Optimizer都有機會修改(優化)執行計劃。最後,優化過的執行計劃可能和原始執行計劃完全不一樣,但是優化后的執行結果必須和原始執行計劃的結果一樣的。

    Execution

    Query Engine 最後一步是去執行優化后的執行計劃,這步是執行框架(Execution Framework)完成的。執行層的每個執行器一次只處理一個執行計劃,計劃中的 action 會挨個一一執行。執行器也會一些有針對性的局部優化,比如:決定是否併發執行。針對不同的 action所需數據和信息,執行器需要經由 meta service 與storage engine的客戶端與他們通信。

    最後,如果你想嘗試編譯一下 Nebula 源代碼可參考如下方式:

    有問題請在 GitHub(GitHub 地址:) 或者微信公眾號上留言,也可以添加 Nebula 小助手微信號:NebulaGraphbot 為好友反饋問題~

    推薦閱讀

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

    【其他文章推薦】

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

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

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

    大陸寄台灣空運注意事項

    大陸海運台灣交貨時間多久?

  • TCP time_wait close_wait問題(可能是全網最清楚的例子)

    TCP time_wait close_wait問題(可能是全網最清楚的例子)

    背景

    公司群里,運維發現一個問題,task服務報錯(如下)

    The stream or file \"/data/logs/adn_task/offer_service.log\" could not be opened:
    failed to open stream: Too many open files

    測試老大看到了,根據經驗就推測是應該是文件句柄使用完了,應該有TCP連接很多沒釋放,果真發現是很多CLOSE_WAIT的狀態

    簡單認知

    短鏈接,一次鏈接就會佔用一個端口,一個端口就是一個文件描述符;
    文件描述符 又稱 句柄,linux系統最大的句柄數是65535,可以通過ulimit -a 查看

    三次握手

    TCP建立連接需要經過三次握手;
    通俗版本:
    A: 你好,你能聽見我說話嗎?
    B: 能聽到,你能聽到我說話嗎?
    A:我也能聽到,我們開始通信吧

    專業版本:
    建立TCP連接時,需要客戶端和服務器共發送3個包。

    • 第一次:客戶端發送初始序號x和syn=1請求標誌
    • 第二次:服務器發送請求標誌syn,發送確認標誌ACK,發送自己的序號seq=y,發送客戶端的確認序號ack=x+1
    • 第三次:客戶端發送ACK確認號,發送自己的序號seq=x+1,發送對方的確認號ack=y+1

    四次揮手

    TCP連接斷開需要經過四次揮手;
    通俗版本:
    前提A和B在通話
    A:好的,我的話就說完了(FIN);
    B:哦哦,我知道你說完啦(ACK),我還有說兩句哈;A: (沒說話,一直聽着)
    B:哦了,我也說完了(FIN)
    A:好的,我也知道你說玩了(ACK),掛電話吧

    專業版本:

    • 第一次揮手:客戶端發出釋放FIN=1,自己序列號seq=u,進入FIN-WAIT-1狀態
    • 第二次揮手:服務器收到客戶端的后,發出ACK=1確認標誌和客戶端的確認號ack=u+1,自己的序列號seq=v,進入CLOSE-WAIT狀態
    • 第三次揮手:客戶端收到服務器確認結果后,進入FIN-WAIT-2狀態。此時服務器發送釋放FIN=1信號,確認標誌ACK=1,確認序號ack=u+1,自己序號seq=w,服務器進入LAST-ACK(最後確認態)
    • 第四次揮手:客戶端收到回復后,發送確認ACK=1,ack=w+1,自己的seq=u+1,客戶端進入TIME-WAIT(時間等待)。客戶端經過2個最長報文段壽命后,客戶端CLOSE;服務器收到確認后,立刻進入CLOSE狀態。

    狀態流轉圖

    實際例子

    建立連接

    linux上起了一個redis服務

    本地起的6379端口

    還是同一台機器上,通過python腳本連接該redis服務:

    此時網絡連接如下:

    關注這兩個網絡連接,第一個是redis-server的,第二是python腳本的,此時都是ESTABLISHED狀態,表示這兩個進程建立了連接

    TIME_WAIT情況

    現在斷掉python

    之前的python的那個連接,是TIME_WAIT狀態
    客戶端(主動方)主動斷開,進入TIME_WAIT狀態,服務端(被動方)進去CLOSE狀態,就是沒有显示了

    等待2MSL(1分鐘)后,如下:

    TIME_WAIT狀態的連接也消失了,TIME_WAIT回收機制,系統ing過一段時間會回收,資源重利用

    CLOSE_WAIT情況

    先建立連接,如下:

    關掉redis服務,service redis stop

    之前的redis-server的45370端口連接 進入了FIN_WAIT2狀態,而python端(被動關閉方)就進去了CLOSE_WAIT狀態

    等待30s后,在看連接

    只有python的那條CLOSE_WAIT

    再次操作python端的腳本,再次get

    關於6379端口(redis端口)的網絡連接都沒有了

    TCP參數設置

    如何快速回收TIME_WAIT和FIN_WAIT
    /etc/sysctl.conf 包含以下配置項
    net.ipv4.tcp_timestamps = 1
    net.ipv4.tcp_tw_recycle = 1
    net.ipv4.tcp_tw_reuse = 1
    net.ipv4.tcp_fin_timeout = 30
    root權限 執行/sbin/sysctl -p使之生效

    經驗之談

    個人經驗,不一定對,如有錯誤,請指正

    1. 當出現了CLOSE_WAIT大概率是業務代碼問題,代碼中沒有處理服務異常的情況,如上面的例子,python再次請求redis的時候,發現redis掛了,就會主動幹掉CLOSE_WAIT狀態
    2. 出現大量TIME_WAIT的情況,一般是服務端沒有及時回收端口,linux內核參數需要調整優化

    參考資料

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

    【其他文章推薦】

    ※專營大陸空運台灣貨物推薦

    台灣空運大陸一條龍服務

  • 用雲開發快速製作客戶業務需求收集小程序丨實戰

    用雲開發快速製作客戶業務需求收集小程序丨實戰

    一、導語

    ​ 如何省去企業上門(現場)搜集客戶需求的環節,節約企業人力和時間成本,將客戶的業務定製需求直接上傳至雲數據庫?雲開發為我們提供了這個便利!

    二、需求背景

    ​ 作為一名XX公司IT萌萌新,這段時間對小程序開發一直有非常濃厚的興趣,並且感慨於“雲開發·不止於快”的境界。近期工作中,剛好碰見業務部門的一個需求,目的是節約上門跟客戶收集業務定製資料的時間,以往是每變更一次,就需要上門一次,碰見地域較遠的,費時費力,且往往要求幾天內完成上線,時間非常緊迫。因此,結合一直以來對雲開發的各種優勢的了解,我說服公司領導通過小程序·雲開發來實現。

    下面是其中一項業務定製界面的展示:
    (1)業務對業務流程有簡單說明;
    (2)相關業務介紹;
    (3)不同客戶輸入個性化需求;
    (4)雲存儲後台實現需求表單的收集。

    ​ 得力於雲開發提供的API和WeUI庫的便利,本項目在我在極短的時間內就實現了比較理想的效果 。接下來,我就從本項目入手,講講我是如何依靠小程序·雲開發將想法快速實現的,其實我也是剛入門沒多久,只是想分享一下自身在學習小程序開發項目中的一些知識點和體會,代碼可能略為粗糙,邏輯也有待優化,歡迎大家在評論區多多交流。

    三、開發過程

    1、組件

    主要使用了官方WeUI擴展能力的一些組件庫來實現主要功能。

    核心的WeUI庫主要有 Msg、Picker、圖片的Upload等(以快為目的,節省自己寫CSS樣式的時間,也方便0基礎的同學上手,這裏又體會到了小程序開發的便捷)。

    2、實現代碼

    本次雲開發包括雲數據庫雲存儲兩大功能:

    (1)雲數據庫

    雲數據庫的主要就是搜集客戶提交上來的表單信息,包括客戶的聯繫方式和選擇的業務類型等,並存儲在雲數據庫中,方便業務經理搜集需求。

    我們來看簡單的實現過程:

    首先是表單,用到了 form 表單組件以及它的 bindsubmit 方法,在 wxml 中放置 form 表單:

    <form bindsubmit="formSubmit">
        <view class="form">
          <view class="section">
            <picker bindchange="bindPickerGsd" mode="selector" value="{{indexGsd}}" range="{{arrayGsd}}">
              <view class="picker">歸屬縣市</view>
              <view class="picker-content" >{{arrayGsd[indexGsd]?arrayGsd[indexGsd]:"(必填項) 請下拉選擇歸屬地"}}</view> 
            </picker>
          </view>    
          <!---中間部分詳見代碼--->
        </view>
    
        <view class="footer">
          <button class="dz-btn" formType="submit" loading="{{formStatus.submitting}}" disabled="{{formStatus.submitting}}" bindtap="openSuccess">提交</button>
        </view>
      </form>

    表單中除了普通的文本輸入,增加有下拉列表的實現(畢竟客戶有時候是比較懶的)。

    來看一下具體代碼:

    bindPickerGsd: function (e) {    
      console.log('歸屬地已選擇,攜帶值為', e.detail.value)
      console.log('歸屬地選擇:', this.data.arrayGsd[e.detail.value])    
      this.setData({
         indexGsd: e.detail.value     
       })   
       this.data.formData.home_county = this.data.arrayGsd[e.detail.value]
    },

    最後表單上傳到雲數據庫:

      // 表單提交
      formSubmit: function (e) {
        var minlength = '';
        var maxlength = '';
        console.log("表單內容",e)
        var that = this;
        var formData = e.detail.value;
        var result = this.wxValidate.formCheckAll(formData);
        
        console.log("表單提交formData", formData);
        console.log("表單提交result", result)
        wx.showLoading({
          title: '發布中...',
        })
        const db = wx.cloud.database()
        db.collection('groupdata').add({
          data: {
            time: getApp().getNowFormatDate(),
            home_county: this.data.formData.home_county,
            group_name: formData.group_name,
            contact_name: formData.contact_name,
            msisdn: formData.msisdn,
            product_name: this.data.formData.product_name,
            word: formData.word,
          },
          success: res => {
            wx.hideLoading()
            console.log('發布成功', res)
    
          },
          fail: err => {
            wx.hideLoading()
            wx.showToast({
              icon: 'none',
              title: '網絡不給力....'
            })
            console.error('發布失敗', err)
          }
        })
      },
    (2)雲存儲

    ​ 因為業務的定製需要填單客戶所在單位的授權證明材料,因此需要提單人(使用人)上傳證明文件,因此增加了使用雲存儲的功能。

    核心代碼:

        promiseArr.push(new Promise((reslove,reject)=>{
            wx.cloud.uploadFile({
                cloudPath: "groupdata/" + group_name + "/" + app.getNowFormatDate() +suffix,
                filePath:filePath
            }).then(res=>{
                console.log("授權文件上傳成功")          
                })
                reslove()
                }).catch(err=>{
                console.log("授權文件上傳失敗",err)
        })
    
        因為涉及到不同頁面的數據傳遞,即將表單頁面的group_name作為雲存儲的文件夾用於存儲該客戶在表單中上傳的圖片,因此還需要用到getCurrentPages()來進行頁面間的數據傳遞 
    
        var pages = getCurrentPages();
        var prePage = pages[pages.length - 2];//pages.length就是該集合長度 -2就是上一個活動的頁面,也即是跳過來的頁面
        var group_name = prePage.data.formData.group_name.value//取上頁data里的group_name數據用於標識授權文件所存儲文件夾的名稱

    3、待進一步優化

    ​ 基於時間關係,本次版本僅對需求進行了簡單實現,作為公司一個可靠的項目,還需要關注”客戶隱私”、“數據安全”,以及更人性化的服務。比如:

    (1)提單人確認和認證過程

    可靠性:增加驗證碼驗證(防止他人冒名登記),以及公司受理業務有個客戶本人提交憑證。

    (2)訂閱消息

    受理成功后,可以給客戶進行處理結果的反饋,增強感知。

    (3)人工客服

    進行在線諮詢等。

    四、總結

    ​ 在本次項目開發中,我深刻體會到了雲開發的“快”,特別是雲數據庫的增刪查改功能非常方便。雲開發提供的種種便利,讓我在有新創意的時候,可以迅速採用小程序雲開發快速實現,省時省力,還能免費使用騰訊雲服務器,推薦大家嘗試!

    源碼地址

    如果你想要了解更多關於雲開發CloudBase相關的技術故事/技術實戰經驗,請掃碼關注【騰訊云云開發】公眾號~

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

    【其他文章推薦】

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

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

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

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

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

    ※試算大陸海運運費!

  • SpringBoot 源碼解析 (六)—– Spring Boot的核心能力 – 內置Servlet容器源碼分析(Tomcat)

    SpringBoot 源碼解析 (六)—– Spring Boot的核心能力 – 內置Servlet容器源碼分析(Tomcat)

    Spring Boot默認使用Tomcat作為嵌入式的Servlet容器,只要引入了spring-boot-start-web依賴,則默認是用Tomcat作為Servlet容器:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    Servlet容器的使用

    默認servlet容器

    我們看看spring-boot-starter-web這個starter中有什麼

    核心就是引入了tomcat和SpringMvc,我們先來看tomcat

    Spring Boot默認支持Tomcat,Jetty,和Undertow作為底層容器。如圖:

    而Spring Boot默認使用Tomcat,一旦引入spring-boot-starter-web模塊,就默認使用Tomcat容器。

    切換servlet容器

    那如果我么想切換其他Servlet容器呢,只需如下兩步:

    • 將tomcat依賴移除掉
    • 引入其他Servlet容器依賴

    引入jetty:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <!--移除spring-boot-starter-web中的tomcat-->
                <artifactId>spring-boot-starter-tomcat</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <!--引入jetty-->
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>

    Servlet容器自動配置原理

    EmbeddedServletContainerAutoConfiguration

    其中
    EmbeddedServletContainerAutoConfiguration是嵌入式Servlet容器的自動配置類,該類在
    spring-boot-autoconfigure.jar中的web模塊可以找到。

    我們可以看到EmbeddedServletContainerAutoConfiguration被配置在spring.factories中,看過我前面文章的朋友應該知道SpringBoot自動配置的原理,這裏將EmbeddedServletContainerAutoConfiguration配置類加入到IOC容器中,接着我們來具體看看這個配置類:

    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
    @Configuration
    @ConditionalOnWebApplication// 在Web環境下才會起作用
    @Import(BeanPostProcessorsRegistrar.class)// 會Import一個內部類BeanPostProcessorsRegistrar
    public class EmbeddedServletContainerAutoConfiguration {
    
        @Configuration
        // Tomcat類和Servlet類必須在classloader中存在 // 文章開頭我們已經導入了web的starter,其中包含tomcat和SpringMvc // 那麼classPath下會存在Tomcat.class和Servlet.class
        @ConditionalOnClass({ Servlet.class, Tomcat.class }) // 當前Spring容器中不存在EmbeddedServletContainerFactory類型的實例
        @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT) public static class EmbeddedTomcat {
    
            @Bean
            public TomcatEmbeddedServletContainerFactory tomcatEmbeddedServletContainerFactory() {
                // 上述條件註解成立的話就會構造TomcatEmbeddedServletContainerFactory這個EmbeddedServletContainerFactory
                return new TomcatEmbeddedServletContainerFactory();
            }
        }
        
        @Configuration
        @ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
                WebAppContext.class })
        @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
        public static class EmbeddedJetty {
    
            @Bean
            public JettyEmbeddedServletContainerFactory jettyEmbeddedServletContainerFactory() {
                return new JettyEmbeddedServletContainerFactory();
            }
    
        }
        
        @Configuration
        @ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
        @ConditionalOnMissingBean(value = EmbeddedServletContainerFactory.class, search = SearchStrategy.CURRENT)
        public static class EmbeddedUndertow {
    
            @Bean
            public UndertowEmbeddedServletContainerFactory undertowEmbeddedServletContainerFactory() {
                return new UndertowEmbeddedServletContainerFactory();
            }
    
        }
        
        //other code...
    }

    在這個自動配置類中配置了三個容器工廠的Bean,分別是:

    • TomcatEmbeddedServletContainerFactory

    • JettyEmbeddedServletContainerFactory

    • UndertowEmbeddedServletContainerFactory

    這裏以大家熟悉的Tomcat為例,首先Spring Boot會判斷當前環境中是否引入了Servlet和Tomcat依賴,並且當前容器中沒有自定義的
    EmbeddedServletContainerFactory的情況下,則創建Tomcat容器工廠。其他Servlet容器工廠也是同樣的道理。

    EmbeddedServletContainerFactory

    • 嵌入式Servlet容器工廠
    public interface EmbeddedServletContainerFactory {
    
        EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers);
    }

    內部只有一個方法,用於獲取嵌入式的Servlet容器。

    該工廠接口主要有三個實現類,分別對應三種嵌入式Servlet容器的工廠類,如圖所示:

    TomcatEmbeddedServletContainerFactory

    以Tomcat容器工廠TomcatEmbeddedServletContainerFactory類為例:

    public class TomcatEmbeddedServletContainerFactory extends AbstractEmbeddedServletContainerFactory implements ResourceLoaderAware {
        
        //other code...
        
        @Override
        public EmbeddedServletContainer getEmbeddedServletContainer( ServletContextInitializer... initializers) {
            //創建一個Tomcat
            Tomcat tomcat = new Tomcat(); //配置Tomcat的基本環節
            File baseDir = (this.baseDirectory != null ? this.baseDirectory: createTempDir("tomcat"));
            tomcat.setBaseDir(baseDir.getAbsolutePath());
            Connector connector = new Connector(this.protocol);
           tomcat.getService().addConnector(connector);
            customizeConnector(connector);
          tomcat.setConnector(connector); tomcat.getHost().setAutoDeploy(false);
            configureEngine(tomcat.getEngine());
            for (Connector additionalConnector : this.additionalTomcatConnectors) {
                tomcat.getService().addConnector(additionalConnector);
            }
            prepareContext(tomcat.getHost(), initializers);
            
            //包裝tomcat對象,返回一個嵌入式Tomcat容器,內部會啟動該tomcat容器
            return getTomcatEmbeddedServletContainer(tomcat);
        }
    }

    首先會創建一個Tomcat的對象,並設置一些屬性配置,最後調用getTomcatEmbeddedServletContainer(tomcat)方法,內部會啟動tomcat,我們來看看:

    protected TomcatEmbeddedServletContainer getTomcatEmbeddedServletContainer(
        Tomcat tomcat) {
        return new TomcatEmbeddedServletContainer(tomcat, getPort() >= 0);
    }

    該函數很簡單,就是來創建Tomcat容器並返回。看看TomcatEmbeddedServletContainer類:

    public class TomcatEmbeddedServletContainer implements EmbeddedServletContainer {
    
        public TomcatEmbeddedServletContainer(Tomcat tomcat, boolean autoStart) {
            Assert.notNull(tomcat, "Tomcat Server must not be null");
            this.tomcat = tomcat;
            this.autoStart = autoStart;
            
            //初始化嵌入式Tomcat容器,並啟動Tomcat
     initialize();
        }
        
        private void initialize() throws EmbeddedServletContainerException {
            TomcatEmbeddedServletContainer.logger
                    .info("Tomcat initialized with port(s): " + getPortsDescription(false));
            synchronized (this.monitor) {
                try {
                    addInstanceIdToEngineName();
                    try {
                        final Context context = findContext();
                        context.addLifecycleListener(new LifecycleListener() {
    
                            @Override
                            public void lifecycleEvent(LifecycleEvent event) {
                                if (context.equals(event.getSource())
                                        && Lifecycle.START_EVENT.equals(event.getType())) {
                                    // Remove service connectors so that protocol
                                    // binding doesn't happen when the service is
                                    // started.
                                    removeServiceConnectors();
                                }
                            }
    
                        });
    
                        // Start the server to trigger initialization listeners
                        //啟動tomcat
                        this.tomcat.start(); // We can re-throw failure exception directly in the main thread
                        rethrowDeferredStartupExceptions();
    
                        try {
                            ContextBindings.bindClassLoader(context, getNamingToken(context),
                                    getClass().getClassLoader());
                        }
                        catch (NamingException ex) {
                            // Naming is not enabled. Continue
                        }
    
                        // Unlike Jetty, all Tomcat threads are daemon threads. We create a
                        // blocking non-daemon to stop immediate shutdown
                        startDaemonAwaitThread();
                    }
                    catch (Exception ex) {
                        containerCounter.decrementAndGet();
                        throw ex;
                    }
                }
                catch (Exception ex) {
                    stopSilently();
                    throw new EmbeddedServletContainerException(
                            "Unable to start embedded Tomcat", ex);
                }
            }
        }
    }

    到這裏就啟動了嵌入式的Servlet容器,其他容器類似。

    Servlet容器啟動原理

    SpringBoot啟動過程

    我們回顧一下前面講解的SpringBoot啟動過程,也就是run方法:

    public ConfigurableApplicationContext run(String... args) {
        // 計時工具
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
    
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    
        configureHeadlessProperty();
    
        // 第一步:獲取並啟動監聽器
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting();
        
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    
            // 第二步:根據SpringApplicationRunListeners以及參數來準備環境
            ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
            configureIgnoreBeanInfo(environment);
    
            // 準備Banner打印器 - 就是啟動Spring Boot的時候打印在console上的ASCII藝術字體
            Banner printedBanner = printBanner(environment);
    
            // 第三步:創建Spring容器
            context = createApplicationContext();
    
            exceptionReporters = getSpringFactoriesInstances(
                    SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
    
            // 第四步:Spring容器前置處理
            prepareContext(context, environment, listeners, applicationArguments,printedBanner);
    
            // 第五步:刷新容器
     refreshContext(context);
    
         // 第六步:Spring容器後置處理
            afterRefresh(context, applicationArguments);
    
          // 第七步:發出結束執行的事件
            listeners.started(context);
            // 第八步:執行Runners
            this.callRunners(context, applicationArguments);
            stopWatch.stop();
            // 返回容器
            return context;
        }
        catch (Throwable ex) {
            handleRunFailure(context, listeners, exceptionReporters, ex);
            throw new IllegalStateException(ex);
        }
    }

    我們回顧一下第三步:創建Spring容器

    public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
                + "annotation.AnnotationConfigApplicationContext";
    
    public static final String DEFAULT_WEB_CONTEXT_CLASS = "org.springframework."
                + "boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext";
    
    protected ConfigurableApplicationContext createApplicationContext() {
        Class<?> contextClass = this.applicationContextClass;
        if (contextClass == null) {
            try {
                //根據應用環境,創建不同的IOC容器
                contextClass = Class.forName(this.webEnvironment ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
            }
        }
        return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
    }

    創建IOC容器,如果是web應用,則創建
    AnnotationConfigEmbeddedWebApplicationContext的IOC容器;如果不是,則創建AnnotationConfigApplicationContext的IOC容器;很明顯我們創建的容器是AnnotationConfigEmbeddedWebApplicationContext
    接着我們來看看
    第五步,刷新容器
    refreshContext(context);

    private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
    }
    
    protected void refresh(ApplicationContext applicationContext) {
        Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
        //調用容器的refresh()方法刷新容器
     ((AbstractApplicationContext) applicationContext).refresh();
    }

    容器刷新過程

    調用抽象父類AbstractApplicationContext的refresh()方法;

    AbstractApplicationContext

     1 public void refresh() throws BeansException, IllegalStateException {
     2     synchronized (this.startupShutdownMonitor) {
     3         /**
     4          * 刷新上下文環境
     5          */
     6         prepareRefresh();
     7 
     8         /**
     9          * 初始化BeanFactory,解析XML,相當於之前的XmlBeanFactory的操作,
    10          */
    11         ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
    12 
    13         /**
    14          * 為上下文準備BeanFactory,即對BeanFactory的各種功能進行填充,如常用的註解@Autowired @Qualifier等
    15          * 添加ApplicationContextAwareProcessor處理器
    16          * 在依賴注入忽略實現*Aware的接口,如EnvironmentAware、ApplicationEventPublisherAware等
    17          * 註冊依賴,如一個bean的屬性中含有ApplicationEventPublisher(beanFactory),則會將beanFactory的實例注入進去
    18          */
    19         prepareBeanFactory(beanFactory);
    20 
    21         try {
    22             /**
    23              * 提供子類覆蓋的額外處理,即子類處理自定義的BeanFactoryPostProcess
    24              */
    25             postProcessBeanFactory(beanFactory);
    26 
    27             /**
    28              * 激活各種BeanFactory處理器,包括BeanDefinitionRegistryBeanFactoryPostProcessor和普通的BeanFactoryPostProcessor
    29              * 執行對應的postProcessBeanDefinitionRegistry方法 和  postProcessBeanFactory方法
    30              */
    31             invokeBeanFactoryPostProcessors(beanFactory);
    32 
    33             /**
    34              * 註冊攔截Bean創建的Bean處理器,即註冊BeanPostProcessor,不是BeanFactoryPostProcessor,注意兩者的區別
    35              * 注意,這裏僅僅是註冊,並不會執行對應的方法,將在bean的實例化時執行對應的方法
    36              */
    37             registerBeanPostProcessors(beanFactory);
    38 
    39             /**
    40              * 初始化上下文中的資源文件,如國際化文件的處理等
    41              */
    42             initMessageSource();
    43 
    44             /**
    45              * 初始化上下文事件廣播器,並放入applicatioEventMulticaster,如ApplicationEventPublisher
    46              */
    47             initApplicationEventMulticaster();
    48 
    49             /**
    50  * 給子類擴展初始化其他Bean 51              */
    52  onRefresh(); 53 
    54             /**
    55              * 在所有bean中查找listener bean,然後註冊到廣播器中
    56              */
    57             registerListeners();
    58 
    59             /**
    60              * 設置轉換器
    61              * 註冊一個默認的屬性值解析器
    62              * 凍結所有的bean定義,說明註冊的bean定義將不能被修改或進一步的處理
    63              * 初始化剩餘的非惰性的bean,即初始化非延遲加載的bean
    64              */
    65             finishBeanFactoryInitialization(beanFactory);
    66 
    67             /**
    68              * 通過spring的事件發布機制發布ContextRefreshedEvent事件,以保證對應的監聽器做進一步的處理
    69              * 即對那種在spring啟動后需要處理的一些類,這些類實現了ApplicationListener<ContextRefreshedEvent>,
    70              * 這裏就是要觸發這些類的執行(執行onApplicationEvent方法)
    71              * spring的內置Event有ContextClosedEvent、ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、RequestHandleEvent
    72              * 完成初始化,通知生命周期處理器lifeCycleProcessor刷新過程,同時發出ContextRefreshEvent通知其他人
    73              */
    74             finishRefresh();
    75         }
    76 
    77         finally {
    78     
    79             resetCommonCaches();
    80         }
    81     }
    82 }

    我們看第52行的方法:

    protected void onRefresh() throws BeansException {
    
    }

    很明顯抽象父類AbstractApplicationContext中的onRefresh是一個空方法,並且使用protected修飾,也就是其子類可以重寫onRefresh方法,那我們看看其子類AnnotationConfigEmbeddedWebApplicationContext中的onRefresh方法是如何重寫的,AnnotationConfigEmbeddedWebApplicationContext又繼承EmbeddedWebApplicationContext,如下:

    public class AnnotationConfigEmbeddedWebApplicationContext extends EmbeddedWebApplicationContext {

    那我們看看其父類EmbeddedWebApplicationContext 是如何重寫onRefresh方法的:

    EmbeddedWebApplicationContext

    @Override
    protected void onRefresh() {
        super.onRefresh();
        try {
            //核心方法:會獲取嵌入式的Servlet容器工廠,並通過工廠來獲取Servlet容器
     createEmbeddedServletContainer();
        }
        catch (Throwable ex) {
            throw new ApplicationContextException("Unable to start embedded container", ex);
        }
    }

    在createEmbeddedServletContainer方法中會獲取嵌入式的Servlet容器工廠,並通過工廠來獲取Servlet容器:

     1 private void createEmbeddedServletContainer() {
     2     EmbeddedServletContainer localContainer = this.embeddedServletContainer;
     3     ServletContext localServletContext = getServletContext();
     4     if (localContainer == null && localServletContext == null) {
     5         //先獲取嵌入式Servlet容器工廠
     6         EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory();  7         //根據容器工廠來獲取對應的嵌入式Servlet容器
     8         this.embeddedServletContainer = containerFactory.getEmbeddedServletContainer(getSelfInitializer());  9     }
    10     else if (localServletContext != null) {
    11         try {
    12             getSelfInitializer().onStartup(localServletContext);
    13         }
    14         catch (ServletException ex) {
    15             throw new ApplicationContextException("Cannot initialize servlet context",ex);
    16         }
    17     }
    18     initPropertySources();
    19 }

    關鍵代碼在第6和第8行,先獲取Servlet容器工廠,然後根據容器工廠來獲取對應的嵌入式Servlet容器

    獲取Servlet容器工廠

    protected EmbeddedServletContainerFactory getEmbeddedServletContainerFactory() {
        //從Spring的IOC容器中獲取EmbeddedServletContainerFactory.class類型的Bean
        String[] beanNames = getBeanFactory().getBeanNamesForType(EmbeddedServletContainerFactory.class); //調用getBean實例化EmbeddedServletContainerFactory.class
        return getBeanFactory().getBean(beanNames[0], EmbeddedServletContainerFactory.class);
    }

    我們看到先從Spring的IOC容器中獲取EmbeddedServletContainerFactory.class類型的Bean,然後調用getBean實例化EmbeddedServletContainerFactory.class,大家還記得我們第一節Servlet容器自動配置類EmbeddedServletContainerAutoConfiguration中注入Spring容器的對象是什麼嗎?當我們引入spring-boot-starter-web這個啟動器后,會注入TomcatEmbeddedServletContainerFactory這個對象到Spring容器中,所以這裏獲取到的Servlet容器工廠是TomcatEmbeddedServletContainerFactory,然後調用

    TomcatEmbeddedServletContainerFactory的getEmbeddedServletContainer方法獲取Servlet容器,並且啟動Tomcat,大家可以看看文章開頭的getEmbeddedServletContainer方法。

    大家看一下第8行代碼獲取Servlet容器方法的參數getSelfInitializer(),這是個啥?我們點進去看看

    private ServletContextInitializer getSelfInitializer() {
        //創建一個ServletContextInitializer對象,並重寫onStartup方法,很明顯是一個回調方法
        return new ServletContextInitializer() { public void onStartup(ServletContext servletContext) throws ServletException { EmbeddedWebApplicationContext.this.selfInitialize(servletContext); } };
    }

    創建一個ServletContextInitializer對象,並重寫onStartup方法,很明顯是一個回調方法,這裏給大家留一點疑問:

    • ServletContextInitializer對象創建過程是怎樣的?
    • onStartup是何時調用的?
    • onStartup方法的作用是什麼?

    ServletContextInitializer是 Servlet 容器初始化的時候,提供的初始化接口。這裏涉及到Servlet、Filter實例的註冊,我們留在下一篇具體講

     

     

     

     

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

    【其他文章推薦】

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

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

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

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

    ※專營大陸快遞台灣服務

    台灣快遞大陸的貨運公司有哪些呢?

  • 技術人如何利用 github+Jekyll ,搭建一個獨立免費的技術博客

    技術人如何利用 github+Jekyll ,搭建一個獨立免費的技術博客

    上次有人留言說,技術博客是程序員的標配,但據我所知絕大部分技術同學到現在仍然沒有自己的技術博客。原因有很多,有的是懶的寫,有的是怕寫不好,還有的是一直想憋個大招,幻想做到完美再發出來,結果一直胎死腹中。但其實更多程序員是不知道如何去搭建一個博客,其實如今搭建一個個人技術博客非常簡單,其中最簡單搭建方式莫屬使用 GitHub Pages + Jekyll 了,我的博客就是使用這種技術。

    GitHub Pages

    Github Pages 是面向用戶、組織和項目開放的公共靜態頁面搭建託管服務,站點可以被免費託管在 Github 上,你可以選擇使用 Github Pages 默認提供的域名 github.io 或者自定義域名來發布站點。Github Pages 支持 自動利用 Jekyll 生成站點,也同樣支持純 HTML 文檔,將你的 Jekyll 站點託管在 Github Pages 上是一個不錯的選擇。

    使用 Github Pages 搭建博客有以下幾個優點:

    • 完全免費,其中服務器、流量、域名什麼的都管,完全零費用搭建一個技術博客
    • 寫博客就是提交代碼,讓寫作和編程的體驗保持一致
    • 支持綁定自己的域名
    • 提供流行的網頁主題模板

    缺點也是有的:

    • 不支持動態內容,博客必須都是靜態網頁,一般會使用 Jekyll 來構建內容。
    • 博客不能被百度索引,因 Github 和百度有過節,所以 Github 就把百度給屏蔽了。
    • 倉庫空間不大於1G
    • 每個月的流量不超過100G
    • 每小時更新不超過 10 次

    Github Pages 使用 Jekyll 來構建內容,那麼 Jekyll 是什麼呢?

    Jekyll 介紹

    Jekyll 是一個簡單的博客形態的靜態站點生產機器。它有一個模版目錄,其中包含原始文本格式的文檔,通過一個轉換器(如 Markdown)和我們的 Liquid 渲染器轉化成一個完整的可發布的靜態網站,你可以發布在任何你喜愛的服務器上。Jekyll 也可以運行在 GitHub Page 上,也就是說,你可以使用 GitHub 的服務來搭建你的項目頁面、博客或者網站,而且是完全免費的。

    但如果我們只是在 GitHub 上面使用的話,到不需要知道 Jekyll 的語法,一般 Github 會自動將我們寫的 Markdown 文件轉換成靜態頁面。使用 Jekyll 需要使用 Markdown 語法來寫你的文章,不過 Markdown 語法非常簡單,做為程序員來講基本上兩三天就掌握了,大家也可以參考這篇文章:。

    給大家分享一些 Jekyll 主題,這個網站下有很多 主題,大家可以根據自己的愛好去選擇博客主題。

    我的個人博客

    我的博客經過了三個階段,第一個階段,完全依託於使用 GitHub Pages 來構建;第二個階段,將博客託管於國外的一個服務商;第三個階段,服務器遷移回到國內、域名備案。之前也寫過幾篇關於技術博客的文章,如下:

    使用 Github Pages + Jekyll 構建一個技術博客很簡單,基本上步驟就是網上找一個自己喜歡的主題,直接 Fork 到自己的 Github ,然後在刪掉原博客中的內容,在上傳自己的文章即可,以我自己的博客為例。

    我的博客最初使用的是,但這個主題已經盡兩年多都沒有更新了。因此後期我在這個主題的基礎上做了一些改動,其中有依賴組件的更新,結合個人情況對個別頁面進行了改版,就成為了現在的樣子:

    使用這個主題的原因是,我比較喜歡簡潔大氣的風格,並且此博客主題對代碼展示支持良好。

    快速構建一個博客

    以我的博客為例,介紹如何最快搭建一個博客。這也是我博客經歷的第一個階段。

    1、首先打開地址,點擊 Fork 按鈕將代碼複製一份到自己的倉庫。

    過上一分鐘,你的 github 倉庫發現一個 ityouknow.github.io 項目。

    2、刪除 CNAME 文件

    刪除項目中的 CNAME 文件,CNAME 是定製域名的時候使用的內容,如果不使用定製域名會存在衝突。

    3、設置 GitHub Pages

    點擊 Settings 按鈕打開設置頁面,頁面往下拉到 GitHub Pages 相關設置,在 Source 下面的複選框中選擇 master branch ,然後點擊旁邊的 Save 按鈕保存設置。

    4、重命名項目

    點擊 Settings 按鈕打開設置頁面,重命名項目名稱為:github_username.github.io。

    github_username 是你的 github 登錄用戶名

    5、重命名之後,再次回到 Settings > GitHub Pages 頁面

    會發現存在這樣一個地址:

    這個時候,你訪問此地址已經可以看到博客的首頁,但是點擊文章的時鏈接跳轉地址不對,這是因為少配置了一個文件。

    6、配置 _config.yml

    打開項目目錄下的 _config.yml 文件,修改以下配置:

    repository: github_username/github_username.github.io
    github_url: https://github.com/github_username
    url: https://github_username.github.io

    這時候在訪問地址: https://github_username.github.io,就會發現博客就已經構建完成了。剩下的事情就是去項目的 _posts 目錄下刪除掉我的文章,然後按照 Jekyll 的語法就寫自己的文章就好了。

    github_username 為你的 github id。

    自定義域名

    雖然通過地址https://github_username.github.io可以正常訪問博客,但是技術小夥伴們肯定有人想使用自己的域名訪問博客,這樣的需求 GitHub Pages 也是支持的。

    首先需要設置域名解析,將域名的地址指向自己的 github 博客地址。這裏以萬網的域名配置為例,選擇需要設置的域名點擊解析,在域名解析頁面添加以下兩條記錄

    紅框內,需要填寫自己github_username值。

    然後重新打開項目的 Settings > GitHub Pages 頁面,Custom domain 下的輸入框輸入剛才設置的域名:xxx.com,點擊保存即可。

    重新配置 _config.yml

    打開項目目錄下的 _config.yml 文件,修改以下配置:

    url: http://www.xxx.com

    等待一分鐘之後,瀏覽器訪問地址:www.xxx.com 即可訪問博客。

    自定義 DIY 博客

    一般同學到上面這一步也就完成了,基本滿足了 80% 技術同學的需求。但還是有一些同學們有更高的追求,比如說使用 Github Pages 雖然簡單方便,但是不能被百度檢索白白流失了大量的流量,還有一個原因有些時候,博客網絡訪問穩定性不是很高。

    當時我在國外有幾個虛擬機,本來用作它用,後來在上面安裝了一個 Nginx 作為靜態頁面的服務器。首先我在本機(win10)安裝了 Jekyll 環境,將 Github 上的博客代碼下載下來之後,在本機編譯成靜態的 Html ,然後手動上傳到服務的 Nginx 目錄下;然後將域名指向虛擬機。

    非常不建議大家實踐以上這段內容,win10 上面安裝 Jekyll 環境是一段慘痛的經歷。

    就這樣很麻煩的步驟我用了幾個月後,實在是受不了了,一方面因為服務器在國外,有時候仍然不穩定(可能因為服務器安裝了代理),另一方面我需要使用一些功能,使用這些功能的前提是網站需要備案,那段時間騰訊雲在做活動,就把博客又從國外搬了回來,順便重新優化了一下流程。

    仍然把博客託管在 Github 上面,每次提交完代碼后,在騰訊雲上面執行一個腳本,這個腳本會自動從 Github 拉取最新更新的文件,並自動生產靜態的 Html 文件推送到 Nginx 目錄,域名重新指向這台服務器。可以在 Github 上面設置一些鈎子,當提交代碼的時候自動觸髮腳本,也可以定時觸髮腳本來發布文章。

    腳本內容如下:

    cd /usr/local/ityouknow.github.io
    git pull http://github.com/ityouknow/ityouknow.github.io.git
    jekyll build --destination=/usr/share/nginx/html

    執行此腳本的前提是安裝好 git\jekyll 環境,這個網上有很多案例,這裏就不再多描述了。
    關於 Jekyll 環境搭建和使用可以參考這裏:

    自動化部署

    這两天看到,我也按照他的步驟實踐了一番,很好用,所以把自動化部署這段寫補上。

    配置 Webhook

    在開發過程中的 Webhook,是一種通過通常的 callback,去增加或者改變 Web page或者 Web app 行為的方法。這些 Callback 可以由第三方用戶和開發者維持當前,修改,管理,而這些使用者與網站或者應用的原始開發沒有關聯。Webhook 這個詞是由 Jeff Lindsay 在 2007 年在計算機科學 hook 項目第一次提出的。

    用大白話講就是,代碼倉庫在收到代碼提交的時候,會自動觸發一個 url 類型的通知,你可以根據這個通知去做一些事情,比如提交了代碼就自動去部署項目。

    我們的自動部署博客也是利用了這個機制,Github 自帶了 Webhook 功能,我們直接配置即可使用。

    在 Github 倉庫的項目界面,比如本博客項目 https://github.com/ityouknow/ityouknow.github.io,點擊 Setting->Webhooks->Add Webhook,在添加 Webhooks 的配置信息,我的配置信息如下:

    Payload URL: http://www.ityouknow.com/deploy
    Content type: application/json
    Secret: 123456

    如下圖:

    服務器接受推送

    我們需要在博客的服務器上面建立一個服務,來接收 Github 提交代碼后的推送,從而來觸發部署的腳本。 Github 上有一個開源項目可以做這個事情 。

    這個開源項目目的很單純,就是負責接收 Github 推送過來的通知,然後執行部署腳本,不過他是使用 NodeJs 來開發的,所以我們先需要在 Centos 上面按照 Node 環境。

    centos7 安裝 Node 環境

    首先添加源

    sudo rpm -ivh https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/e/epel-release-7-11.noarch.rpm
    
    //yum安裝node.js
    yum install -y nodejs

    然後在安裝 github-webhook-handler

    npm install -g github-webhook-handler     #安裝 github-webhook-handler
    #如果沒有安裝成功,可以選擇法2來安裝
    npm install -g cnpm --registry=http://r.cnpmjs.org
    cnpm install -g github-webhook-handler

    安裝成功之後,我們需要添加一個腳本。進入到安裝目錄下:

    cd  /usr/lib/node_modules/github-webhook-handler

    新建 deploy.js

    vi deploy.js

    腳本內容如下:

    var http = require('http')
    var createHandler = require('github-webhook-handler')
    var handler = createHandler({ path: '/deploy', secret: 'ityouknow' }) //監聽請求路徑,和Github 配置的密碼
     
    function run_cmd(cmd, args, callback) {
      var spawn = require('child_process').spawn;
      var child = spawn(cmd, args);
      var resp = "";
     
      child.stdout.on('data', function(buffer) { resp += buffer.toString(); });
      child.stdout.on('end', function() { callback (resp) });
    }
     
    http.createServer(function (req, res) {
      handler(req, res, function (err) {
        res.statusCode = 404
        res.end('no such location')
      })
    }).listen(3006)//監聽的端口
     
    handler.on('error', function (err) {
      console.error('Error:', err.message)
    })
     
    handler.on('push', function (event) {
      console.log('Received a push event for %s to %s',
        event.payload.repository.name,
        event.payload.ref);
      run_cmd('sh', ['/usr/local/depoly.sh'], function(text){ console.log(text) });//成功后,執行的腳本。
    })

    腳本的作業就是啟動一個監聽端口來接收請求,接收到請求后執行部署腳本,腳本內容的關鍵點已經標註上註釋。

    部署博客的腳本如下:depoly.sh

    echo `date`
    cd /usr/local/ityouknow.github.io
    echo start pull from github 
    git pull http://github.com/ityouknow/ityouknow.github.io.git
    echo start build..
    jekyll build --destination=/usr/share/nginx/html

    就是拉取代碼,進行部署而已。

    這個腳本的啟動需要藉助 Node 中的一個管理 forever 。forever 可以看做是一個 nodejs 的守護進程,能夠啟動,停止,重啟我們的 app 應用。

    不過我們的先安裝 forever,然後需要使用 forever 來啟動 deploy.js 的服務,執行命令如下:

    npm install forever -g   #安裝
    $ forever start deploy.js          #啟動
    $ forever stop deploy.js           #關閉
    $ forever start -l forever.log -o out.log -e err.log deploy.js   #輸出日誌和錯誤
    /root/node-v8.12.0-linux-x64/lib/node_modules/forever/bin/forever start -l forever.log -o out.log -e err.log deploy.js
    
    如果報錯:
    /root/node-v8.12.0-linux-x64/lib/node_modules/forever/bin/forever start -a -l forever.log -o out.log -e err.log deploy.js

    同時一般情況下,我們不會對外保留很多端口,所以需要通過博客的地址來轉發,需要在 Nginx 上面添加一個轉發配置,用來監聽的 /deploy 請求轉發到 nodejs 服務上,配置代碼如下:

    location = /deploy {
         proxy_pass http://127.0.0.1:3006/deploy;
    }

    這樣我們整個自動化部署就完了,每次提交代碼時,Github 會發送 Webhook 給地址http://www.ityouknow.com/deploy,Nginx 將 /deploy 地址轉發給 Nodejs 端口為 3306 的服務,最後通過 github-webhook-handler 來執行部署腳本,已到達自動部署的目的。

    以後只需要我們提交代碼到 Github ,就會自動觸發博客的自動化部署。

    可能會出現的問題

    有一些小夥伴反饋在克隆博客的時候出現了一些問題,在這裏集中回復一下。

    1、克隆博客后格式丟失

    這是很多讀者反饋的第一個問題,因為我的博客 css 和 圖片是放到另外一個域名下的:www.itmind.net ,因此這塊大家克隆過去需要改成本地的。

    主要涉及的文件 ityouknow.github.io\_includes 目錄下 head.html 和 footer.html 兩個文件夾,將文件中的 http://www.ityouknow.com/xxx/xxx 改為相對路徑/xxx/xxx即可。

    2、留言功能丟失

    這裏就需要大家修改一下 _config.yml 中 gitalk 的配置信息。具體如何操作大家可以參考這篇文章 。註冊完之後,需要在 _config.yml 配置以下信息:

    gitalk:
        owner: ityouknow
        repo: blog-comments
        clientID: 61bfc53d957e74e78f8f
        clientSecret: 31c61e66cdcc9ada8db2267ee779d0bdafac434c

    將這裏改成你註冊好的信息

    3、博客

    博客現在還缺檢索功能,下一頁和上一頁功能、系列文章優化查看的功能,大家克隆後有完善功能的,也請幫忙留意,共同把這個博客完善的更好。

    最後,大家可以在這篇文章下留下你的個人博客地址,方便同行們觀賞、交流、學習。

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

    【其他文章推薦】

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

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

    台灣海運大陸貨務運送流程

    兩岸物流進出口一站式服務

  • 021.掌握Pod-Pod調度策略

    021.掌握Pod-Pod調度策略

    一 Pod生命周期管理

    1.1 Pod生命周期

    Pod在整個生命周期過程中被系統定義了如下各種狀態。

    狀態值 描述
    Pending API Server已經創建該Pod,且Pod內還有一個或多個容器的鏡像沒有創建,包括正在下載鏡像的過程。
    Running Pod內所有容器均已創建,且至少有一個容器處於運行狀態、正在啟動狀態或正在重啟狀態。
    Succeeded Pod內所有容器均成功執行退出,且不會重啟。
    Failed Pod內所有容器均已退出,但至少有一個容器退出為失敗狀態。
    Unknown 由於某種原因無法獲取該Pod狀態,可能由於網絡通信不暢導致。

    1.2 Pod重啟策略

    Pod重啟策略(RestartPolicy)應用於Pod內的所有容器,並且僅在Pod所處的Node上由kubelet進行判斷和重啟操作。當某個容器異常退出或者健康檢查失敗時,kubelet將根據RestartPolicy的設置來進行相應操作。
    Pod的重啟策略包括Always、OnFailure和Never,默認值為Always。

    • Always:當容器失效時,由kubelet自動重啟該容器;
    • OnFailure:當容器終止運行且退出碼不為0時,由kubelet自動重啟該容器;
    • Never:不論容器運行狀態如何,kubelet都不會重啟該容器。

           kubelet重啟失效容器的時間間隔以sync-frequency乘以2n來計算,例如1/2/4/8倍等,最長延時5min,並且在成功重啟后的10min后重置該時間。

    Pod的重啟策略與控制方式關聯,當前可用於管理Pod的控制器包括ReplicationController、Job、DaemonSet及直接管理kubelet管理(靜態Pod)。
    不同控制器的重啟策略限制如下:

    • RC和DaemonSet:必須設置為Always,需要保證該容器持續運行;
    • Job:OnFailure或Never,確保容器執行完成后不再重啟;
    • kubelet:在Pod失效時重啟,不論將RestartPolicy設置為何值,也不會對Pod進行健康檢查。








    Pod包含的容器數 Pod當前的狀態 發生事件 Pod的結果狀態
    RestartPolicy=Always RestartPolicy=OnFailure RestartPolicy=Never
    包含1個容器 Running 容器成功退出 Running Succeeded Succeeded
    包含1個容器 Running 容器失敗退出 Running Running Failed
    包括兩個容器 Running 1個容器失敗退出 Running Running Running
    包括兩個容器 Running 容器被OOM殺掉 Running Running Failed

    1.3 Pod健康檢查

    對Pod的健康檢查可以通過兩類探針來檢查:LivenessProbe和ReadinessProbe。
    LivenessProbe探針:用於判斷容器是否存活(running狀態),如果LivenessProbe探針探測到容器不健康,則kubelet將殺掉該容器,並根據容器的重啟策略做相應處理。若一個容器不包含LivenessProbe探針,kubelet認為該容器的LivenessProbe探針返回值用於是“Success”。
    ReadineeProbe探針:用於判斷容器是否啟動完成(ready狀態)。如果ReadinessProbe探針探測到失敗,則Pod的狀態將被修改。Endpoint Controller將從Service的Endpoint中刪除包含該容器所在Pod的Eenpoint。
    kubelet定期執行LivenessProbe探針來診斷容器的健康狀態,通常有以下三種方式:

    • ExecAction:在容器內執行一個命令,若返回碼為0,則表明容器健康。

    示例:通過執行”cat /tmp/health”命令判斷一個容器運行是否正常。容器初始化並創建該文件,10s后刪除該文件,15s秒通過命令判斷,由於該文件已被刪除,因此判斷該容器Fail,導致kubelet殺掉該容器並重啟。

      1 [root@uk8s-m-01 study]# vi dapi-liveness.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: dapi-liveness-pod
      6   labels:
      7     test: liveness-exec
      8 spec:
      9   containers:
     10     - name: dapi-liveness
     11       image: busybox
     12       args:
     13       - /bin/sh
     14       - -c
     15       - echo ok > /tmp/health; sleep 10; rm -rf /tmp/health; sleep 600
     16       livenessProbe:
     17         exec:
     18           command:
     19           - cat
     20           - /tmp/health
     21 
     22 [root@uk8s-m-01 study]# kubectl describe pod dapi-liveness-pod

    • TCPSocketAction:通過容器的IP地址和端口號執行TCP檢查,若能建立TCP連接,則表明容器健康。

    示例:

      1 [root@uk8s-m-01 study]# vi dapi-tcpsocket.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: dapi-healthcheck-tcp
      6 spec:
      7   containers:
      8     - name: nginx
      9       image: nginx
     10       ports:
     11       - containerPort: 80
     12       livenessProbe:
     13         tcpSocket:
     14           port: 80
     15         initialDelaySeconds: 30
     16         timeoutSeconds: 1
     17 
     18 [root@uk8s-m-01 study]# kubectl create -f dapi-tcpsocket.yaml


    提示:對於每種探測方式,都需要設置如下兩個參數,其包含的含義如下:

    initialDelaySeconds:啟動容器後進行首次健康檢查的等待時間,單位為s;

    timeoutSeconds:健康檢查發送請求后等待響應的超時時間,單位為s,當超時發生時,kubelet會認為容器已經無法提供服務,將會重啟該容器。

    二 Pod調度

    Kubernetes中,Pod通常是容器的載體,一般需要通過Deployment、DaemonSet、RC、Job等對象來完成一組Pod的調度與自動控制功能。

    2.1 Depolyment/RC自動調度

    Deployment或RC的主要功能之一就是自動部署一個容器應用的多份副本,以及持續監控副本的數量,在集群內始終維持用戶指定的副本數量。
    示例:

      1 [root@uk8s-m-01 study]# vi nginx-deployment.yaml
      2 apiVersion: apps/v1beta1
      3 kind: Deployment
      4 metadata:
      5   name: nginx-deployment-01
      6 spec:
      7   replicas: 3
      8   template:
      9     metadata:
     10       labels:
     11         app: nginx
     12     spec:
     13       containers:
     14       - name: nginx
     15         image: nginx:1.7.9
     16         ports:
     17         - containerPort: 80
     18 
     19 [root@uk8s-m-01 study]# kubectl get deployments
     20 NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
     21 nginx-deployment-01   3/3     3            3           30s
     22 [root@uk8s-m-01 study]# kubectl get rs
     23 NAME                             DESIRED   CURRENT   READY   AGE
     24 nginx-deployment-01-5754944d6c   3         3         3       75s
     25 [root@uk8s-m-01 study]# kubectl get pod | grep nginx
     26 nginx-deployment-01-5754944d6c-hmcpg   1/1     Running     0          84s
     27 nginx-deployment-01-5754944d6c-mcj8q   1/1     Running     0          84s
     28 nginx-deployment-01-5754944d6c-p42mh   1/1     Running     0          84s

    2.2 NodeSelector定向調度

    當需要手動指定將Pod調度到特定Node上,可以通過Node的標籤(Label)和Pod的nodeSelector屬性相匹配。
    # kubectl label nodes <node-name> <label-key>=<label-value>
    node節點創建對應的label后,可通過在定義Pod的時候加上nodeSelector的設置實現指定的調度。
    示例:

      1 [root@uk8s-m-01 study]# kubectl label nodes 172.24.9.14 speed=io
      2 node/172.24.9.14 labeled
      3 [root@uk8s-m-01 study]# vi nginx-master-controller.yaml
      4 kind: ReplicationController
      5 metadata:
      6   name: nginx-master
      7   labels:
      8     name: nginx-master
      9 spec:
     10   replicas: 1
     11   selector:
     12     name: nginx-master
     13   template:
     14     metadata:
     15       labels:
     16         name: nginx-master
     17     spec:
     18       containers:
     19       - name: master
     20         image: nginx:1.7.9
     21         ports:
     22         - containerPort: 80
     23       nodeSelector:
     24         speed: io
     25 
     26 [root@uk8s-m-01 study]# kubectl create -f nginx-master-controller.yaml
     27 [root@uk8s-m-01 study]# kubectl get pods -o wide
     28 NAME                READY   STATUS    RESTARTS    AGE    IP            NODE
     29 nginx-master-7fjgj  1/1     Running   0           82s    172.24.9.71   172.24.9.14


    提示:可以將集群中具有不同特點的Node貼上不同的標籤,實現在部署時就可以根據應用的需求設置NodeSelector來進行指定Node範圍的調度。

    注意:若在定義Pod中指定了NodeSelector條件,但集群中不存在符合該標籤的Node,即使集群有其他可供使用的Node,Pod也無法被成功調度。

    2.3 NodeAffinity親和性調度

    親和性調度機制極大的擴展了Pod的調度能力,主要增強功能如下:

    1. 更具表達力,即更精細的力度控制;
    2. 可以使用軟限制、優先採用等限制方式,即調度器在無法滿足優先需求的情況下,會使用其他次條件進行滿足;
    3. 可以依據節點上正在運行的其他Pod的標籤來進行限制,而非節點本身的標籤,從而實現Pod之間的親和或互斥關係。

    目前有兩種節點親和力表達:
    requiredDuringSchedulingIgnoredDuringExecution:硬規則,必須滿足指定的規則,調度器才可以調度Pod至Node上(類似nodeSelector,語法不同)。
    preferredDuringSchedulingIgnoredDuringExecution:軟規則,優先調度至滿足的Node的節點,但不強求,多個優先級規則還可以設置權重值。
    IgnoredDuringExecution指:如果一個Pod所在的節點在Pod運行期間標籤發生了變化,不再符合該Pod的節點親和性需求,則系統將忽略Node上Label的變化,該Pod能繼續在該節點運行。
    示例:
    條件1:只運行在amd64的節點上;盡量運行在ssd節點上。

      1 [root@uk8s-m-01 study]# vi nodeaffinity-pod.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: with-node-affinity
      6 spec:
      7   affinity:
      8     nodeAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10         nodeSelectorTerms:
     11         - matchExpressions:
     12           - key: kubernetes.io/arch
     13             operator: In
     14             values:
     15             - amd64
     16       preferredDuringSchedulingIgnoredDuringExecution:
     17       - weight: 1
     18         preference:
     19           matchExpressions:
     20           - key: disk-type
     21             operator: In
     22             values:
     23             - ssd
     24   containers:
     25   - name: with-node-affinity
     26     image: gcr.azk8s.cn/google_containers/pause:2.0


    NodeAffinity操作語法;In、NotIn、Exists、DoesNotExist、Gt、Lt。NotIn和DoesNotExist可以實現互斥功能。
    NodeAffinity規則設置注意事項:

    • 若同時定義nodeSelector和nodeAffinity,則必須兩個條件都滿足,Pod才能最終運行指定在Node上;;
    • 若nodeAffinity指定多個nodeSelectorTerms,則只需要其中一個能夠匹配成功即可;
    • 若nodeSelectorTerms中有多個matchExpressions,則一個節點必須滿足所有matchExpressions才能運行該Pod。

    2.4 PodAffinity親和性調度

    PodAffinity根據節點上正在運行的Pod標籤而不是Node標籤來判斷和調度,要求對節點和Pod兩個條件進行匹配。
    規則描述為:若在具有標籤X的Node上運行了一個或多個符合條件Y的Pod,則Pod應該(或者不應該)運行在這個Node上。
    X通常為Node節點的機架、區域等概念,Pod是屬於某個命名空間,所以條件Y表達的是一個或全部命名空間中的一個Label Selector。
    Pod親和性定義與PodSpec的affinity字段下的podAffinity字段里,互斥性定義於同一層次的podAntiAffinity子字段中。
    舉例:

      1 [root@uk8s-m-01 study]# vi nginx-flag.yaml	#創建名為pod-flag,帶有兩個標籤的Pod
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: pod-affinity
      6 spec:
      7   affinity:
      8     podAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10       - labelSelector:
     11           matchExpressions:
     12           - key: security
     13             operator: In
     14             values:
     15             - S1
     16         topologyKey: kubernetes.io/hostname
     17   containers:
     18   - name: with-pod-affinity
     19     image: gcr.azk8s.cn/google_containers/pause:2.0

      1 [root@uk8s-m-01 study]# vi nginx-affinity-in.yaml	#創建定義標籤security=S1,對應如上Pod “Pod-flag”。
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: pod-affinity
      6 spec:
      7   affinity:
      8     podAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10       - labelSelector:
     11           matchExpressions:
     12           - key: security
     13             operator: In
     14             values:
     15             - S1
     16         topologyKey: kubernetes.io/hostname
     17   containers:
     18   - name: with-pod-affinity
     19     image: gcr.azk8s.cn/google_containers/pause:2.0
     20 
     21 [root@uk8s-m-01 study]# kubectl create -f nginx-affinity-in.yaml
     22 [root@uk8s-m-01 study]# kubectl get pods -o wide


    提示:由上Pod親和力可知,兩個Pod處於同一個Node上。

      1 [root@uk8s-m-01 study]# vi nginx-affinity-out.yaml	#創建不能與參照目標Pod運行在同一個Node上的調度策略
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: anti-affinity
      6 spec:
      7   affinity:
      8     podAffinity:
      9       requiredDuringSchedulingIgnoredDuringExecution:
     10       - labelSelector:
     11           matchExpressions:
     12           - key: security
     13             operator: In
     14             values:
     15             - S1
     16         topologyKey: failure-domain.beta.kubernetes.io/zone
     17     podAntiAffinity:
     18       requiredDuringSchedulingIgnoredDuringExecution:
     19       - labelSelector:
     20           matchExpressions:
     21           - key: security
     22             operator: In
     23             values:
     24             - nginx
     25         topologyKey: kubernetes.io/hostname
     26   containers:
     27   - name: anti-affinity
     28     image: gcr.azk8s.cn/google_containers/pause:2.0
     29 
     30 [root@uk8s-m-01 study]# kubectl get pods -o wide	#驗證

    2.5 Taints和Tolerations(污點和容忍)

    Taint:使Node拒絕特定Pod運行;
    Toleration:為Pod的屬性,表示Pod能容忍(運行)標註了Taint的Node。
    Taint語法:$ kubectl taint node node1 key=value:NoSchedule
    解釋:為node1加上一個Taint,該Taint的鍵為key,值為value,Taint的效果為NoSchedule。即除非特定聲明可以容忍此Taint,否則不會調度至node1上。
    Toleration示例:

      1 tolerations:
      2 - key: "key"
      3   operator: "Equal"
      4   value: "value"
      5   effect: "NoSchedule"

      1 tolerations:
      2 - key: "key"
      3   operator: "Exists"
      4   effect: "NoSchedule"

    注意:Pod的Toleration聲明中的key和effect需要與Taint的設置保持一致,並且滿足以下條件:

    • operator的值是Exists(無須指定value);
    • operator的值是Equal並且value相等;
    • 空的key配合Exists操作符能夠匹配所有的鍵和值;
    • 空的effect匹配所有的effect。


    若不指定operator,則默認值為Equal。
    taint說明:系統允許在同一個Node上設置多個taint,也可以在Pod上設置多個toleration。Kubernetes調度器處理多個taint和toleration的邏輯順序:首先列出節點中所有的taint,然後忽略pod的toleration能夠匹配的部分,剩下的沒有忽略掉的taint就是對pod的效果。以下是幾種特殊情況:
    若剩餘的taint中存在effect=NoSchedule,則調度器不會把該Pod調度到這一節點上;
    若剩餘的taint中沒有NoSchedule效果,但有PreferNoSchedule效果,則調度器會嘗試不把這個Pod指派到此節點;
    若剩餘taint的效果有NoSchedule,並且這個Pod已經在該節點上運行,則會被驅逐,若沒有在該節點上運行,也不會再被調度到該節點上。
    示例:

      1 $ kubectl taint node node1 key=value1:NoSchedule
      2 $ kubectl taint node node1 key=value1:NoExecute
      3 $ kubectl taint node node1 key=value2:NoSchedule
      4 tolerations:
      5 - key: "key1"
      6   operator: "Equal"
      7   value: "value"
      8   effect: "NoSchedule"
      9 tolerations:
     10 - key: "key1"
     11   operator: "Equal"
     12   value: "value1"
     13   effect: "NoExecute"


    釋義:此Pod聲明了兩個容忍,且能匹配Node1的taint,但是由於沒有能匹配第三個taint的toleration,因此此Pod依舊不能調度至此Node。若該Pod已經在node1上運行了,那麼在運行時設置了第3個taint,它還能繼續在node1上運行,這是因為Pod可以容忍前兩個taint。
    通常,若node加上effect=NoExecute的taint,那麼該Node上正在運行的所有無對應toleration的Pod都會被立刻驅逐,而具有相應toleration的Pod則永遠不會被驅逐。同時,系統可以給具有NoExecute效果的toleration加入一個可選的tolerationSeconds字段,表明Pod可以在taint添加到Node之後還能在此Node運行多久。

      1 tolerations:
      2 - key: "key1"
      3   operator: "Equal"
      4   value: "value"
      5   effect: "NoSchedule"
      6   tolerationSeconds: 3600

    釋義:若Pod正在運行,所在節點被加入一個匹配的taint,則這個pod會持續在該節點運行3600s后被驅逐。若在此期限內,taint被移除,則不會觸發驅逐事件。
    Taints和Tolerations常用場景:

    • 獨佔節點:

    給特定的節點運行特定應用。
    $ kubectl taint nodes 【nodename】 dedicated=groupName:NoSchedule
    同時在Pod中設置對應的toleration配合,帶有合適toleration的Pod允許同時使用其他節點一樣使用有taint的節點。

    • 具有特殊硬件設備的節點

    集群中部分特殊硬件(如安裝了GPU),則可以把不需要佔用GPU的Pod禁止在此Node上調度。

      1 $ kubectl taint nodes 【nodename】 special=true:NoSchedule
      2 $ kubectl taint nodes 【nodename】 special=true:PreferNoSchedule

    • 定義Pod驅逐行為

    NoExecute的taint對節點上正在運行的Pod有以下影響:

      1. 沒有設置toleration的pod會被立刻驅逐;
      2. 配置了對應toleration的pod,若沒有為tolerationSeconds賦值,則會一直保留在此節點中;
      3. 配置了對應toleration的pod,且為tolerationSeconds賦值,則在指定時間后驅逐。

    2.6 DaemonSet

    DaemonSet是在每個Node上調度一個Pod的資源對象,用於管理集群中每個Node僅運行一份Pod的副本實例。
    常見場景:
    在每個Node上運行一個GlusterFS存儲的Daemon進程;
    在每個Node上運行一個日誌採集程序,例如Fluentd;
    在每個Node上運行一個性能監控程序,採集該Node的運行性能數據,例如Prometheus。
    示例:

      1 [root@uk8s-m-01 study]# vi fluentd-ds.yaml
      2 apiVersion: extensions/v1beta1
      3 kind: DaemonSet
      4 metadata:
      5   name: fluentd-cloud-logging
      6   namespace: kube-system
      7   labels:
      8     k8s-app: fluentd-cloud-logging
      9 spec:
     10   template:
     11     metadata:
     12       namespace: kube-system
     13       labels:
     14         k8s-app: fluentd-cloud-logging
     15     spec:
     16       containers:
     17       - name: fluentd-cloud-logging
     18         image: gcr.azk8s.cn/google_containers/fluentd-elasticsearch:1.17
     19         resources:
     20           limits:
     21             cpu: 100m
     22             memory: 200Mi
     23         env:
     24         - name: FLUENTD_ARGS
     25           value: -q
     26         volumeMounts:
     27         - name: varlog
     28           mountPath: /var/log
     29           readOnly: false
     30         - name: containers
     31           mountPath: /var/lib/docker/containers
     32           readOnly: false
     33       volumes:
     34       - name: containers
     35         hostPath:
     36           path: /var/lib/docker/containers
     37       - name: varlog
     38         hostPath:
     39           path: /var/log

    2.7 Job批處理調度

    通過Kubernetes Job資源對象可以定義並啟動一個批處理任務,批處理任務通過并行(或者串行)啟動多個計算進程去處理一批工作項。根據批處理方式不同,批處理任務可以分為如下幾種模式:
    Job Template Expansion模式:一個Job對象對應一個待處理的Work item,有幾個work item就產生幾個獨立的Job。通常適合Work item數量少、每個Work item要處理的數據量比較大的場景。
    Queue with Pod Per Work Item模式:採用一個任務隊列存放Work item,一個Job對象作為消費者去完成這些Work item。此模式下,Job會啟動N個Pod,每個Pod都對應一個Work item。
    Queue with Variable Pod Count模式:採用一個任務隊列存放Work item,一個Job對象作為消費者去完成這些Work item,但此模式下Job啟動的數量是可變的。
    Kubernetes將Job氛圍以下三類:

    • Non-parallel Jobs

    通常一個Job只啟動一個Pod,除非Pod異常,才會重啟該Pod,一旦此Pod正常結束,Job將結束。

    • Parallel Jobs with a fixed completion count

    并行Job會啟動多個Pod,此時需要設定Job的.spec.completions參數為一個正數,當正常結束的Pod數量達至此參數設定的值后,Job結束。同時.spec.parallelism參數用來控制并行度,即同時啟動幾個Job來處理Work Item。

    • Parallel Jobs with a work queue

    任務隊列方式的并行Job需要一個獨立的Queue,Work Item都在一個Queue中存放,不能設置Job的.spec.completions參數,此時Job具有以下特性:

      1. 每個Pod都能獨立判斷和決定是否還有任務項需要處理;
      2. 如果某個Pod正常結束,則Job不會再啟動新的Pod;
      3. 如果一個Pod成功結束,則此時應該不存在其他Pod還在工作的情況。它們應該都處於即將結束、退出的狀態;
      4. 如果所有Pod都結束了,且至少有一個Pod成功結束,則整個Jod成功結束。

    2.8 Cronjob定時任務

    表達式:Minutes Hours DayofMonth Month DayofWeek Year
    Minutes:可出現”,”、”_”、”*”、”/”,有效範圍為0~59的整數;
    Hours:出現”,”、”_”、”*”、”/”,有效範圍為0~23的整數;
    DayofMonth:出現”,”、”_”、”*”、”/”、”L”、”W”、”C”,有效範圍為0~31的整數;
    Month:可出現”,”、”_”、”*”、”/”,有效範圍為1~12的整數或JAN~DEC;
    DayofWeek:出現”,”、”_”、”*”、”/”、”L”、”W”、”C”、”#”,有效範圍為1~7的整數或SUN~SAT;
    *: 表示匹配該域的任意值, 假如在Minutes域使用“*”, 則表示每分鐘都會觸發事件。
    /: 表示從起始時間開始觸發, 然後每隔固定時間觸發一次,例如在Minutes域設置為5/20, 則意味着第1次觸發在第5min時, 接下來每20min觸發一次, 將在第25min、 第45min等時刻分別觸發。
    示例:*/1 * * * * #每隔1min執行一次任務

      1 [root@uk8s-m-01 study]# vi cron.yaml
      2 apiVersion: batch/v2alpha1
      3 kind: CronJob
      4 metadata:
      5   name: hello
      6 spec:
      7   schedule: "*/1 * * * *"
      8   jobTemplate:
      9     spec:
     10       template:
     11         spec:
     12           containers:
     13           - name: hello
     14             image: busybox
     15             args:
     16             - /bin/sh
     17             - -c
     18             - date; echo Hello from the Kubernetes cluster
     19           restartPolicy: OnFailure

      1 [root@master study]# kubectl create -f cron.yaml
      2 [root@master study]# kubectl get cronjob hello
      3 NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
      4 hello   */1 * * * *   False     0        <none>          29s
      5 [root@master study]# kubectl get pods
      6 NAME                     READY   STATUS      RESTARTS   AGE
      7 hello-1573378080-zvvm5   0/1     Completed   0          68s
      8 hello-1573378140-9pmwz   0/1     Completed   0          8s
      9 [root@node1 ~]# docker logs c7					#node節點查看日誌
     10 Sun Nov 10 09:31:13 UTC 2019
     11 Hello from the Kubernetes cluster
     12 [root@master study]# kubectl get jobs				#查看任務
     13 NAME               COMPLETIONS   DURATION   AGE
     14 hello-1573378500   1/1           8s         3m7s
     15 hello-1573378560   1/1           4s         2m7s
     16 hello-1573378620   1/1           6s         67s
     17 hello-1573378680   1/1           4s         7s
     18 [root@master study]# kubectl get pods -o wide | grep hello-1573378680	#以job任務查看對應的pod
     19 [root@master study]# kubectl delete cj hello			#刪除cronjob

    2.9 初始化容器

    在很多應用場景中, 應用在啟動之前都需要進行如下初始化操作。

    • 等待其他關聯組件正確運行( 例如數據庫或某個後台服務) 。
    • 基於環境變量或配置模板生成配置文件。
    • 從遠程數據庫獲取本地所需配置, 或者將自身註冊到某个中央數據庫中。
    • 下載相關依賴包, 或者對系統進行一些預配置操作。

    示例:以Nginx應用為例, 在啟動Nginx之前, 通過初始化容器busybox為Nginx創建一個index.html主頁文件。同時init container和Nginx設置了一個共享的Volume, 以供Nginx訪問init container設置的index.html文件。

      1 [root@uk8s-m-01 study]# vi nginx-init-containers.yaml
      2 apiVersion: v1
      3 kind: Pod
      4 metadata:
      5   name: nginx
      6   annotations:
      7 spec:
      8   initContainers:
      9   - name: install
     10     image: busybox
     11     command:
     12     - wget
     13     - "-O"
     14     - "/work-dir/index.html"
     15     - http://kubernetes.io
     16     volumeMounts:
     17     - name: workdir
     18       mountPath: "/work-dir"
     19   containers:
     20   - name: nginx
     21     image: nginx:1.7.9
     22     ports:
     23     - containerPort: 80
     24     volumeMounts:
     25     - name: workdir
     26       mountPath: /usr/share/nginx/html
     27   dnsPolicy: Default
     28   volumes:
     29   - name: workdir
     30     emptyDir: {}

      1 [root@uk8s-m-01 study]# kubectl get pods
      2 NAME    READY   STATUS     RESTARTS   AGE
      3 nginx   0/1     Init:0/1   0          2s
      4 [root@uk8s-m-01 study]# kubectl get pods
      5 NAME    READY   STATUS    RESTARTS   AGE
      6 nginx   1/1     Running   0          13s
      7 [root@uk8s-m-01 study]# kubectl describe pod nginx		#查看事件可知會先創建init容器,名為install


    init容器與應用容器的區別如下。
    (1) init container的運行方式與應用容器不同, 它們必須先於應用容器執行完成, 當設置了多個init container時, 將按順序逐個運行, 並且只有前一個init container運行成功后才能運行后一個init container。 當所有init container都成功運行后, Kubernetes才會初始化Pod的各種信息, 並開始創建和運行應用容器。
    (2) 在init container的定義中也可以設置資源限制、 Volume的使用和安全策略, 等等。 但資源限制的設置與應用容器略有不同。

    • 如果多個init container都定義了資源請求/資源限制, 則取最大的值作為所有init container的資源請求值/資源限制值。
    • Pod的有效(effective) 資源請求值/資源限制值取以下二者中的較大值。
      • 所有應用容器的資源請求值/資源限制值之和。
      • init container的有效資源請求值/資源限制值。
    • 調度算法將基於Pod的有效資源請求值/資源限制值進行計算,即init container可以為初始化操作預留系統資源, 即使後續應用容器無須使用這些資源。
    • Pod的有效QoS等級適用於init container和應用容器。
    • 資源配額和限制將根據Pod的有效資源請求值/資源限制值計算生效。
    • Pod級別的cgroup將基於Pod的有效資源請求/限制, 與調度機制

    一致。
    (3) init container不能設置readinessProbe探針, 因為必須在它們成功運行后才能繼續運行在Pod中定義的普通容器。在Pod重新啟動時, init container將會重新運行, 常見的Pod重啟場景如下。

    • init container的鏡像被更新時, init container將會重新運行, 導致Pod重啟。 僅更新應用容器的鏡像只會使得應用容器被重啟。
    • Pod的infrastructure容器更新時, Pod將會重啟。
    • 若Pod中的所有應用容器都終止了, 並且RestartPolicy=Always, 則Pod會重啟。

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

    【其他文章推薦】

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

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

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

    小三通海運與一般國際貿易有何不同?

    小三通快遞通關作業有哪些?

  • Spring Boot2 系列教程(二十四)Spring Boot 整合 Jpa

    Spring Boot2 系列教程(二十四)Spring Boot 整合 Jpa

    Spring Boot 中的數據持久化方案前面給大夥介紹了兩種了,一個是 JdbcTemplate,還有一個 MyBatis,JdbcTemplate 配置簡單,使用也簡單,但是功能也非常有限,MyBatis 則比較靈活,功能也很強大,據我所知,公司採用 MyBatis 做數據持久化的相當多,但是 MyBatis 並不是唯一的解決方案,除了 MyBatis 之外,還有另外一個東西,那就是 Jpa,松哥也有一些朋友在公司里使用 Jpa 來做數據持久化,本文就和大夥來說說 Jpa 如何實現數據持久化。

    Jpa 介紹

    首先需要向大夥介紹一下 Jpa,Jpa(Java Persistence API)Java 持久化 API,它是一套 ORM 規範,而不是具體的實現,Jpa 的江湖地位類似於 JDBC,只提供規範,所有的數據庫廠商提供實現(即具體的數據庫驅動),Java 領域,小夥伴們熟知的 ORM 框架可能主要是 Hibernate,實際上,除了 Hibernate 之外,還有很多其他的 ORM 框架,例如:

    • Batoo JPA
    • DataNucleus (formerly JPOX)
    • EclipseLink (formerly Oracle TopLink)
    • IBM, for WebSphere Application Server
    • JBoss with Hibernate
    • Kundera
    • ObjectDB
    • OpenJPA
    • OrientDB from Orient Technologies
    • Versant Corporation JPA (not relational, object database)

    Hibernate 只是 ORM 框架的一種,上面列出來的 ORM 框架都是支持 JPA2.0 規範的 ORM 框架。既然它是一個規範,不是具體的實現,那麼必然就不能直接使用(類似於 JDBC 不能直接使用,必須要加了驅動才能用),我們使用的是具體的實現,在這裏我們採用的實現實際上還是 Hibernate。

    Spring Boot 中使用的 Jpa 實際上是 Spring Data Jpa,Spring Data 是 Spring 家族的一個子項目,用於簡化 SQL、NoSQL 的訪問,在 Spring Data 中,只要你的方法名稱符合規範,它就知道你想幹嘛,不需要自己再去寫 SQL。

    關於 Spring Data Jpa 的具體情況,大家可以參考

    工程創建

    創建 Spring Boot 工程,添加 Web、Jpa 以及 MySQL 驅動依賴,如下:

    工程創建好之後,添加 Druid 依賴,完整的依賴如下:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.28</version>
        <scope>runtime</scope>
    </dependency>

    如此,工程就算創建成功了。

    基本配置

    工程創建完成后,只需要在 application.properties 中進行數據庫基本信息配置以及 Jpa 基本配置,如下:

    # 數據庫的基本配置
    spring.datasource.username=root
    spring.datasource.password=root
    spring.datasource.url=jdbc:mysql:///test01?useUnicode=true&characterEncoding=UTF-8
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    
    # JPA配置
    spring.jpa.database=mysql
    # 在控制台打印SQL
    spring.jpa.show-sql=true
    # 數據庫平台
    spring.jpa.database-platform=mysql
    # 每次啟動項目時,數據庫初始化策略
    spring.jpa.hibernate.ddl-auto=update
    # 指定默認的存儲引擎為InnoDB
    spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect

    注意這裏和 JdbcTemplate 以及 MyBatis 比起來,多了 Jpa 配置,Jpa 配置含義我都註釋在代碼中了,這裏不再贅述,需要強調的是,最後一行配置,默認情況下,自動創建表的時候會使用 MyISAM 做表的引擎,如果配置了數據庫方言為 MySQL57Dialect,則使用 InnoDB 做表的引擎。

    好了,配置完成后,我們的 Jpa 差不多就可以開始用了。

    基本用法

    ORM(Object Relational Mapping) 框架表示對象關係映射,使用 ORM 框架我們不必再去創建表,框架會自動根據當前項目中的實體類創建相應的數據表。因此,我這裏首先創建一個 User 對象,如下:

    @Entity(name = "t_user")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        @Column(name = "name")
        private String username;
        private String address;
        //省略getter/setter
    }

    首先 @Entity 註解表示這是一個實體類,那麼在項目啟動時會自動針對該類生成一張表,默認的表名為類名,@Entity 註解的 name 屬性表示自定義生成的表名。@Id 註解表示這個字段是一個 id,@GeneratedValue 註解表示主鍵的自增長策略,對於類中的其他屬性,默認都會根據屬性名在表中生成相應的字段,字段名和屬性名相同,如果開發者想要對字段進行定製,可以使用 @Column 註解,去配置字段的名稱,長度,是否為空等等。

    做完這一切之後,啟動 Spring Boot 項目,就會發現數據庫中多了一個名為 t_user 的表了。

    針對該表的操作,則需要我們提供一個 Repository,如下:

    public interface UserDao extends JpaRepository<User,Integer> {
        List<User> getUserByAddressEqualsAndIdLessThanEqual(String address, Integer id);
        @Query(value = "select * from t_user where id=(select max(id) from t_user)",nativeQuery = true)
        User maxIdUser();
    }

    這裏,自定義 UserDao 接口繼承自 JpaRepository,JpaRepository 提供了一些基本的數據操作方法,例如保存,更新,刪除,分頁查詢等,開發者也可以在接口中自己聲明相關的方法,只需要方法名稱符合規範即可,在 Spring Data 中,只要按照既定的規範命名方法,Spring Data Jpa 就知道你想幹嘛,這樣就不用寫 SQL 了,那麼規範是什麼呢?參考下圖:

    當然,這種方法命名主要是針對查詢,但是一些特殊需求,可能並不能通過這種方式解決,例如想要查詢 id 最大的用戶,這時就需要開發者自定義查詢 SQL 了。

    如上代碼所示,自定義查詢 SQL,使用 @Query 註解,在註解中寫自己的 SQL,默認使用的查詢語言不是 SQL,而是 JPQL,這是一種數據庫平台無關的面向對象的查詢語言,有點定位類似於 Hibernate 中的 HQL,在 @Query 註解中設置 nativeQuery 屬性為 true 則表示使用原生查詢,即大夥所熟悉的 SQL。上面代碼中的只是一個很簡單的例子,還有其他一些點,例如如果這個方法中的 SQL 涉及到數據操作,則需要使用 @Modifying 註解。

    好了,定義完 Dao 之後,接下來就可以將 UserDao 注入到 Controller 中進行測試了(這裏為了省事,就沒有提供 Service 了,直接將 UserDao 注入到 Controller 中)。

    @RestController
    public class UserController {
        @Autowired
        UserDao userDao;
        @PostMapping("/")
        public void addUser() {
            User user = new User();
            user.setId(1);
            user.setUsername("張三");
            user.setAddress("深圳");
            userDao.save(user);
        }
        @DeleteMapping("/")
        public void deleteById() {
            userDao.deleteById(1);
        }
        @PutMapping("/")
        public void updateUser() {
            User user = userDao.getOne(1);
            user.setUsername("李四");
            userDao.flush();
        }
        @GetMapping("/test1")
        public void test1() {
            List<User> all = userDao.findAll();
            System.out.println(all);
        }
        @GetMapping("/test2")
        public void test2() {
            List<User> list = userDao.getUserByAddressEqualsAndIdLessThanEqual("廣州", 2);
            System.out.println(list);
        }
        @GetMapping("/test3")
        public void test3() {
            User user = userDao.maxIdUser();
            System.out.println(user);
        }
    }
    

    如此之後,即可查詢到需要的數據。

    好了,本文的重點是 Spring Boot 和 Jpa 的整合,這個話題就先說到這裏。

    多說兩句

    在和 Spring 框架整合時,如果用到 ORM 框架,大部分人可能都是首選 Hibernate,實際上,在和 Spring+SpringMVC 整合時,也可以選擇 Spring Data Jpa 做數據持久化方案,用法和本文所述基本是一樣的,Spring Boot 只是將 Spring Data Jpa 的配置簡化了,因此,很多初學者對 Spring Data Jpa 覺得很神奇,但是又覺得無從下手,其實,此時可以回到 Spring 框架,先去學習 Jpa,再去學習 Spring Data Jpa,這是給初學者的一點建議。

    相關案例已經上傳到 GitHub,歡迎小夥伴們們下載:

    掃碼關注松哥,公眾號後台回復 2TB,獲取松哥獨家 超2TB 學習資源

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

    【其他文章推薦】

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

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

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

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

    小三通物流營運型態?

    ※快速運回,大陸空運推薦?

  • webpack優化之玩轉代碼分割和公共代碼提取

    webpack優化之玩轉代碼分割和公共代碼提取

    前言

    開發多頁應用的時候,如果不對webpack打包進行優化,當某個模塊被多個入口模塊引用時,它就會被打包多次(在最終打包出來的某幾個文件里,它們都會有一份相同的代碼)。當項目業務越來越複雜,打包出來的代碼會非常冗餘,文件體積會非常龐大。大體積文件會增加編譯時間,影響開發效率;如果直接上線,還會拉長請求和加載時長,影響網站體驗。作為一個追求極致體驗的攻城獅,是不能忍的。所以在多頁應用中優化打包尤為必要。那麼如何優化webpack打包呢?

    一、概念

    在一切開始前,有必要先理清一下這三個概念:

    • module: 模塊,在webpack眼裡,任何可以被導入導出的文件都是一個模塊。
    • chunk: chunk是webpack拆分出來的:
      • 每個入口文件都是一個chunk
      • 通過 import、require 引入的代碼也是
      • 通過 splitChunks 拆分出來的代碼也是
    • bundle: webpack打包出來的文件,也可以理解為就是對chunk編譯壓縮打包等處理后的產出。

    二、問題分析

    首先,簡單分析下,我們剛才提到的打包問題:

    • 核心問題就是:多頁應用打包後代碼冗餘,文件體積大。
    • 究其原因就是:相同模塊在不同入口之間沒有得到復用,bundle之間比較獨立。

    弄明白了問題的原因,那麼大致的解決思路也就出來了:

    • 我們在打包的時候,應該把不同入口之間,共同引用的模塊,抽離出來,放到一個公共模塊中。這樣不管這個模塊被多少個入口引用,都只會在最終打包結果中出現一次。——解決代碼冗餘。
    • 另外,當我們把這些共同引用的模塊都堆在一個模塊中,這個文件可能異常巨大,也是不利於網絡請求和頁面加載的。所以我們需要把這個公共模塊再按照一定規則進一步拆分成幾個模塊文件。——減小文件體積。
    • 至於如何拆分,方式因人而異,因項目而異。我個人的拆分原則是:
      • 業務代碼和第三方庫分離打包,實現代碼分割;
      • 業務代碼中的公共業務模塊提取打包到一個模塊;
      • 第三方庫最好也不要全部打包到一個文件中,因為第三方庫加起來通常會很大,我會把一些特別大的庫分別獨立打包,剩下的加起來如果還很大,就把它按照一定大小切割成若干模塊。

    optimization.splitChunks

    webpack提供了一個非常好的內置插件幫我們實現這一需求:CommonsChunkPlugin。不過在 webpack4 中CommonsChunkPlugin被刪除,取而代之的是optimization.splitChunks, 所幸的是optimization.splitChunks更強大!

    三、 實現

    通過一個多頁應用的小demo,我們一步一步來實現上述思路的配置。

    demo目錄結構:

    |--public/
    |   |--a.html
    |   |--index.html
    |--src/
    |   |--a.js
    |   |--b.js
    |   |--c.js
    |   |--index.js
    |--package.json
    |--webpack.config.js

    代碼邏輯很簡單,index模塊中引用了 ab 2個模塊,a 模塊中引用了 c 模塊和 jquery庫,b 模塊中也引用了 c 模塊和 jquery 庫,c 是一個獨立的模塊沒有其他依賴。

    index.js代碼如下:

    //index.js
    import a from './a.js';
    import b from './b.js';
    function fn() {
        console.log('index-------');
    }
    fn();

    a.js代碼如下:

    //a.js
    require('./c.js');
    const $ = require('jquery')
    function fn() {
        console.log('a-------');
    }
    module.exports = fn();

    b.js代碼如下:

    //b.js
    require('./c.js');
    const $ = require('jquery')
    function fn() {
        console.log('b-------');
    }
    module.exports = fn();

    c.js代碼如下:

    //c.js
    function fn() {
        console.log('c-------');
    }
    module.exports = fn();

    1. 基本配置

    webpack先不做優化,只做基本配置,看看效果。項目配置了2個入口,搭配html-webpack-plugin實現多頁打包:

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
        entry: {
            index: './src/index.js',
            a: './src/a.js'
        },
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: '[name].js'
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: './public/index.html',
                filename: 'index.html'
            }),
            new HtmlWebpackPlugin({
                template: './public/a.html',
                filename: 'a.html'
            })
        ]
    }

    在開發模式下運行webpack:

    可以看到,打包出兩個html和兩個體積很大的(300多K)的文件a.js,index.js

    進入dist目錄檢查js文件:

    • a.js里包含c模塊代碼和jquery代碼
    • index.js里包含a模塊、b模塊、c模塊和jquery代碼

    看,同樣的代碼cjquery被打包了2遍。

    2. 初步添加splitChunks優化配置

    首先解決相同代碼打包2次的問題,我們需要讓webpack把cjquery提取出來打包為公共模塊。

    在webpack配置文件添加splitChunks:

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial'
                }
            }
        }
    }

    – cacheGroups

    • cacheGroupssplitChunks配置的核心,對代碼的拆分規則全在cacheGroups緩存組裡配置。
    • 緩存組的每一個屬性都是一個配置規則,我這裏給他的default屬性進行了配置,屬性名可以不叫default可以自己定。
    • 屬性的值是一個對象,裏面放的我們對一個代碼拆分規則的描述。

    – name

    • name:提取出來的公共模塊將會以這個來命名,可以不配置,如果不配置,就會生成默認的文件名,大致格式是index~a.js這樣的。

    – chunks

    • chunks:指定哪些類型的chunk參与拆分,值可以是string可以是函數。如果是string,可以是這個三個值之一:all, async, initialall 代表所有模塊,async代表只管異步加載的, initial代表初始化時就能獲取的模塊。如果是函數,則可以根據chunk參數的name等屬性進行更細緻的篩選。

    再次打包:

    可以看到a.js,index.js從300多K減少到6點幾K。同時增加了一個common.js文件,並且兩個打包入口都自動添加了common.js這個公共模塊:

    進入dist目錄,依次查看這3個js文件:

    • a.js里不包含任何模塊的代碼了,只有webpack生成的默認代碼。
    • index.js里同樣不包含任何模塊的代碼了,只有webpack生成的默認代碼。
    • common.js里有a,b,c,index,jquery代碼。

    發現,提是提取了,但是似乎跟我們預料的不太一樣,所有的模塊都跑到common.js里去了。

    這是因為我們沒有告訴webpack(splitChunks)什麼樣的代碼為公共代碼,splitChunks默認任何模塊都會被提取。

    – minChunks

    splitChunks是自帶默認配置的,而緩存組默認會繼承這些配置,其中有個minChunks屬性:

    • 它控制的是每個模塊什麼時候被抽離出去:當模塊被不同entry引用的次數大於等於這個配置值時,才會被抽離出去。
    • 它的默認值是1。也就是任何模塊都會被抽離出去(入口模塊其實也會被webpack引入一次)。

    我們上面沒有配置minChunks,只配置了namechunk兩個屬性,所以minChunks的默認值 1 生效。也難怪所有的模塊都被抽離到common.js中了。

    優化一下,在緩存組裡配置minChunks覆蓋默認值:

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial',
                    minChunks: 2  //模塊被引用2次以上的才抽離
                }
            }
        }
    }

    然後運行webpack

    可以看到有2個文件的大小發生了變化:common.js由314K減小到311K,index.js由6.22K增大到7.56K。

    進入dist目錄查看:

    • a.js里依然不包含任何模塊的代碼(正常,因為a作為模塊被index引入了一次,又作為入口被webpack引入了一次,所以a是有2次引用的)。
    • index.js里出現了bindex模塊的代碼了。
    • common.js里只剩a,c,和jquery模塊的代碼。

    現在我們把共同引用的模塊a, c, jquery,從aindex這兩個入口模塊里抽取到common.js里了。有點符合我們的預期了。

    3. 配置多個拆分規則

    3.1 實現代碼分離,拆分第三方庫

    接下來,我希望公共模塊common.js中,業務代碼和第三方模塊jquery能夠剝離開來。

    我們需要再添加一個拆分規則。

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            minSize: 30,  //提取出的chunk的最小大小
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial',
                    minChunks: 2,  //模塊被引用2次以上的才抽離
                    priority: -20
                },
                vendors: {  //拆分第三方庫(通過npm|yarn安裝的庫)
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendor',
                    chunks: 'initial',
                    priority: -10
                }
            }
        }
    }

    我給cacheGroups添加了一個vendors屬性(屬性名可以自己取,只要不跟緩存組下其他定義過的屬性同名就行,否則後面的拆分規則會把前面的配置覆蓋掉)。

    – minSize

    minSize設置的是生成文件的最小大小,單位是字節。如果一個模塊符合之前所說的拆分規則,但是如果提取出來最後生成文件大小比minSize要小,那它仍然不會被提取出來。這個屬性可以在每個緩存組屬性中設置,也可以在splitChunks屬性中設置,這樣在每個緩存組都會繼承這個配置。這裏由於我的demo中文件非常小,為了演示效果,我把minSize設置為30字節,好讓公共模塊可以被提取出來,正常項目中不用設這麼小。

    – priority

    priority屬性的值為数字,可以為負數。作用是當緩存組中設置有多個拆分規則,而某個模塊同時符合好幾個規則的時候,則需要通過優先級屬性priority來決定使用哪個拆分規則。優先級高者執行。我這裏給業務代碼組設置的優先級為-20,給第三方庫組設置的優先級為-10,這樣當一個第三方庫被引用超過2次的時候,就不會打包到業務模塊里了。

    – test

    test屬性用於進一步控制緩存組選擇的模塊,與chunks屬性的作用有一點像,但是維度不一樣。test的值可以是一個正則表達式,也可以是一個函數。它可以匹配模塊的絕對資源路徑或chunk名稱,匹配chunk名稱時,將選擇chunk中的所有模塊。我這裏用了一個正則/[\\/]node_modules[\\/]/來匹配第三方模塊的絕對路徑,因為通過npm或者yarn安裝的模塊,都會存放在node_modules目錄下。

    運行一下webpack:

    可以看到新產生了一個叫vendor.js的文件(name屬性的值),同時common.js文件體積由原來的311k減少到了861bytes!

    進入dist目錄,檢查js文件:

    • a.js里不包含任何模塊代碼。
    • common.js只包含ac模塊的代碼。
    • index.js只包含bindex模塊的代碼。
    • vendor.js只包含jquery模塊的代碼。

    現在,我們在上一步的基礎上,成功從common.js里把第三方庫jquery抽離出來放到了vendor.js里。

    3.2 拆分指定文件

    如果我們還想把項目中的某一些文件單獨拎出來打包(比如工程本地開發的組件庫),可以繼續添加拆分規則。比如我的src下有個locallib.js文件要單獨打包,假設a.js中引入了它。

    //a.js
    require('./c.js');
    require('./locallib.js');  //引入自己本地的庫
    const $ = require('jquery')
    function fn() {
        console.log('a-------');
    }
    module.exports = fn();

    可以這麼配置:

    //webpack.config.js
    
    optimization: {
        splitChunks: {
            minSize: 30,  //提取出的chunk的最小大小
            cacheGroups: {
                default: {
                    name: 'common',
                    chunks: 'initial',
                    minChunks: 2,  //模塊被引用2次以上的才抽離
                    priority: -20
                },
                vendors: {  //拆分第三方庫(通過npm|yarn安裝的庫)
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendor',
                    chunks: 'initial',
                    priority: -10
                },
                locallib: {  //拆分指定文件
                    test: /(src\/locallib\.js)$/,
                    name: 'locallib',
                    chunks: 'initial',
                    priority: -9
                }
            }
        }
    }

    我在緩存組下又新增了一個拆分規則,通過test正則指定我就要單獨打包src/locallib.js文件,並且把優先級設置為-9,這樣當它被多次引用時,不會進入其他拆分規則組,因為另外兩個規則的優先級都比它要低。

    運行webpack打包后:

    可以看到新產生了一個locallib.js文件。進入dist目錄查看:

    • a.js里不包含任何模塊代碼。
    • common.js只包含ac模塊的代碼。
    • index.js只包含bindex模塊的代碼。
    • vendor.js只包含jquery模塊的代碼。
    • locallib.js里只包含locallib模塊的代碼。

    現在我們又在上一步的基礎上獨立打包了一個指定的模塊locallib.js

    至此,我們就成功實現了抽離公共模塊、業務代碼和第三方代碼剝離、獨立打包指定模塊。

    對比一下,優化前,打包出來js一共有633KB:

    優化后,打包出來js一共不到330KB:

    優化打包后的文件分類清晰,體積比優化前縮小了幾乎50%,有點小完美是不是!擊掌!這還只是我舉的一個簡單例子,在實際多頁應用中,優化力度說不定還不止這麼多。

    小結

    webpack很強大,以上只是冰山一角,但是只要掌握了上述optimization.splitChunks的核心配置,我們就可以幾乎隨心所欲地按照自己的想法來拆分優化代碼控制打包文件了,是不是很酷?玩轉代碼拆分,你也可以!

    如果覺得這些依然不能滿足你的需求,還想更精(bian)細(tai)地定製打包規則,可以到查看optimization.splitChunks的更多配置。

    歡迎交流~

    本文的完整webpack配置和demo源碼可以在這裏獲取:

    歡迎轉載,轉載請註明出處:

    本文同步發表於:

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

    【其他文章推薦】

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

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

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

    台灣寄大陸海運貨物規則及重量限制?

    大陸寄台灣海運費用試算一覽表