繼法國之後,英國政府宣布2040 年禁售汽柴油車

汽柴油車真的要走入歷史了嗎?繼法國宣布2040 年禁售汽柴油車後,英國政府為解決空氣污染問題,準備從30 億英鎊抗空汙的資金中提撥2.55 億英鎊協助委員會加快地方措施,以應對柴油車輛的污染,終極目標也將在2040 年前禁售汽柴油車。

英國獨立報(The Independent) 報導,空氣污染與英國每年約4 萬人過早死亡有關,運輸也佔溫室氣體排放量的很大一部分。英國政府先前推出的抗空汙計畫版本被環保人士反對,認為力道太弱,無法達到歐盟的排放標準,英國最高法院要求7 月31 日前英國政府必須制定新的計畫,以降低有害二氧化氮的排放量,而就在法院規定的截止日前,英國政府宣布2040 年汽柴油車禁令。

英國追隨法國腳步頒布禁售令,顯示向電動車轉型的速度正在加快,BMW 宣布計畫推出Mini 電動車版,將在英國牛津進行組裝,Volvo 也宣布清潔能源車計畫。

英國政府還將討論柴油車報廢計劃的執行細節。英國環保倡議者認為,這項計劃應包括政府資助且強制性的清潔空氣區,對進入高空氣污染地區的高污染車輛收取費用。對清潔空氣區設立收費制度被視為是最有效打擊二氧化氮污染的政策,而柴油車是排量二氧化氮的禍首。

但是英國政府對此有疑慮,認為這是懲罰柴油車駕駛,畢竟原本認為柴油車的碳排量比汽車少,因此鼓勵消費者購買。英國政府傾向改裝巴士等交通工具,降低排放量,或改變道路佈局,甚至改變速度和重新編排交通號誌等功能,使交通流量更加順暢,減少污染。

英國政府發言人表示,不該責怪柴油車駕駛,為了幫助他們改用清潔車,政府會討論針對性的報廢計劃,支持受本地計劃影響的駕駛。但英國在野黨不滿意這種溫和的作風,呼籲柴油車禁售令應該提前至2025 年,並提出廢止計劃幫駕駛轉換成更環保的車輛。

印度2030 年實現全電動車目標

除歐洲國家之外,受嚴重空汙困擾的印度動作也非常積極,印度政府計畫2030 年實現全電動車目標,而印度政府此舉並不只是為了抗空汙,還可減少燃料進口費用。印度重工業部和印度國家研究院正在製定促進電動汽車發展政策,主要是朝降低成本提高價格誘因做起,現在印度對混合動力車與電動車提供補貼,初期也會補貼業者度過轉型期,為2030 年禁售汽柴油車鋪路。

未來3 年印度將大規模佈建充電基礎設施與電池交換計畫,目前只有印度只有電動車廠  Mahindra Electric 在印度提供全電動車,最近Anand Mahindra 執行長在社群媒體上邀請Tesla 到印度設置商店,Tesla 將在2017 年底之前進入印度市場,開放印度消費者訂購Model 3,明年可能開始展店。

除了電動汽車,混合動力汽車市場處於更好的市場位置,豐田、Volvo 和BMW 等幾家製造商在印度提供混合動力車或插電式混合動力車。特斯拉和日產也宣布進在印度市場堆出Model 3和Leaf。若印度政府的計劃順利進行,印度將成為全球電動車品牌的一級戰場。

(合作媒體:。圖片出處:public domain CC0)

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

【其他文章推薦】

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

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

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

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

java中的string對象深入了解

這裏來對Java中的String對象做一個稍微深入的了解。

Java對象實現的演進

String對象是Java中使用最頻繁的對象之一,所以Java開發者們也在不斷地對String對象的實現進行優化,以便提升String對象的性能。

Java6以及之前版本中String對象的屬性

在Java6以及之前版本中,String對象是對char數組進行了封裝實現的對象,其主要有4個成員成員變量,分別是char數組、偏移量offset、字符數量count和哈希值hash。String對象是通過offset和count兩個屬性來定位char[]數組,獲取字符串。這樣做可以高效、快速地共享數組對象,同時節省內存空間,但是這種方式卻可能會導致內存泄漏的發生。

Java7、8版本中String對象的屬性

從Java7版本開始,Java對String類做了一些改變,具體是String類不再有offset和count兩個變量了。這樣做的好處是String對象佔用的內存稍微少了點,同時String.substring()方法也不再共享char[]了,從而解決了使用該方法可能導致的內存泄漏問題。

Java9以及之後版本中String對象的屬性

從Java9版本開始,Java將char[]數組改為了byte[]數組。我們都知道,char是兩個字節的,如果用來存一個字節的情況下就會造成內存空間的浪費。而為了節約這一個字節的空間,Java開發者就改成了一個使用一個字節的byte來存儲字符串。

另外,在Java9中,String對象維護了一個新的屬性coder,這個屬性是編碼格式的標識,在計算字符串長度或者調用indexOf()方法的時候,會需要根據這個字段去判斷如何計算字符串長度。coder屬性默認有0和1兩個值,其中0代表Latin-1(單字節編碼),1則表示UTF-16編碼。

String對象的創建方式與在內存中的存放

在Java中,對於基本數據類型的變量和對對象的引用,保存在棧內存的局部變量表中;而通過new關鍵字和Constructor創建的對象,則是保存在堆內存中。而String對象的創建方式一般為兩種,一種是字面量(字符串常量)的方式,一種則是構造函數(String())的方式,兩種方式在內存中的存放有所不同。

字面量(字符串常量)的創建方式

使用字面量的方式創建字符串時,JVM會在字符串常量池中先檢查是否存在該字面量,如果存在,則返回該字面量在內存中的引用地址;如果不存在,則在字符串常量池中創建該字面量並返回引用。使用這種方式創建的好處是避免了相同值的字符串在內存中被重複創建,節約了內存,同時這種寫法也會比較簡單易讀一些。

String str = "i like yanggb.";

字符串常量池

這裏要特別說明一下常量池。常量池是JVM為了減少字符串對象的重複創建,特別維護了一個特殊的內存,這段內存被稱為字符串常量池或者字符串字面量池。在JDK1.6以及之前的版本中,運行時常量池是在方法區中的。在JDK1.7以及之後版本的JVM,已經將運行時常量池從方法區中移了出來,在Java堆(Heap)中開闢了一塊區域用來存放運行時常量池。而從JDK1.8開始,JVM取消了Java方法區,取而代之的是位於直接內存的元空間(MetaSpace)。總結就是,目前的字符串常量池在堆中。

我們所知道的幾個String對象的特點都來源於String常量池。

1.在常量池中會共享所有的String對象,因此String對象是不可被修改的,因為一旦被修改,就會導致所有引用此String對象的變量都隨之改變(引用改變),所以String對象是被設計為不可修改的,後面會對這個不可變的特性做一個深入的了解。

2.String對象拼接字符串的性能較差的說法也是來源於此,因為String對象不可變的特性,每次修改(這裡是拼接)都是返回一個新的字符串對象,而不是再原有的字符串對象上做修改,因此創建新的String對象會消耗較多的性能(開闢另外的內存空間)。

3.因為常量池中創建的String對象是共享的,因此使用雙引號聲明的String對象(字面量)會直接存儲在常量池中,如果該字面量在之前已存在,則是會直接引用已存在的String對象,這一點在上面已經描述過了,這裏再次提及,是為了特別說明這一做法保證了在常量池中的每個String對象都是唯一的,也就達到了節約內存的目的。

構造函數(String())的創建方式

使用構造函數的方式創建字符串時,JVM同樣會在字符串常量池中先檢查是否存在該字面量,只是檢查后的情況會和使用字面量創建的方式有所不同。如果存在,則會在堆中另外創建一個String對象,然後在這個String對象的內部引用該字面量,最後返回該String對象在內存地址中的引用;如果不存在,則會先在字符串常量池中創建該字面量,然後再在堆中創建一個String對象,然後再在這個String對象的內部引用該字面量,最後返回該String對象的引用。

String str = new String("i like yanggb.");

這就意味着,只要使用這種方式,構造函數都會另行在堆內存中開闢空間,創建一個新的String對象。具體的理解是,在字符串常量池中不存在對應的字面量的情況下,new String()會創建兩個對象,一個放入常量池中(字面量),一個放入堆內存中(字符串對象)。

String對象的比較

比較兩個String對象是否相等,通常是有【==】和【equals()】兩個方法。

在基本數據類型中,只可以使用【==】,也就是比較他們的值是否相同;而對於對象(包括String)來說,【==】比較的是地址是否相同,【equals()】才是比較他們內容是否相同;而equals()是Object都擁有的一個函數,本身就要求對內部值進行比較。

String str = "i like yanggb.";
String str1 = new String("i like yanggb.");

System.out.println(str == str1); // false
System.out.println(str.equals(str1)); // true

因為使用字面量方式創建的String對象和使用構造函數方式創建的String對象的內存地址是不同的,但是其中的內容卻是相同的,也就導致了上面的結果。

String對象中的intern()方法

我們都知道,String對象中有很多實用的方法。為什麼其他的方法都不說,這裏要特別說明這個intern()方法呢,因為其中的這個intern()方法最為特殊。它的特殊性在於,這個方法在業務場景中幾乎用不上,它的存在就是在為難程序員的,也可以說是為了幫助程序員了解JVM的內存結構而存在的(?我信你個鬼,你個糟老頭子壞得很)。

/**
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
**/
public native String intern();

上面是源碼中的intern()方法的官方註釋說明,大概意思就是intern()方法用來返回常量池中的某字符串,如果常量池中已經存在該字符串,則直接返回常量池中該對象的引用。否則,在常量池中加入該對象,然後返回引用。然後我們可以從方法簽名上看出intern()方法是一個native方法。

下面通過幾個例子來詳細了解下intern()方法的用法。

第一個例子

String str1 = new String("1");
System.out.println(str1 == str1.intern()); // false
System.out.println(str1 == "1"); // false

在上面的例子中,intern()方法返回的是常量池中的引用,而str1保存的是堆中對象的引用,因此兩個打印語句的結果都是false。

第二個例子

String str2 = new String("2") + new String("3");
System.out.println(str2 == str2.intern()); // true
System.out.println(str2 == "23"); // true

在上面的例子中,str2保存的是堆中一個String對象的引用,這和JVM對【+】的優化有關。實際上,在給str2賦值的第一條語句中,創建了3個對象,分別是在字符串常量池中創建的2和3、還有在堆中創建的字符串對象23。因為字符串常量池中不存在字符串對象23,所以這裏要特別注意:intern()方法在將堆中存在的字符串對象加入常量池的時候採取了一種截然不同的處理方案——不是在常量池中建立字面量,而是直接將該String對象自身的引用複製到常量池中,即常量池中保存的是堆中已存在的字符串對象的引用。根據前面的說法,這時候調用intern()方法,就會在字符串常量池中複製出一個對堆中已存在的字符串常量的引用,然後返回對字符串常量池中這個對堆中已存在的字符串常量池的引用的引用(就是那麼繞,你來咬我呀)。這樣,在調用intern()方法結束之後,返回結果的就是對堆中該String對象的引用,這時候使用【==】去比較,返回的結果就是true了。同樣的,常量池中的字面量23也不是真正意義的字面量23了,它真正的身份是堆中的那個String對象23。這樣的話,使用【==】去比較字面量23和str2,結果也就是true了。

第三個例子

String str4 = "45";
String str3 = new String("4") + new String("5");
System.out.println(str3 == str3.intern()); // false
System.out.println(str3 == "45"); // false

這個例子乍然看起來好像比前面的例子還要複雜,實際上卻和上面的第一個例子是一樣的,最難理解的反而是第二個例子。

所以這裏就不多說了,而至於為什麼還要舉這個例子,我相信聰明的你一下子就明白了(我有醫保,你來打我呀)。

String對象的不可變性

先來看String對象的一段源碼。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
}

從類簽名上來看,String類用了final修飾符,這就意味着這個類是不能被繼承的,這是決定String對象不可變特性的第一點。從類中的數組char[] value來看,這個類成員變量被private和final修飾符修飾,這就意味着其數值一旦被初始化之後就不能再被更改了,這是決定String對象不可變特性的第二點。

Java開發者為什麼要將String對象設置為不可變的,主要可以從以下三個方面去考慮:

1.安全性。假設String對象是可變的,那麼String對象將可能被惡意修改。

2.唯一性。這個做法可以保證hash屬性值不會頻繁變更,也就確保了唯一性,使得類似HashMap的容器才能實現相應的key-value緩存功能。

3.功能性。可以實現字符串常量池(究竟是先有設計,還是先有實現呢)。

String對象的優化

字符串是常用的Java類型之一,所以對字符串的操作是避免不了的。而在對字符串的操作過程中,如果使用不當的話,性能可能會有天差地別,所以有一些地方是要注意一下的。

拼接字符串的性能優化

字符串的拼接是對字符串的操作中最頻繁的一個使用。由於我們都知道了String對象的不可變性,所以我們在開發過程中要盡量減少使用【+】進行字符串拼接操作。這是因為使用【+】進行字符串拼接,會在得到最終想要的結果前產生很多無用的對象。

String str = 'i';
str = str + ' ';
str = str + 'like';
str = str + ' ';
str = str + 'yanggb';
str = str + '.';

System.out.println(str); // i like yanggb.

事實上,如果我們使用的是比較智能的IDE編寫代碼的話,編譯器是會提示將代碼優化成使用StringBuilder或者StringBuffer對象來優化字符串的拼接性能的,因為StringBuilder和StringBuffer都是可變對象,也就避免了過程中產生無用的對象了。而這兩種替代方案的區別是,在需要線程安全的情況下,選用StringBuffer對象,這個對象是支持線程安全的;而在不需要線程安全的情況下,選用StringBuilder對象,因為StringBuilder對象的性能在這種場景下,要比StringBuffer對象或String對象要好得多。

使用intern()方法優化內存佔用

前面吐槽了intern()方法在實際開發中沒什麼用,這裏又來說使用intern()方法來優化內存佔用了,這人真的是,嘿嘿,真香。關於方法的使用就不說了,上面有詳盡的用法說明,這裏來說說具體的應用場景好了。有一位Twitter的工程師在Qcon全球軟件開發大會上分享了一個他們對String對象優化的案例,他們利用了這個String.intern()方法將以前需要20G內存存儲優化到只需要幾百兆內存。具體就是,使用intern()方法將原本需要創建到堆內存中的String對象都放到常量池中,因為常量池的不重複特性(存在則返回引用),也就避免了大量的重複String對象造成的內存浪費問題。

什麼,要我給intern()方法道歉?不可能。String.intern()方法雖好,但是也是需要結合場景來使用的,並不能夠亂用。因為實際上,常量池的實現是類似於一個HashTable的實現方式,而HashTable存儲的數據越大,遍歷的時間複雜度就會增加。這就意味着,如果數據過大的話,整個字符串常量池的負擔就會大大增加,有可能性能不會得到提升卻反而有所下降。

字符串分割的性能優化

字符串的分割是字符串操作的常用操作之一,對於字符串的分割,大部分人使用的都是split()方法,split()方法在大部分場景下接收的參數都是正則表達式,這種分割方式本身沒有什麼問題,但是由於正則表達式的性能是非常不穩定的,使用不恰當的話可能會引起回溯問題並導致CPU的佔用居高不下。在以下兩種情況下split()方法不會使用正則表達式:

1.傳入的參數長度為1,且不包含“.$|()[{^?*+\”regex元字符的情況下,不會使用正則表達式。

2.傳入的參數長度為2,第一個字符是反斜杠,並且第二個字符不是ASCII数字或ASCII字母的情況下,不會使用正則表達式。

所以我們在字符串分割時,應該慎重使用split()方法,而首先考慮使用String.indexOf()方法來進行字符串分割,在String.indexOf()無法滿足分割要求的時候再使用Split()方法。而在使用split()方法分割字符串時,需要格外注意回溯問題。

總結

雖然說在不了解String對象的情況下也能使用String對象進行開發,但是了解String對象可以幫助我們寫出更好的代碼。

 

“只希望在故事的最後,我還是我,你也還是你。”

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

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

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

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

Freemarker + xml 實現Java導出word

前言

最近做了一個調查問卷導出的功能,需求是將維護的題目,答案,導出成word,參考了幾種方案之後,選擇功能強大的freemarker+固定格式之後的wordxml實現導出功能。導出word的代碼是可以直接復用的,於是在此貼出,並進行總結,方便大家拿走。

實現過程概覽

先在word上,調整好自己想要的樣子。然後存為xml文件。保存為freemarker模板,以ftl後綴結尾。將需要替換的變量使用freemarker的語法進行替換。最終將數據準備好,和模板進行渲染,生成文件並返回給瀏覽器流。

詳細的實現過程

準備好word的樣式

我們新建一個word,我們應該使用Microsoft office,如果使用wps可能會造成樣式有些不兼容。在新建的office中,設置好我們的表格樣式。我們的調查問卷涉及到四種類型,單選,多選,填空,簡答。我們做出四種類型的示例。

樣式沒有問題后,我們選擇另存為word xml 2003版本。將會生成一個xml文件。

格式化xml,並用freemarker語法替換xml

我們可以先下載一個工具 firstobject xml editor,這個可以幫助我們查看xml,同時方便我們定位我們需要改的位置。
複製過去之後,按f8可以將其進行格式化,左側是標籤,右側是內容,我們只需要關注w:body即可。

像右側的調查問卷這個就是個標題,我們實際渲染的時候應該將其進行替換,比如我們的程序數據map中,有title屬性,我們想要這裏展示,我們就使用語法${title}即可。

freemarker的具體語法,可以參考freemarker的問題,在這裏我給出幾個簡單的例子。
比如我們將所有的數據放置在dataList中,所以我們需要判斷,dataList是不是空,是空,我們不應該進行下面的邏輯,不是空,我們應該先循環題目是必須的,答案是需要根據類型進行再次循環的。語法參考文檔,這裏不再贅述。

程序端引入freemarker

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
</dependency>

將我們的flt文件放在resources下的templates下。

後端代碼實現

此代碼可以復用,在此貼出

public class WordUtils {

    private static Configuration configuration = null;
    private static final String templateFolder = WordUtils.class.getClassLoader().getResource("").getPath()+"/templates/word";
    static {
        configuration = new Configuration();
        configuration.setDefaultEncoding("utf-8");
        try {
            configuration.setDirectoryForTemplateLoading(new File(templateFolder));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     *  @Description:導出word,傳入request,response,map就是值,title是導出問卷名,ftl是你要使用的模板名
     */
    public static void exportWord(HttpServletRequest request, HttpServletResponse response, Map map, String title, String ftlFile) throws Exception {
        Template freemarkerTemplate = configuration.getTemplate(ftlFile);
        File file = null;
        InputStream fin = null;
        ServletOutputStream out = null;
        try {
            file = createDocFile(map,freemarkerTemplate);
            fin = new FileInputStream(file);
            String fileName = title + ".doc";
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/msword");
            response.setHeader("Content-Disposition", "attachment;filename="
             +fileName);
            out = response.getOutputStream();
            byte[] buffer = new byte[512];  
            int bytesToRead = -1;
            while((bytesToRead = fin.read(buffer)) != -1) {
                out.write(buffer, 0, bytesToRead);
            }
        }finally {
            if(fin != null) fin.close();
            if(out != null) out.close();
            if(file != null) file.delete(); 
        }
    }

    /**
     *  @Description:創建doc文件
     */
    private static File createDocFile(Map<?, ?> dataMap, Template template) {
        File file = new File("init.doc");
        try {
            Writer writer = new OutputStreamWriter(new FileOutputStream(file), "utf-8");
            template.process(dataMap, writer);
            writer.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return file;
    }

}

有了工具類后,我們準備好我們的map數據。map裏面的數據大家可以自行定義。然後調用utils中的導出方法即可。

WordUtils.exportWord(request, response, dataMap, "word", "demo.ftl");

結語

至此已經結束了,十分的好用,有疑問的話,可以評論交流。

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

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

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

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

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

軟件架構模式

閱讀也花了較長的時間,大致也了解到整潔的架構要做到以下兩點:

  • well-isolated components:component是獨立部署的最小單元,由一系列遵循SOLID原則的module按照REP、CCP、CEP原則組成。
  • dependency rule:低層的detail去依賴高層的police

但感覺並沒有對架構設計給出可行的參考。

clean architecture 中的架構實例

在的第34章 “The Missing Chapter”(由 Simon Brown 編寫)給出了一個具體的案例,用四種架構設計來實現一個 “online book store”。

package by layer

這是最常見的方案,從前往後分為:前端、後台(business logic)、持久化DB。

優點是:簡單、容易上手,符合大多數公司的組織架構。

存在的問題:

  • 軟件規模和複雜度增加時,三層架構就不夠了,需要重新考慮拆分;
  • 分層架構體現不出business domain;

PACKAGE BY FEATURE

垂直切分方案,所有的java代碼都放在一個package裏面

好處在於凸顯domain concept

PORTS AND ADAPTERS

clean architecture這本書推薦的方案, 外層依賴於內層的domain

PACKAGE BY COMPONENT

本章作者 Simon Brown 提出的方案,service-centric view,將所有相關的職責打包稱一個粗粒度的Jar包

bundling all of the responsibilities related to a single coarse-grained component into a single Java package

看起來類似現在微服務的部署方式

對於以上四種結構,依賴關係看起來是這樣的

值得注意的是

  • 虛線箭頭表示component之間的依賴關係
  • PORTS AND ADAPTERS這種架構更能體現domain(business logic),即接口命名為Orders而不是OrdersRepository

本章的作者最後還指出:++不管架構怎麼設計,粗心的implementation都可能違背最初的設計;依賴編譯器來保證架構的一以貫之,而不是自我約束或者事後檢查。++

五種常見架構模式

看完了clean architecture后,在網上搜索架構設計相關的書籍,發現了這本小冊子,篇幅很短,稱不上book,而是一個report。

指出缺乏架構設計的軟件往往高度耦合,難以改變。因此,這本小冊子的目標就是介紹常用架構模式的特點、優點、缺點,幫助我們針對特定的業務需求做出合適的選擇。

Layered Architecture

分層架構也稱為n-tire architecture,這是最為常見的一種架構模式,一般從前往後分為四層:presentation, business, persistence, and database。如下圖所示:

分層架構一般是一個新系統的最佳首選,因為其完美匹配傳統IT公司組織架構:一般的公司招人都是前端、後端、數據庫。

分層架構的優點在於關注點隔離(separation of concerns),每一層做好自己這一層的職責,並向上一層提供服務即可,最為經典的案例就是七層網絡模型,這有利於開發、測試、管理與維護。

分層架構中,需要注意的是兩個概念:closed layeropen layer

closed layer的核心就是不要越層訪問,比如在上圖中,Presentation Layer就不應該跨國Business Layer直接去Persistence Layer訪問數據。

A closed layer means that as a request moves from layer to layer, it must go through the layer right below it to get to the next layer below that one

closed layer保證了層隔離( layers of isolation),使得某一層的修改影響的範圍盡可能小,比較可控。但closed layer有時候也會帶來一個問題:architecture sinkhole anti pattern(污水池反模式),具體是指,為了查簡單數據,層層轉發請求。比如為了在展示層显示玩家的某個數據,需要通過業務層、再到持久化層、再到DB層;取到數據再一層層傳遞迴來,在這個過程中,業務層並沒有對數據有邏輯上的處理。

显示,污水池反模式衝擊了closed layer的美好想法。如何衡量這種層層轉發的請求是不是問題,可以參考80-20法則。

如果80%是附帶邏輯的,那麼就是ok的,但如果有80% 是 simple passthrough processing,那麼就得考慮讓某些layer open。比如在複雜的業務系統中, 經常會有一些可復用的邏輯,這個時候會抽取為通用的服務層(service layer)。如下圖所示

open layer 、close layer的概念可以幫助理清楚架構和請求流程之間的關係,讓架構師、程序員都清楚架構的邊界(boundary)在哪裡,重要的是,這個open-closed關係需要明確的文檔化,不要隨意打破,否則就會一團糟。

Event-Driven Architecture

The event-driven architecture pattern is a popular distributed asynchronous architecture pattern used to produce highly scalable applications.

從上述定義可以看出事件驅動架構的幾個特點:分佈式、異步、可伸縮。其核心是:高度解耦合、專一職責的事件處理單元(Event Processor)

事件驅動架構有兩種常見拓撲結構: the mediator and the broker.

Mediator Topology

需要一个中心化(全局唯一)的協調單元,用於組織一個事件中的多個步驟,這些步驟中有些是可并行的,有些必須是順序執行的,這就依賴Event Mediator的調度。如下圖所示

Broker Topology
這種是沒有中心的架構

the message flow is distributed across the event processor components in a chain-like fashion through a lightweight message broker

如下圖所示

事件驅動的好處在於,高度可伸縮、便於部署、整體性能較好(得益於某些事件的併發執行)。但由於其分佈式異步的本性,其缺點也很明顯:開發比較複雜、維護成本較高;而且很難支持事務,尤其是一個邏輯事件跨越多個processor的時候。

Microkernel Architecture

微內核架構又稱之為插件式架構(plug-in architecture)。如下圖所示:

微內核架構包含兩部分組件

  • a core system
  • plug-in modules.

plug-in modules 是相互獨立的組件,用於增加、擴展 core system 的功能。

這種架構非常適用於 product-based applications 即需要打包、下載、安裝的應用,比如桌面應用。最經典的例子就是Eclipse編輯器,玩遊戲的同學經常下載使用的MOD也可以看出插件。

微內核架構通常可以是其他架構的一部分,以實現特定部分的漸進式設計、增量開發

Microservices Architecture Pattern

微服務架構並不是為了解決新問題而發明的新架構,而是從分層架構的單體式應用和SOA(service-oriented architecture)演化而來。

微服務解決了分層架構潛在的成為單體式應用(Monolithic application)的問題:

through the development of continuous delivery, separating the application into multiple deployable units

同時,微服務還通過簡化(泛化)服務的概念,消除編排需求,簡化對服務組件的連接訪問。從而避免了SOA的各種缺點:複雜、昂貴、重度、難以理解和開發。

The microservices architecture style addresses this complexity by simplifying the notion of a service, eliminating orchestration needs, and simplifying connectivity and access to service components.

微服務架構如下:

其核心是service component,這些服務組件相互解耦,易於獨立開發、部署。服務組件的粒度是微服務架構中最難的挑戰

  • 太大:失去了微服務架構的優勢
  • 太小:導致需要編排,或者服務組件間的通信、事務。

而微服務架構相比SOA而言,其優勢就在於避免依賴和編排 — 編排引入大量的複雜工作。

對於單個請求 如果service之間還要通信,那麼可能是就是粒度過小。解決辦法:

  • 如果通信是為了訪問數據:那麼可以通過共享db解決
  • 如果通信是為了使用功能:那麼可以考慮代碼的冗餘,雖然這違背了DRY原則。在clean architecture中也指出,component的自完備性有時候要高於代碼復用。

Space-Based Architecture

基於空間的架構,其核心目標是解決由於數據庫瓶頸導致的低伸縮性、低併發問題。

分層架構中,在用戶規模激增的情況下,數據層的擴展往往會成為最後的瓶頸(相對而言,前端和業務邏輯都容易做成無狀態,比較好水平擴展)。而基於空間的架構的核心是內存複製,根本上解決了這個問題。

High scalability is achieved by removing the central database constraint and using replicated in-memory data grids instead

架構如下:

其核心組件包括

  • processing unit,處理單元,其內部又包含一下組成
    • business logic
    • in-memory data grid
    • an optional asynchronous persistent store for failover
    • replication engine,用於同步數據修改
  • virtualized middleware
    • Messaging Grid: 監控processing unit可用性,路由客戶端請求到processing unit
    • Data Grid: 核心,負責processingunit之間的數據同步,毫秒級同步?
    • Processing Grid: 可選組件,如果一個請求需要多個processing unit的服務,那麼負責協調分發
    • Deployment Manager: 負責processing unit的按需啟停

基於空間的架構很少見,而且從上面的核心組件描述來看的話,開發和維護應該都是比較負責的,由於是數據的同步這塊。而且由於數據都保存在內存中,那麼數據量就不能太大。

基於空間的架構適用於需求變化大的小型web應用,不適用於有大量數據操作的傳統大規模關係型數據庫應用

references

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

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

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

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

利用Python學習線性代數 — 1.1 線性方程組

利用Python學習線性代數 — 1.1 線性方程組

系列,

本節實現的主要功能函數,在源碼文件中,後續章節將作為基本功能調用。

線性方程

線性方程組由一個或多個線性方程組成,如
\[ \begin{array}\\ x_1 – 2 x_2 &= -1\\ -x_1 + 3 x_2 &= 3 \end{array} \]

求包含兩個變量兩個線性方程的方程組的解,等價於求兩條直線的交點。
這裏可以畫出書圖1-1和1-2的線性方程組的圖形。
通過改變線性方程的參數,觀察圖形,體會兩個方程對應直線平行、相交、重合三種可能。

那麼,怎麼畫二元線性方程的直線呢?

方法是這樣的:
假如方程是 \(a x_1 + b x_2 = c\) 的形式,可以寫成 \(x_2 = (c – a x_1) / b\)
在以 \(x_1\)\(x_2\)為兩個軸的直角坐標系中,\(x_1\)取一組值,如 \((-3, -2.9, -2.8, \dots, 2.9, 3.0)\)
計算相應的 \(x_2\),然後把所有點 \((x_1, x_2)\) 連起來成為一條線。
\(b\)\(0\) 時, 則在\(x_1 = c / a\)處畫一條垂直線。

# 引入Numpy和 Matplotlib庫
import numpy as np
from matplotlib import pyplot as plt

Matplotlib 是Python中使用較多的可視化庫,這裏只用到了它的一些基本功能。

def draw_line(a, b, c, start=-4, 
              stop=5, step=0.01):
    """根據線性方程參數繪製一條直線"""
    # 如果b為0,則畫一條垂線
    if np.isclose(b, 0):
        plt.vlines(start, stop, c / a)
    else: # 否則畫 y = (c - a*x) / b
        xs = np.arange(start, stop, step)
        plt.plot(xs, (c - a*xs)/b)
# 1.1 圖1-1
draw_line(1, -2, -1)
draw_line(-1, 3, 3)

def draw_lines(augmented, start=-4, 
              stop=5, step=0.01):
    """給定增廣矩陣,畫兩條線."""
    plt.figure()
    for equation in augmented:
        draw_line(*equation, start, stop, step)
    plt.show()
# Fig. 1-1
# 增廣矩陣用二維數組表示 
# [[1, -2, -1], [-1, 3, 3]]
# 這些数字對應圖1-1對應方程的各項係數
draw_lines([[1, -2, -1], [-1, 3, 3]])

# Fig. 1-2
draw_lines([[1, -2, -2], [-1, 2, 3]])
# Fig. 1-3
draw_lines([[1, -2, -1], [-1, 2, 1]])

  • 建議:改變這些係數,觀察直線,體會兩條直線相交、平行和重合的情況

例如

draw_lines([[1, -2, -2], [-1, 2, 9]])

如果對Numpy比較熟悉,則可以採用更簡潔的方式實現上述繪圖功能。
在計算多條直線方程時,可以利用向量編程的方式,用更少的代碼實現。

def draw_lines(augmented, start=-4, 
               stop=5, step=0.01):
    """Draw lines represented by augmented matrix on 2-d plane."""
    am = np.asarray(augmented)
    xs = np.arange(start, stop, step).reshape([1, -1])
    # 同時計算兩條直線的y值
    ys = (am[:, [-1]] - am[:, [1]]*xs) / am[:, [0]]
    for y in ys:
        plt.plot(xs[0], y)
    plt.show()

矩陣記號

矩陣是一個數表,在程序中通常用二維數組表示,例如

# 嵌套列表表示矩陣
matrix = [[1, -2, 1, 0],
          [0, 2, -8, 8],
          [5, 0, -5, 10]]
matrix
[[1, -2, 1, 0], [0, 2, -8, 8], [5, 0, -5, 10]]

實際工程和研究實踐中,往往會採用一些專門的數值計算庫,簡化和加速計算。
Numpy庫是Python中數值計算的常用庫。
在Numpy中,多維數組類型稱為ndarray,可以理解為n dimensional array。
例如

# Numpy ndarray 表示矩陣
matrix = np.array([[1, -2, 1, 0],
                    [0, 2, -8, 8],
                    [5, 0, -5, 10]])
matrix
array([[ 1, -2,  1,  0],
       [ 0,  2, -8,  8],
       [ 5,  0, -5, 10]])

解線性方程組

本節解線性方程組的方法是 高斯消元法,利用了三種基本行變換。

  1. 把某個方程換成它與另一個方程的倍數的和;
  2. 交換兩個方程的位置;
  3. 某個方程的所有項乘以一個非零項。

假設線性方程的增廣矩陣是\(A\),其第\(i\)\(j\)列的元素是\(a_{ij}\)
消元法的基本步驟是:

  • 增廣矩陣中有 \(n\) 行,該方法的每一步處理一行。
    1. 在第\(i\)步,該方法處理第\(i\)
      • \(a_{ii}\)為0,則在剩餘行 \(\{j| j \in (i, n]\}\)中選擇絕對值最大的行\(a_{ij}\)
        • \(a_{ij}\)為0,返回第1步。
        • 否則利用變換2,交換\(A\)的第\(i\)\(j\)行。
    2. 利用行變換3,第\(i\)行所有元素除以\(a_{ii}\),使第 \(i\) 個方程的第 \(i\)個 係數為1
    3. 利用行變換1,\(i\)之後的行減去第\(i\)行的倍數,使這些行的第 \(i\) 列為0

為了理解這些步驟的實現,這裏先按書中的例1一步步計算和展示,然後再總結成完整的函數。
例1的增廣矩陣是

\[ \left[ \begin{array} &1 & -2 & 1 & 0\\ 0 & 2 & -8 & 8\\ 5 & 0 & -5 & 10 \end{array} \right] \]

# 增廣矩陣
A = np.array([[1, -2, 1, 0],
              [0, 2, -8, 8],
              [5, 0, -5, 10]])
# 行號從0開始,處理第0行
i = 0
# 利用變換3,將第i行的 a_ii 轉成1。這裏a_00已經是1,所不用動
# 然後利用變換1,把第1行第0列,第2行第0列都減成0。
# 這裏僅需考慮i列之後的元素,因為i列之前的元素已經是0
#   即第1行減去第0行的0倍
#   而第2行減去第0行的5倍
A[i+1:, i:] = A[i+1:, i:] - A[i+1:, [i]] * A[i, i:]
A
array([[  1,  -2,   1,   0],
       [  0,   2,  -8,   8],
       [  0,  10, -10,  10]])
i = 1
# 利用變換3,將第i行的 a_ii 轉成1。
A[i] = A[i] / A[i, i]
A
array([[  1,  -2,   1,   0],
       [  0,   1,  -4,   4],
       [  0,  10, -10,  10]])
# 然後利用變換1,把第2行第i列減成0。
A[i+1:, i:] = A[i+1:, i:] - A[i+1:, [i]] * A[i, i:]
A
array([[  1,  -2,   1,   0],
       [  0,   1,  -4,   4],
       [  0,   0,  30, -30]])
i = 2
# 利用變換3,將第i行的 a_ii 轉成1。
A[i] = A[i] / A[i, i]
A
array([[ 1, -2,  1,  0],
       [ 0,  1, -4,  4],
       [ 0,  0,  1, -1]])

消元法的前向過程就結束了,我們可以總結成一個函數

def eliminate_forward(augmented): 
    """
    消元法的前向過程.
    
    返回行階梯形,以及先導元素的坐標(主元位置)
    """
    A = np.asarray(augmented, dtype=np.float64)
    # row number of the last row
    pivots = []
    i, j = 0, 0
    while i < A.shape[0] and j < A.shape[1]:
        A[i] = A[i] / A[i, j]
        if (i + 1) < A.shape[0]: # 除最後一行外
            A[i+1:, j:] = A[i+1:, j:] - A[i+1:, [j]] * A[i, j:]
        pivots.append((i, j))
        i += 1
        j += 1
    return A, pivots

這裡有兩個細節值得注意

  1. 先導元素 \(a_{ij}\),不一定是在主對角線位置,即 \(i\) 不一定等於\(j\).
  2. 最後一行只需要用變換3把先導元素轉為1,沒有剩餘行需要轉換
# 測試一個增廣矩陣,例1
A = np.array([[1, -2, 1, 0],
              [0, 2, -8, 8],
              [5, 0, -5, 10]])
A, pivots = eliminate_forward(A)
print(A)
print(pivots)
[[ 1. -2.  1.  0.]
 [ 0.  1. -4.  4.]
 [ 0.  0.  1. -1.]]
[(0, 0), (1, 1), (2, 2)]

消元法的後向過程則更簡單一些,對於每一個主元(這裏就是前面的\(a_{ii}\)),將其所在的列都用變換1,使其它行對應的列為0.

for i, j in reversed(pivots):
    A[:i, j:] = A[:i, j:] - A[[i], j:] * A[:i, [j]] 
A
array([[ 1.,  0.,  0.,  1.],
       [ 0.,  1.,  0.,  0.],
       [ 0.,  0.,  1., -1.]])
def eliminate_backward(simplified, pivots):
    """消元法的後向過程."""
    A = np.asarray(simplified)
    for i, j in reversed(pivots):
        A[:i, j:] = A[:i, j:] - A[[i], j:] * A[:i, [j]] 
    return A

至此,結合 eliminate_forward 和eliminate_backward函數,可以解形如例1的線性方程。

然而,存在如例3的線性方程,在eliminate_forward算法進行的某一步,主元為0,需要利用變換2交換兩行。
交換行時,可以選擇剩餘行中,選擇當前主元列不為0的任意行,與當前行交換。
這裏每次都採用剩餘行中,當前主元列絕對值最大的行。
補上行交換的前向過程函數如下

def eliminate_forward(augmented): 
    """消元法的前向過程"""
    A = np.asarray(augmented, dtype=np.float64)
    # row number of the last row
    pivots = []
    i, j = 0, 0
    while i < A.shape[0] and j < A.shape[1]:
        # if pivot is zero, exchange rows
        if np.isclose(A[i, j], 0):
            if (i + 1) < A.shape[0]:
                max_k = i + 1 + np.argmax(np.abs(A[i+1:, i]))
            if (i + 1) >= A.shape[0] or np.isclose(A[max_k, i], 0):
                j += 1
                continue
            A[[i, max_k]] = A[[max_k, i]]
        A[i] = A[i] / A[i, j]
        if (i + 1) < A.shape[0]:
            A[i+1:, j:] = A[i+1:, j:] - A[i+1:, [j]] * A[i, j:]
        pivots.append((i, j))
        i += 1
        j += 1
    return A, pivots

行交換時,有一種特殊情況,即剩餘所有行的主元列都沒有非零元素
這種情況下,在當前列的右側尋找不為零的列,作為新的主元列。

# 用例3測試eliminate_forward
aug = [[0, 1, -4, 8],
       [2, -3, 2, 1],
       [4, -8, 12, 1]]
echelon, pivots = eliminate_forward(aug)
print(echelon)
print(pivots)
[[ 1.   -2.    3.    0.25]
 [ 0.    1.   -4.    0.5 ]
 [ 0.    0.    0.    1.  ]]
[(0, 0), (1, 1), (2, 3)]

例3化簡的結果與書上略有不同,由行交換策略不同引起,也說明同一個矩陣可能由多個階梯形。

結合上述的前向和後向過程,即可以給出一個完整的消元法實現

def eliminate(augmented):
    """
    利用消元法前向和後向步驟,化簡線性方程組.
    
    如果是矛盾方程組,則僅輸出前向化簡結果,並打印提示
    否則輸出簡化后的方程組,並輸出最後一列
    """
    print(np.asarray(augmented))
    A, pivots = eliminate_forward(augmented)
    print(" The echelon form is\n", A)
    print(" The pivots are: ", pivots)
    pivot_cols = {p[1] for p in pivots}
    simplified = eliminate_backward(A, pivots)
    if (A.shape[1]-1) in pivot_cols:
        print(" There is controdictory.\n", simplified)
    elif len(pivots) == (A.shape[1] -1):
        print(" Solution: ", simplified[:, -1])
        is_correct = solution_check(np.asarray(augmented), 
                            simplified[:, -1])
        print(" Is the solution correct? ", is_correct)
    else:
        print(" There are free variables.\n", simplified)
    print("-"*30)
eliminate(aug)
[[ 0  1 -4  8]
 [ 2 -3  2  1]
 [ 4 -8 12  1]]
 The echelon form is
 [[ 1.   -2.    3.    0.25]
 [ 0.    1.   -4.    0.5 ]
 [ 0.    0.    0.    1.  ]]
 The pivots are:  [(0, 0), (1, 1), (2, 3)]
 There is controdictory.
 [[ 1.  0. -5.  0.]
 [ 0.  1. -4.  0.]
 [ 0.  0.  0.  1.]]
------------------------------

利用 Sympy 驗證消元法實現的正確性

Python的符號計算庫Sympy,有化簡矩陣為行最簡型的方法,可以用來檢驗本節實現的代碼是否正確。

# 導入 sympy的 Matrix模塊
from sympy import Matrix
Matrix(aug).rref(simplify=True)
# 返回的是行最簡型和主元列的位置
(Matrix([
 [1, 0, -5, 0],
 [0, 1, -4, 0],
 [0, 0,  0, 1]]), (0, 1, 3))
echelon, pivots = eliminate_forward(aug)
simplified = eliminate_backward(echelon, pivots)
print(simplified, pivots)
# 輸出與上述rref一致
[[ 1.  0. -5.  0.]
 [ 0.  1. -4.  0.]
 [ 0.  0.  0.  1.]] [(0, 0), (1, 1), (2, 3)]

綜合前向和後向步驟,並結果的正確性

綜合前向和後向消元,就可以得到完整的消元法過程。
消元結束,如果沒有矛盾(最後一列不是主元列),基本變量數與未知數個數一致,則有唯一解,可以驗證解是否正確。
驗證的方法是將解與係數矩陣相乘,檢查與原方程的b列一致。

def solution_check(augmented, solution):
    # 係數矩陣與解相乘
    b = augmented[:, :-1] @ solution.reshape([-1, 1])
    b = b.reshape([-1])
    # 檢查乘積向量與b列一致
    return all(np.isclose(b - augmented[:, -1], np.zeros(len(b))))
def eliminate(augmented):
    from sympy import Matrix
    print(np.asarray(augmented))
    A, pivots = eliminate_forward(augmented)
    print(" The echelon form is\n", A)
    print(" The pivots are: ", pivots)
    pivot_cols = {p[1] for p in pivots}
    simplified = eliminate_backward(A, pivots)
    if (A.shape[1]-1) in pivot_cols: # 最後一列是主元列
        print(" There is controdictory.\n", simplified)
    elif len(pivots) == (A.shape[1] -1): # 唯一解
        is_correct = solution_check(np.asarray(augmented), 
                            simplified[:, -1])
        print(" Is the solution correct? ", is_correct)
        print(" Solution: \n", simplified)
    else: # 有自由變量
        print(" There are free variables.\n", simplified)
    print("-"*30)
    print("對比Sympy的rref結果")
    print(Matrix(augmented).rref(simplify=True))
    print("-"*30)

測試書中的例子

aug_1_1_1 = [[1, -2, 1, 0], 
             [0, 2, -8, 8], 
             [5, 0, -5, 10]]
eliminate(aug_1_1_1)
# 1.1 example 3
aug_1_1_3 = [[0, 1, -4, 8],
             [2, -3, 2, 1],
             [4, -8, 12, 1]]
eliminate(aug_1_1_3)
eliminate([[1, -6, 4, 0, -1],
           [0, 2, -7, 0, 4],
           [0, 0, 1, 2, -3],
           [0, 0, 3, 1, 6]])
eliminate([[0, -3, -6, 4, 9],
           [-1, -2, -1, 3, 1],
           [-2, -3, 0, 3, -1],
           [1, 4, 5, -9, -7]])

eliminate([[0, 3, -6, 6, 4, -5],
           [3, -7, 8, -5, 8, 9],
           [3, -9, 12, -9, 6, 15]])
[[ 1 -2  1  0]
 [ 0  2 -8  8]
 [ 5  0 -5 10]]
 The echelon form is
 [[ 1. -2.  1.  0.]
 [ 0.  1. -4.  4.]
 [ 0.  0.  1. -1.]]
 The pivots are:  [(0, 0), (1, 1), (2, 2)]
 Is the solution correct?  True
 Solution: 
 [[ 1.  0.  0.  1.]
 [ 0.  1.  0.  0.]
 [ 0.  0.  1. -1.]]
------------------------------
對比Sympy的rref結果
(Matrix([
[1, 0, 0,  1],
[0, 1, 0,  0],
[0, 0, 1, -1]]), (0, 1, 2))
------------------------------
[[ 0  1 -4  8]
 [ 2 -3  2  1]
 [ 4 -8 12  1]]
 The echelon form is
 [[ 1.   -2.    3.    0.25]
 [ 0.    1.   -4.    0.5 ]
 [ 0.    0.    0.    1.  ]]
 The pivots are:  [(0, 0), (1, 1), (2, 3)]
 There is controdictory.
 [[ 1.  0. -5.  0.]
 [ 0.  1. -4.  0.]
 [ 0.  0.  0.  1.]]
------------------------------
對比Sympy的rref結果
(Matrix([
[1, 0, -5, 0],
[0, 1, -4, 0],
[0, 0,  0, 1]]), (0, 1, 3))
------------------------------
[[ 1 -6  4  0 -1]
 [ 0  2 -7  0  4]
 [ 0  0  1  2 -3]
 [ 0  0  3  1  6]]
 The echelon form is
 [[ 1.  -6.   4.   0.  -1. ]
 [ 0.   1.  -3.5  0.   2. ]
 [ 0.   0.   1.   2.  -3. ]
 [-0.  -0.  -0.   1.  -3. ]]
 The pivots are:  [(0, 0), (1, 1), (2, 2), (3, 3)]
 Is the solution correct?  True
 Solution: 
 [[ 1.   0.   0.   0.  62. ]
 [ 0.   1.   0.   0.  12.5]
 [ 0.   0.   1.   0.   3. ]
 [-0.  -0.  -0.   1.  -3. ]]
------------------------------
對比Sympy的rref結果
(Matrix([
[1, 0, 0, 0,   62],
[0, 1, 0, 0, 25/2],
[0, 0, 1, 0,    3],
[0, 0, 0, 1,   -3]]), (0, 1, 2, 3))
------------------------------
[[ 0 -3 -6  4  9]
 [-1 -2 -1  3  1]
 [-2 -3  0  3 -1]
 [ 1  4  5 -9 -7]]
 The echelon form is
 [[ 1.   1.5 -0.  -1.5  0.5]
 [-0.   1.   2.  -3.  -3. ]
 [-0.  -0.  -0.   1.  -0. ]
 [ 0.   0.   0.   0.   0. ]]
 The pivots are:  [(0, 0), (1, 1), (2, 3)]
 There are free variables.
 [[ 1.  0. -3.  0.  5.]
 [-0.  1.  2.  0. -3.]
 [-0. -0. -0.  1. -0.]
 [ 0.  0.  0.  0.  0.]]
------------------------------
對比Sympy的rref結果
(Matrix([
[1, 0, -3, 0,  5],
[0, 1,  2, 0, -3],
[0, 0,  0, 1,  0],
[0, 0,  0, 0,  0]]), (0, 1, 3))
------------------------------
[[ 0  3 -6  6  4 -5]
 [ 3 -7  8 -5  8  9]
 [ 3 -9 12 -9  6 15]]
 The echelon form is
 [[ 1.         -2.33333333  2.66666667 -1.66666667  2.66666667  3.        ]
 [ 0.          1.         -2.          2.          1.33333333 -1.66666667]
 [ 0.          0.          0.          0.          1.          4.        ]]
 The pivots are:  [(0, 0), (1, 1), (2, 4)]
 There are free variables.
 [[  1.   0.  -2.   3.   0. -24.]
 [  0.   1.  -2.   2.   0.  -7.]
 [  0.   0.   0.   0.   1.   4.]]
------------------------------
對比Sympy的rref結果
(Matrix([
[1, 0, -2, 3, 0, -24],
[0, 1, -2, 2, 0,  -7],
[0, 0,  0, 0, 1,   4]]), (0, 1, 4))
------------------------------

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

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

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

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

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

PL真有意思(三):名字、作用域和約束

前言

這兩篇寫了詞法分析和語法分析,比較偏向實踐。這一篇來看一下語言設計里一個比較重要的部分:名字。在大部分語言里,名字就是標識符,如果從抽象層面來看名字就是對更低一級的內存之類的概念的一層抽象。但是名字還有其它相關的比如它的約束時間和生存周期等等

約束時間

約束就是兩個東西之間的一種關聯,例如一個名字和它所命名的事物,約束時間就是指創建約束的時間。有關的約束可以在許多不同的時間作出

  • 語言設計時
  • 語言實現時
  • 編寫程序時
  • 編譯時
  • 鏈接時
  • 裝入時
  • 運行時

這就是為什麼基於編譯的語言實現通常會比基於解釋器的語言的實現更高效的原因,因為基於編譯的語言在更早的時候就做了約束,比如對於全局變量在編譯時就已經確定了它在內存中的布局了

對象生存期和存儲管理

在名字和它們所引用的對象的約束之間有幾個關鍵事件

  • 對象的創建
  • 約束的創建
  • 對變量、子程序、類型等的引用,所有這些都使用了約束
  • 對可能暫時無法使用的約束進行失活或者重新約束
  • 約束的撤銷
  • 對象的撤銷

對象的生存期和存儲分配機制有關

  • 靜態對象被賦予一個絕對地址,這個地址在程序的整個執行過程中都保持不變
  • 棧對象按照後進先出的方式分配和釋放,通常與子程序的調用和退出同時進行
  • 堆對象可以在任意時刻分配或者釋放,它們要求更通用的存儲管理算法

靜態分配

全局變量是靜態對象最顯而易見的例子,還有構成程序的機器語言翻譯結果的那些指令,也可以看作是靜態分配對象。

還有像每次調用函數都會保持相同的值的局部變量也是靜態分配的。對於數值和字符串這些常量也是靜態分配。

還有用來支持運行時的各種程序,比如廢料收集和異常處理等等也可以看作是靜態分配

基於棧的分配

如果一種語言允許遞歸,那麼局部變量就不能使用靜態分配的方式了,因為在同一時刻,一個局部變量存在的實例個數是不確定的

所以一般對於子程序,都用棧來保存它相關的變量信息。在運行時,一個子程序的每個實例都在棧中有一個相應的棧幀,保存着它的參數、返回值、局部變量和一些簿記信息

基於堆的分配

堆是一塊存儲區域,其中的子存儲塊可以在任意時間分配與釋放。因為堆具有它的動態性,所以就需要對堆空間進行嚴格的管理。許多存儲管理算法都維護着堆中當前尚未使用的存儲塊的一個鏈接表,稱為自由表。

初始時這個表只有一個塊,就是整個堆,每當遇到分配請求時,算法就在表中查找一個大小適當的塊。所以當請求次數增多,就會出現碎片問題,也需要相應的解決

所以有廢料收集的語言其實就是對堆的管理

作用域作用

一個約束起作用的那一段程序正文區域,稱為這個約束的作用域。

現在大多數語言使用的都是靜態作用域,也就是在編譯時就確定了。也有少數語言使用動態作用域,它們的約束需要等到運行時的執行流才能確定

靜態作用域

在使用靜態作用域的語言,也叫作詞法作用域。一般當前的約束就是程序中包圍着一個給定點的最近的,其中有與該名字匹配的聲明的那個快中建立的那個約束。比如C語言在進入子程序時,如果局部變量和全局變量,那麼當前的約束就是與局部變量關聯,直到退齣子程序才撤銷這個約束

但是有的語言提供了一種可以提供約束的生存期的機制,比如Fortran的save和C的static

嵌套子程序

有許多語言允許一個子程序嵌套在另一個子程序的。這樣有關約束的定義通常來說都是首先用這個名字在當前、最內層的作用域中查找相應的聲明,如果找不到就直接到更外圍的作用域查找當前的約束,直到到達全局作用域,否則就發生一個錯誤

訪問非局部變量

上面提到的訪問外圍作用域的變量,但是當前子程序只能訪問到當前的棧幀,所以就需要一個調用幀鏈來讓當前的作用域訪問到外圍作用,通過調用順序形成一個靜態鏈

聲明的順序

關於約束還有一個問題,就是在同一作用域里,先聲明的名字是否能使用在此之後的聲明

在Pascal里有這樣兩條規則:

  1. 修改變量要求名字在使用之前就進行聲明
  2. 但是當前聲明的作用域是整個程序塊

所以在這兩個的相互作用下,會造成一個讓人吃驚的問題

const N = 10;

procedure foo;
const
  M = N; (*靜態語義錯誤*)
  N = 20;

但是在C、C++和Java等語言就不會出現這個問題,它們都規定標識符的作用域不是整個塊,而是從其聲明到塊結束的那一部分

並且C++和Java還進一步放寬了規則,免除了使用之前必須聲明的要求

模塊

恰當模塊化的代碼可以減少程序員的思維負擔,因為它最大限度的減少了理解系統的任意給定部分時所需的信息量。在設計良好的程序中,模塊之間的接口應盡可能的小,所有可能改變的設計決策都隱藏在某個模塊里。

模塊作為抽象

模塊可以將一組對象(如子程序、變量、類型)封裝起來。使得:

  1. 這些內部的對象相互可見
  2. 但是外部對象和內部對象,除非显示的導入,否則都是不可見的

模塊作為管理器

模塊使我們很容易的創建各種抽象,但是如果需要多個棧的實例,那麼就需要一個讓模塊成為一個類型的管理器。這種管理器組織方式一般都是要求在模塊中增加創建/初始化函數,並給每一個函數增加一個用於描述被操作的實例

模塊類型

對於像這種多實例的問題,除了管理器,在許多語言里的解決方法都是可以將模塊看作是類型。當模塊是類型的時候,就可以將當前的方法認為是屬於這個類型的,簡單來說就是調用方法變化了

push(A, x) -> A.push(x)

本質上的實現區別不大

面向對象

在更面向對象里的方法里,可以把類看作是一種擴充了一種繼承機制的模塊類型。繼承機制鼓勵其中所有操作都被看作是從屬於對象的,並且新的對象可以從現有對象繼承大部分的操作,而不需要為這些操作重寫代碼。

類的概念最早應該是起源於Simula-67,像後來的C++,Java和C#中的類的思想也都起源於它。類也是像Python和Ruby這些腳本語言的核心概念

從模塊到模塊類型再到類都是有其思想基礎,但是最初都是為了更好的數據抽象。但是即使有了類也不能完全取代模塊,所以許多語言都提供了面向對象和模塊的機制

動態作用域

在使用動態作用域的語言中,名字與對象間的約束依賴於運行時的控制流,特別是依賴子程序的調用順序

n : integer

procedure first
  n := 1

procedure second
  n : integer
  first()

n := 2
if read_integer() > 0
  second()
else
  first()
write_integer()

這裏最後的輸出結果完全取決於read_integer讀入的数字的正負,如果為正,輸出就為2,否則就打印一個1

作用域的實現

為了跟蹤靜態作用域程序中的哥哥名字,編譯器需要依靠一個叫做符號表的數據結構。從本質上看,符號表就是一個記錄名字和它已知信息的映射關係的字典,但是由於作用域規則,所以還需要更強大的數據結構。像之前那個寫編譯器系列的符號表就是使用哈希表加上同一層作用域鏈表來實現的

而對於動態作用域來說就需要在運行時執行一些操作

作用域中名字的含義

別名

在基於指針的數據結構使用別名是很自然的情況,但是使用別名可能會導致編譯器難以優化或者造成像懸空引用的問題,所以需要謹慎使用

重載

在大多數語言中都或多或少的提供了重載機制,比如C語言中(+)可以被用在整數類型也可以用在浮點數類型,還有Java中的String類型也支持(+)運算髮

要在編譯器的符號表中處理重載問題,就需要安排查找程序根據當前的上下文環境返回一個有意義的符號

比如C++、Java和C#中的類方法重載都可以根據當前的參數類型和數量來判斷使用哪個符號

內部運算符的重載

C++、C#和Haskell都支持用戶定義的類型重載內部的算術運算符,在C++和C#的內部實現中通常是將A+B看作是operator+(A, B)的語法糖

多態性

對於名字,除了重載還有兩個重要的概念:強制和多態。這三個概念都用於在某些環境中將不同類型的參數傳給一個特定名字的子程序

強制是編譯器為了滿足外圍環境要求,自動將某類型轉換為另一類型的值的操作

所以在C中,定義一個計算整數或者浮點數兩個值中的最小值的函數

double min(double x, double y);

只要浮點數至少有整數那麼多有效二進制位,那麼結果就一定會是正確的。因為編譯器會對int類型強制轉換為double類型

這是強制提供的方法,但是多態性提供的是,它使同一個子程序可以不加轉換的接受多種類型的參數。要使這個概念有意義,那麼這多種類型肯定要具有共同的特性

顯式的參數多態性就叫做泛型,像Ada、C++、Clu、Java和C#都支持泛型機制,像剛才的例子就可以在Ada中用泛型來實現

generic
  type T is private;
  with function "<" (x, y : T) return Boolean;
function min(x, y : T) return T;

function min(x, y : T) return T is
begin
  if x < y then return x;
  else return y;
  end if;
end min

function string_min is new min(string, "<")
function date_min is new min(date, date_precedes);

像List和ML中就可以直接寫

(define min (lambda (a b) (if (< a b) a b)))

其中有關類型的任何細節都由解釋器處理

引用環境的約束

提到引用環境的約束就有兩種方式:淺約束和深約束

推遲到調用時建立約束的方式淺約束。一般動態作用域的語言默認是淺約束,當然動態作用域和深約束也是可以組合到一起的。
執行時依然使用傳遞時的引用環境,而非執行時的引用環境。那麼這種規則稱為深約束,一般靜態作用域的語言默認是深約束

閉包

為了實現神約束,需要創建引用環境的一種顯示錶示形式,並將它與對有關子程序的引用捆綁在一起,這樣的捆綁叫做閉包

總而言之,如果子程序可以被當作參數傳遞,那麼它的引用環境一樣也會被傳遞過去

一級值和非受限生存期

一般而言,在語言中,如果一個值可以賦值給變量、可以當作參數傳遞、可以從子程序返回,那麼它被稱為具有一級狀態(和我們在js中說函數是一等公民一個含義)。大多數的語言中數據對象都是一級狀態。二級狀態是只能當作參數傳遞;三級值則是連參數也不能做,比如C#中一些+-*/等符號。

在一級子程序會出現一個複雜性,就是它的生存期可能持續到這個子程序的作用域的執行期外。為了避免這一問題,大部分函數式語言都表示局部變量具有非受限的生命周期,它們的生命周期無限延長,直到GC能證明這些對象再也不使用了才會撤銷。那麼不撤銷帶來的問題就是這些子程序的存儲分配基於棧幀是不行了,只能是基於堆來分配管理。為了維持能基於棧的分配,有些語言會限制一級子程序的能力,比如C++,C#,都是不允許子程序嵌套,也就從根本上不會存在閉包帶來的懸空引用問題。

小結

這一篇從名字入手,介紹了名字與其背後的對象的約束關係、以及約束時間的概念;然後介紹了對象的分配策咯(靜態、棧、堆);緊接着討論了名字與對象之間建立的約束的生命周期,並由此引出了作用域的概念;進一步延伸出多個約束組成的引用環境的相關概念以及問題。

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

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

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

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

【algo&ds】8.最小生成樹

1.最小生成樹介紹

什麼是最小生成樹?

最小生成樹(Minimum spanning tree,MST)是在一個給定的無向圖G(V,E)中求一棵樹T,使得這棵樹擁有圖G中的所有頂點,且所有邊都是來自圖G中的邊,並且滿足整棵樹的邊權值和最小。

2.prim算法

和Dijkstra算法很像!!請看如下Gif圖,prim算法的核心思想是對圖G(V,E)設置集合S,存放已被訪問的頂點,然後每次從集合V-S中選擇與集合S的最短距離最小的一個頂點(記為u),訪問並加入集合S。之後,令頂點u為中間點,優化所有從u能到達的頂點v與集合s之間的最短距離。這樣的操作執行n次,直到集合s中包含所有頂點。

不同的是,Dijkstra算法中的dist是從源點s到頂點w的最短路徑;而prim算法中的dist是從集合S到頂點w的最短路徑,以下是他們的偽碼描述對比,關於Dijkstra算法的詳細描述請

算法實現:

#include<iostream>
#include<vector>
#define INF 100000
#define MaxVertex 105
typedef int Vertex; 
int G[MaxVertex][MaxVertex];
int parent[MaxVertex];   // 並查集 
int dist[MaxVertex]; // 距離 
int Nv;    // 結點 
int Ne;    // 邊 
int sum;  // 權重和 
using namespace std; 
vector<Vertex> MST;  // 最小生成樹 

// 初始化圖信息 
void build(){
    Vertex v1,v2;
    int w;
    cin>>Nv>>Ne;
    for(int i=1;i<=Nv;i++){
        for(int j=1;j<=Nv;j++)
            G[i][j] = 0;  // 初始化圖 
        dist[i] = INF;   // 初始化距離
        parent[i] = -1;  // 初始化並查集 
    }
    // 初始化點
    for(int i=0;i<Ne;i++){
        cin>>v1>>v2>>w;
        G[v1][v2] = w;
        G[v2][v1] = w;
    }
}

// Prim算法前的初始化 
void IniPrim(Vertex s){
    dist[s] = 0;
    MST.push_back(s);
    for(Vertex i =1;i<=Nv;i++)
        if(G[s][i]){
            dist[i] = G[s][i];
            parent[i] = s;
        } 
}

// 查找未收錄中dist最小的點 
Vertex FindMin(){
    int min = INF;
    Vertex xb = -1;
    for(Vertex i=1;i<=Nv;i++)
        if(dist[i] && dist[i] < min){ 
            min = dist[i];
            xb = i;
        }
    return xb;
}

void output(){
    cout<<"被收錄順序:"<<endl; 
    for(Vertex i=1;i<=Nv;i++)
        cout<<MST[i]<<" ";
    cout<<"權重和為:"<<sum<<endl; 
    cout<<"該生成樹為:"<<endl; 
    for(Vertex i=1;i<=Nv;i++)
        cout<<parent[i]<<" ";
}

void Prim(Vertex s){
    IniPrim(s);
    while(1){
        Vertex v = FindMin();
        if(v == -1)
            break;
        sum += dist[v];
        dist[v] = 0;
        MST.push_back(v);
        for(Vertex w=1;w<=Nv;w++)
            if(G[v][w] && dist[w])
                if(G[v][w] < dist[w]){
                    dist[w] = G[v][w];
                    parent[w] = v;
                }
    }
}


int main(){
    build();
    Prim(1);
    output();
    return 0;
} 

關於prim算法的更加詳細講解請

3.kruskal算法

Kruskal算法也可以用來解決最小生成樹的問題,其算法思想很容易理解,典型的邊貪心,其算法思想為:

  • 在初始狀態時隱去圖中所有的邊,這樣圖中每個頂點都是一個單獨的連通塊,一共有n個連通塊
  • 對所有邊按邊權從小到大進行排序
  • 按邊權從小到大測試所有邊,如果當前測試邊所連接的兩個頂點不在同一個連通塊中,則把這條測試邊加入當前最小生成樹中,否則,將邊捨棄。
  • 重複執行上一步驟,直到最小生成樹中的邊數等於總頂點數減一 或者測試完所有邊時結束;如果結束時,最小生成樹的邊數小於總頂點數減一,說明該圖不連通。

請看下面的Gif圖!

算法實現:

#include<iostream>
#include<string>
#include<vector>
#include<queue>
#define INF 100000
#define MaxVertex 105
typedef int Vertex; 
int G[MaxVertex][MaxVertex];
int parent[MaxVertex];   // 並查集最小生成樹 
int Nv;    // 結點 
int Ne;    // 邊 
int sum;  // 權重和 
using namespace std; 
struct Node{
    Vertex v1;
    Vertex v2;
    int weight; // 權重 
    // 重載運算符成最大堆 
    bool operator < (const Node &a) const
    {
        return weight>a.weight;
    }
};
vector<Node> MST;  // 最小生成樹 
priority_queue<Node> q;   // 最小堆 

// 初始化圖信息 
void build(){
    Vertex v1,v2;
    int w;
    cin>>Nv>>Ne;
    for(int i=1;i<=Nv;i++){
        for(int j=1;j<=Nv;j++)
            G[i][j] = 0;  // 初始化圖
        parent[i] = -1;
    }
    // 初始化點
    for(int i=0;i<Ne;i++){
        cin>>v1>>v2>>w;
        struct Node tmpE;
        tmpE.v1 = v1;
        tmpE.v2 = v2;
        tmpE.weight = w;
        q.push(tmpE); 
    }
}

//  路徑壓縮查找 
int Find(int x){
    if(parent[x] < 0)
        return x;
    else
        return parent[x] = Find(parent[x]);
} 

//  按秩歸併 
void Union(int x1,int x2){
    if(parent[x1] < parent[x2]){
        parent[x1] += parent[x2];
        parent[x2] = x1;
    }else{
        parent[x2] += parent[x1];
        parent[x1] = x2;
    }
} 

void Kruskal(){
    // 最小生成樹的邊不到 Nv-1 條且還有邊 
    while(MST.size()!= Nv-1 && !q.empty()){
        Node E = q.top();  // 從最小堆取出一條權重最小的邊
        q.pop(); // 出隊這條邊 
        if(Find(E.v1) != Find(E.v2)){  // 檢測兩條邊是否在同一集合 
            sum += E.weight; 
            Union(E.v1,E.v2);     // 並起來 
            MST.push_back(E);
        }
    }
    
} 


void output(){
    cout<<"被收錄順序:"<<endl; 
    for(Vertex i=0;i<Nv;i++)
        cout<<MST[i].weight<<" ";
    cout<<"權重和為:"<<sum<<endl; 
    for(Vertex i=1;i<=Nv;i++)
        cout<<parent[i]<<" ";
    cout<<endl;
}


int main(){
    build();
    Kruskal();
    output();
    return 0;
} 

關於kruskal算法更詳細的講解

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

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

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

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

.NET高級特性-Emit(1)

  在這個大數據/雲計算/人工智能研發普及的時代,Python的崛起以及Javascript的前後端的侵略,程序員與企業似乎越來越青睞動態語言所帶來的便捷性與高效性,即使靜態語言在性能,錯誤檢查等方面的優於靜態語言。對於.NETer來說,.NET做為一門靜態語言,我們不僅要打好.NET的基本功,如基本類型/語法/底層原理/錯誤檢查等知識,也要深入理解.NET的一些高級特性,來為你的工作減輕負擔和提高代碼質量。

  ok,咱們今天開始聊一聊.NET中的Emit。

一、什麼是Emit?

  Emit含義為發出、產生的含義,這是.NET中的一組類庫,命名空間為System.Reflection.Emit,幾乎所有的.NET版本(Framework/Mono/NetCore)都支持Emit,可以實現用C#代碼生成代碼的類庫

二、Emit的本質

  我們知道.NET可以由各種語言進行編寫,比如VB,C++等,當然絕大部分程序員進行.NET開發都是使用C#語言進行的,這些語言都會被各自的語言解釋器解釋為IL語言並執行,而Emit類庫的作用就是用這些語言來編寫生成IL語言,並交給CLR(公共語言運行時)進行執行。

  我們先來看看IL語言長什麼樣子:

  (1) 首先我們創建一個Hello,World程序

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }

  (2) 將程序編譯成dll文件,我們可以看到在開發目錄下生成了bin文件夾

  

  (3) 向下尋找,我們可以看到dll文件已經生成,筆者使用netcore3進行開發,故路徑為bin/Debug/netcoreapp3.0

  

  (4) 這時候,我們就要祭出我們的il查看神器了,ildasm工具

  

  如何找到這個工具?打開開始菜單,找到Visual Studio文件夾,打開Developer Command Prompt,在打開的命令行中鍵入ildasm回車即可,筆者使用vs2019進行演示,其它vs版本操作方法均一致

  

 

 

 

 

 

 

   (5) 在dasm菜單欄選擇文件->打開,選擇剛剛生成的dll文件

  

 

 

   (6) 即可查看生成il代碼

  

 

  有了ildasm的輔助,我們就能夠更好的了解IL語言以及如何編寫IL語言,此外,Visual Studio中還有許多插件支持查看il代碼,比如JetBrains出品的Resharper插件等,如果覺得筆者方式較為麻煩可以使用以上插件查看il代碼

三、理解IL代碼

  在上一章節中,我們理解了Emit的本質其實就是用C#來編寫IL代碼,既然要編寫IL代碼,那麼我們首先要理解IL代碼是如何進行工作的,IL代碼是如何完成C#當中的順序/選擇/循環結構的,是如何實現類的定義/字段的定義/屬性的定義/方法的定義的。

  IL代碼是一種近似於指令式的代碼語言,與彙編語言比較相近,所以習慣於寫高級語言的.NETer來說比較難以理解

  讓我們來看看Hello,World程序的IL代碼:

IL_0000:  nop
IL_0001:  ldstr      "Hello World!"
IL_0006:  call       void [System.Console]System.Console::WriteLine(string)
IL_000b:  nop
IL_000c:  ret

  我們可以把IL代碼看成棧的運行

  第一條指令,nop表示不做任何事情,表示代碼不做任何事情

  第二條指令,ldstr表示將字符串放入棧中,字符串的值為“Hello,World!”

  第三條指令,call表示調用方法,參數為調用方法的方法信息,並把返回的結構壓入棧中,使用的參數為之前已經入棧的“Hello World!”,以此類推,如果方法有n個參數,那麼他就會調取棧中n個數據,並返回一個結果放回棧中

  第四條指令,nop表示不做任何事情

  第五條指令,ret表示將棧中頂部的數據返回,如果方法定義為void,則無返回值

  關於Hello,world程序IL的理解就說到這裏,更多的指令含義讀者可以參考微軟官方文檔,筆者之後也會繼續對Emit進行講解和Emit的應用

四、用Emit類庫編寫IL代碼

  既然IL代碼咱們理解的差不多了,咱們就開始嘗試用C#來寫IL代碼了,有了IL代碼的參考,咱們也可以依葫蘆畫瓢的把代碼寫出來了

  (1) 引入Emit命名空間

using System.Reflection.Emit;

  (2) 首先我們定義一個Main方法,入參無,返回類型void

//定義方法名,返回類型,輸入類型
var method = new DynamicMethod("Main", null, Type.EmptyTypes);

  (3) 生成IL代碼

//生成IL代碼
var ilGenerator = method.GetILGenerator();
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ldstr,"Hello World!");
ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); //尋找Console的WriteLine方法
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ret);

  (4) 創建委託並調用

//創建委託
var helloWorldMethod = method.CreateDelegate(typeof(Action)) as Action;
helloWorldMethod.Invoke();

  (5)運行,即輸出Hello World!

五、小結

  Emit的本質是使用高級語言生成IL代碼,進而進行調用的的一組類庫,依賴Emit我們可以實現用代碼生成代碼的操作,即編程語言的自舉,可以有效彌補靜態語言的靈活性的缺失。

  Emit的性能非常好,除了第一次構建IL代碼所需要時間外,之後只要將操作緩存在計算機內存中,速度與手寫代碼相差無幾

  有許多著名.NET類庫均依賴於Emit:

  (.NET JSON操作庫)Json.NET/Newtonsoft.Json:

  (輕量ORM)Dapper:

  (ObjectToObjectMapper)EmitMapper:

  (AOP庫)Castle.DynamicProxy:

  學習Emit:

  .NET官方文檔:

  .NET API瀏覽器:

  之後作者將繼續講解.NET Emit的相關內容和應用,感謝閱讀

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

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

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

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

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

網絡相關的命令工具研究報告

主機配置:DHCP

  DHCP(動態主機配置協議),是在一台主機啟動后,第一個運行的客戶/服務器應用程序。換言之,當一台主機啟動后,如果它認為自己當前應當連接到因特網上,但又不知道自己的IP地址時,DHCP就以引導程序的身份發揮作用。

  每個連接到TCP/IP互聯網的計算機都必須知道自己的IP地址、一個路由器的IP地址、一個名字服務器的IP地址以及自己的子網掩碼這四種信息。

 DHCP分組格式:

 

 

一、曾經使用過的協議

  在DHCP成為正式的主機配置協議之前,還有過一些其他的協議。

1.RARP:

  在因特網時代的初期,人們曾設計了一個稱為逆地址解析協議(Reverse Address Resolution Protocol,RARP)來向被引導的主機提供IP地址。實際上,RARP是ARP的一個版本。ARP將一個IP地址映射為一個物理地址,而RARP則將一個物理地址映射成為一個IP地址。但是RARP已經被淘汰了,原因有兩個:首先,RARP利用了數據鏈路層的廣播服務,這也就表示每個網絡上都必須存在一台RARP服務器。第二,RARP只能提供計算機的IP地址,但如今的計算機需要前面提到的所有四種信息。

2.BOOTP:

  引導程序協議(BOOTstrap Protocol,BOOTP)是DHCP的先驅。它是一個客戶/服務器協議,被設計用來克服RARP協議存在的缺陷。但是BOOTP是一個靜態配置協議,當客戶請求自己的IP地址時,BOOTP服務器就諮詢一張表,將客戶的物理地址映射成相應的IP地址。這就意味着客戶的物理地址和IP地址之間的綁定是已經存在的。這個綁定關係是事先設定好的。

  在某些場合,我們需要的是一個動態配置協議。例如,當一台主機從一個物理網絡移動到另一個物理網絡時,它的物理地址就改變了。再比如,有時候主機需要在某一段時間內使用一個臨時的IP地址。BOOTP無法處理這種狀況,因為物理地址和IP地址之間的綁定是靜態的,是固定存放在一張表中的,除非管理員更改這張表。

  而DHCP的設計就是為了解決這些不足之處。

3. DHCP:

  動態主機配置協議(Dynamic Host Configuration Protocol,DHCP)是一種客戶/服務器協議,設計這個協議是為了將上述四種信息傳遞給無盤計算機或者第一次啟動的計算機。DHCP是BOOTP的繼承者,並且能夠兼容BOOTP。 

二、DHCP操作

  DHCP客戶和DHCP服務器可以在同一個網絡上,也可以位於不同的網絡。

1.DHCP客戶和DHCP服務器在同一個網絡

  雖然這種情況不是很常見,不過管理員可以把客戶和服務器放在同一個網絡中。如圖所示:

 

這種情況的操作如下:

(1)DHCP服務器在UDP端口67發出被動打開命令,等待客戶請求。

(2)被引導的客戶在UDP端口68發出主動打開命令。這個報文被封裝成UDP用戶報,其目的端口是67,源端口號是68。這個UDP用戶數據報在封裝成IP數據包。客戶使用的是全0的源地址和全1的目的地址。

(3)服務器或者用廣播報文,或者用單播報文來響應這個用戶,它使用了UDP源端口號67和目的端口68.這個響應可以是單播的,因為服務器知道客戶的IP地址,同時也知道客戶的物理地址,也就是說它不需要使用ARP的服務進行從邏輯地址到物理地址的映射。但是某些系統不允許旁路掉ARP,結果就要使用廣播地址。

2. DHCP客戶和DHCP服務器在不同的網絡

如圖所示:

 
  像其他應用層的進程一樣,客戶可以在某個網絡上,而服務器可以在相隔好幾個網絡之外的另一網絡上。這就帶來了一個必須要解決的問題。DHCP請求是廣播發送的,因為客戶不知道服務器的IP地址。而廣播的IP數據報不能通過任何路由器。路由器收到這樣的分組就丟棄它。

  要解決這個問題,就需要一个中介物。某台主機(或是一台能夠配置為在應用層工作的路由器)可以用來充當中繼。在這種情況下,該主機就稱為中繼代理。中繼代理知道DHCP服務器的單播地址,並在端口67監聽廣播報文。當它收到這種類型的分組后,就把它封裝成一個單播數據報,並且把此請求發送給DHCP服務器。攜帶了單播目的地址的分組可以被任何一個路由器轉發,最終到達DHCP服務器。DHCP服務器知道這個報文來自中繼代理,因為在請求報文中有一個字段定義了中繼代理的IP地址。中繼代理在收到回答后,再把它發送給DHCP客戶。 

三、配置

  人們設計DHCP是為了提供靜態和動態的地址分配。

1.靜態地址分配

  對於靜態地址分配,DHCP有一個專門的數據庫,可以靜態地吧物理地址綁定到IP地址。

2.動態地址分配

  DHCP還有第二個數據庫,包括一個可用的IP地址池。第二個數據庫使DHCP成為動態的。當DHCP客戶請求臨時的IP地址時,DHCP服務器就從可用(即為使用的)IP地址池中取出一個IP地址進行指派,這個IP地址的使用時間長短可協商。

  當DHCP客戶想DHCP服務器發送請求是,服務器首先檢查它的靜態數據庫。若靜態數據庫中存在所請求物理地址的表項,則返回給這個客戶的永久IP地址。反之,若靜態數據庫中沒有這個表項,服務器就從可用IP地址池中選擇一個IP地址,並把這個地址指派給客戶,然後再把相應的表項加入到動態數據庫中。

  如果主機要從一個網絡移動到另一個網絡,或者與一個網絡時連時斷,那麼DHCP的這種動態特性就有了用武之地。DHCP可以在有限時間內提供一個臨時的IP地址。

從地址池指派的地址都是臨時地址。DHCP服務器向客戶授予某一段時間內對該地址池的租用權。當租用時效過期,客戶或者停止使用這個IP地址,或者續租。服務器有權力選擇同意或不同意續租。若服務器不同意,客戶就停止使用這個地址。

3.轉換狀態

  為了提供動態的地址分配,DHCP客戶可以像狀態機那樣從一個狀態轉換到另一個狀態,狀態轉換取決於收到的報文和發送的報文。在這種情況下,報文的類型是由包含在DHCP分組中的標記為53的選項來定義的。標記為53 的選項如圖所示:

 

DHCP的不同狀態:

(1)INIT狀態

  當DHCP客戶首次啟動時,它處於INIT狀態(初始化狀態)。客戶使用端口67廣播DHCPDISCOVER報文(一個帶有DHCPDISCOVER選項的請求報文)。

(2)SELECTING狀態

  在發送DHCPDISCOVER報文後,客戶就進入SELECTING(選擇)狀態。能夠提供這種類型服務的服務器要用DHCPOFFER報文進行相應。在此類報文中,服務器提供了一個IP地址。它們還要提供租用時間長度,其默認值是1小時。在發送DHCPOFFER報文的服務器,把提供的IP地址鎖定,使這個地址不會再提供給任何其他的客戶。客戶選擇所提供的地址中的一個,並向所選擇的服務器發送DHCPREQUEST報文。然後就進入REQUESTING狀態。如果客戶沒有收到DHCPOFFER報文,它還要再嘗試四次,每一次間隔2秒。如果對這些DHCPDISCOVER都沒有收到回答,客戶就睡眠5分鐘后再試。

(3)REQUESTING狀態

  客戶保持在這個REQUESTING(請求)狀態,直至它收到來自服務器的DHCPACK報文為止,這個報文創建了客戶物理地址和它的IP地址之間的綁定。客戶收到DHCPACK報文後進入BOUND狀態。

(4)BOUND狀態

  在這種狀態下,客戶可以使用該IP地址,直到租用時間到期。當到達租用時間的50%時,客戶就再發送一個DHCPREQUEST報文以請求更新。於是,客戶進入RENEWING。在BOUND(綁定)狀態時,客戶也可以取消租用,並進入到初始化狀態。

(5)RENEWING狀態

  客戶保持在RENEWING(更新)狀態,直至下面兩個事件之一發生。客戶可以收到更新租用協定的DHCPACK報文。在這種情況下,客戶把計時器複位,然後回到BOUND狀態。或者,如果沒有收到DHCPACK,同時到達租用時間的87.5%時,客戶就進入REBINDING狀態。

(6)REBINDING狀態

  客戶保持在REBINDING(重新綁定)狀態,直至下面三個事件之一發生。若客戶收到一個DHCPNACK報文或者租用時間到期,則回到初始化狀態,並嘗試得到另一個IP地址。若客戶收到DHCPACK報文,它就進入綁定狀態,並把計時器複位。

DHCP不同狀態的轉換圖:

 

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

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

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

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

源碼分析RocketMQ消息軌跡

目錄

本文沿着的思路,從如下3個方面對其源碼進行解讀:

  1. 發送消息軌跡
  2. 消息軌跡格式
  3. 存儲消息軌跡數據

@(本節目錄)

1、發送消息軌跡流程

首先我們來看一下在消息發送端如何啟用消息軌跡,示例代碼如下:

public class TraceProducer {
    public static void main(String[] args) throws MQClientException, InterruptedException {
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName",true);      // @1
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();
        for (int i = 0; i < 10; i++)
            try {
                {
                    Message msg = new Message("TopicTest",
                        "TagA",
                        "OrderID188",
                        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                    SendResult sendResult = producer.send(msg);
                    System.out.printf("%s%n", sendResult);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        producer.shutdown();
    }
}

從上述代碼可以看出其關鍵點是在創建DefaultMQProducer時指定開啟消息軌跡跟蹤。我們不妨瀏覽一下DefaultMQProducer與啟用消息軌跡相關的構造函數:

public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace)
public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic)

參數如下:

  • String producerGroup
    生產者所屬組名。
  • boolean enableMsgTrace
    是否開啟跟蹤消息軌跡,默認為false。
  • String customizedTraceTopic
    如果開啟消息軌跡跟蹤,用來存儲消息軌跡數據所屬的主題名稱,默認為:RMQ_SYS_TRACE_TOPIC。

1.1 DefaultMQProducer構造函數

public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,final String customizedTraceTopic) {      // @1
    this.producerGroup = producerGroup;
    defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
    //if client open the message trace feature
    if (enableMsgTrace) {                                                                                                                                                                                            // @2
        try {
            AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(customizedTraceTopic, rpcHook);                                                         
            dispatcher.setHostProducer(this.getDefaultMQProducerImpl());
            traceDispatcher = dispatcher;
            this.getDefaultMQProducerImpl().registerSendMessageHook(
                new SendMessageTraceHookImpl(traceDispatcher));                                                                                                                             // @3
        } catch (Throwable e) {
            log.error("system mqtrace hook init failed ,maybe can't send msg trace data");
        }
    }
}

代碼@1:首先介紹一下其局部變量。

  • String producerGroup
    生產者所屬組。
  • RPCHook rpcHook
    生產者發送鈎子函數。
  • boolean enableMsgTrace
    是否開啟消息軌跡跟蹤。
  • String customizedTraceTopic
    定製用於存儲消息軌跡的數據。

代碼@2:用來構建AsyncTraceDispatcher,看其名:異步轉發消息軌跡數據,稍後重點關注。

代碼@3:構建SendMessageTraceHookImpl對象,並使用AsyncTraceDispatcher用來異步轉發。

1.2 SendMessageTraceHookImpl鈎子函數

1.2.1 SendMessageTraceHookImpl類圖

  1. SendMessageHook
    消息發送鈎子函數,用於在消息發送之前、發送之後執行一定的業務邏輯,是記錄消息軌跡的最佳擴展點。
  2. TraceDispatcher
    消息軌跡轉發處理器,其默認實現類AsyncTraceDispatcher,異步實現消息軌跡數據的發送。下面對其屬性做一個簡單的介紹:
    • int queueSize
      異步轉發,隊列長度,默認為2048,當前版本不能修改。
    • int batchSize
      批量消息條數,消息軌跡一次消息發送請求包含的數據條數,默認為100,當前版本不能修改。
    • int maxMsgSize
      消息軌跡一次發送的最大消息大小,默認為128K,當前版本不能修改。
    • DefaultMQProducer traceProducer
      用來發送消息軌跡的消息發送者。
    • ThreadPoolExecutor traceExecuter
      線程池,用來異步執行消息發送。
    • AtomicLong discardCount
      記錄丟棄的消息個數。
    • Thread worker
      woker線程,主要負責從追加隊列中獲取一批待發送的消息軌跡數據,提交到線程池中執行。
    • ArrayBlockingQueue< TraceContext> traceContextQueue
      消息軌跡TraceContext隊列,用來存放待發送到服務端的消息。
    • ArrayBlockingQueue< Runnable> appenderQueue
      線程池內部隊列,默認長度1024。
    • DefaultMQPushConsumerImpl hostConsumer
      消費者信息,記錄消息消費時的軌跡信息。
    • String traceTopicName
      用於跟蹤消息軌跡的topic名稱。

1.2.2 源碼分析SendMessageTraceHookImpl

1.2.2.1 sendMessageBefore
public void sendMessageBefore(SendMessageContext context) { 
    //if it is message trace data,then it doesn't recorded
    if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName())) {   // @1
        return;
    }
    //build the context content of TuxeTraceContext
    TraceContext tuxeContext = new TraceContext();
    tuxeContext.setTraceBeans(new ArrayList<TraceBean>(1));
    context.setMqTraceContext(tuxeContext);
    tuxeContext.setTraceType(TraceType.Pub);
    tuxeContext.setGroupName(context.getProducerGroup());                                                                                                                       // @2
    //build the data bean object of message trace
    TraceBean traceBean = new TraceBean();                                                                                                                                                // @3
    traceBean.setTopic(context.getMessage().getTopic());
    traceBean.setTags(context.getMessage().getTags());
    traceBean.setKeys(context.getMessage().getKeys());
    traceBean.setStoreHost(context.getBrokerAddr());
    traceBean.setBodyLength(context.getMessage().getBody().length);
    traceBean.setMsgType(context.getMsgType());
    tuxeContext.getTraceBeans().add(traceBean);
}

代碼@1:如果topic主題為消息軌跡的Topic,直接返回。

代碼@2:在消息發送上下文中,設置用來跟蹤消息軌跡的上下環境,裏面主要包含一個TraceBean集合、追蹤類型(TraceType.Pub)與生產者所屬的組。

代碼@3:構建一條跟蹤消息,用TraceBean來表示,記錄原消息的topic、tags、keys、發送到broker地址、消息體長度等消息。

從上文看出,sendMessageBefore主要的用途就是在消息發送的時候,先準備一部分消息跟蹤日誌,存儲在發送上下文環境中,此時並不會發送消息軌跡數據。

1.2.2.2 sendMessageAfter
public void sendMessageAfter(SendMessageContext context) {
    //if it is message trace data,then it doesn't recorded
    if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName())     // @1
        || context.getMqTraceContext() == null) {
        return;
    }
    if (context.getSendResult() == null) {
        return;
    }

    if (context.getSendResult().getRegionId() == null
        || !context.getSendResult().isTraceOn()) {
        // if switch is false,skip it
        return;
    }

    TraceContext tuxeContext = (TraceContext) context.getMqTraceContext();
    TraceBean traceBean = tuxeContext.getTraceBeans().get(0);                                                                                                // @2
    int costTime = (int) ((System.currentTimeMillis() - tuxeContext.getTimeStamp()) / tuxeContext.getTraceBeans().size());     // @3
    tuxeContext.setCostTime(costTime);                                                                                                                                      // @4
    if (context.getSendResult().getSendStatus().equals(SendStatus.SEND_OK)) {                                                                    
        tuxeContext.setSuccess(true);
    } else {
        tuxeContext.setSuccess(false);
    }
    tuxeContext.setRegionId(context.getSendResult().getRegionId());                                                                                      
    traceBean.setMsgId(context.getSendResult().getMsgId());
    traceBean.setOffsetMsgId(context.getSendResult().getOffsetMsgId());
    traceBean.setStoreTime(tuxeContext.getTimeStamp() + costTime / 2);
    localDispatcher.append(tuxeContext);                                                                                                                                   // @5
}

代碼@1:如果topic主題為消息軌跡的Topic,直接返回。

代碼@2:從MqTraceContext中獲取跟蹤的TraceBean,雖然設計成List結構體,但在消息發送場景,這裏的數據永遠只有一條,及時是批量發送也不例外。

代碼@3:獲取消息發送到收到響應結果的耗時。

代碼@4:設置costTime(耗時)、success(是否發送成功)、regionId(發送到broker所在的分區)、msgId(消息ID,全局唯一)、offsetMsgId(消息物理偏移量,如果是批量消息,則是最後一條消息的物理偏移量)、storeTime,這裏使用的是(客戶端發送時間 + 二分之一的耗時)來表示消息的存儲時間,這裡是一個估值。

代碼@5:將需要跟蹤的信息通過TraceDispatcher轉發到Broker服務器。其代碼如下:

public boolean append(final Object ctx) {
    boolean result = traceContextQueue.offer((TraceContext) ctx);
    if (!result) {
        log.info("buffer full" + discardCount.incrementAndGet() + " ,context is " + ctx);
    }
    return result;
}

這裏一個非常關鍵的點是offer方法的使用,當隊列無法容納新的元素時會立即返回false,並不會阻塞。

接下來將目光轉向TraceDispatcher的實現。

1.3 TraceDispatcher實現原理

TraceDispatcher,用於客戶端消息軌跡數據轉發到Broker,其默認實現類:AsyncTraceDispatcher。

1.3.1 TraceDispatcher構造函數

public AsyncTraceDispatcher(String traceTopicName, RPCHook rpcHook) throws MQClientException {    
    // queueSize is greater than or equal to the n power of 2 of value
    this.queueSize = 2048;
    this.batchSize = 100;
    this.maxMsgSize = 128000;                                        
    this.discardCount = new AtomicLong(0L);         
    this.traceContextQueue = new ArrayBlockingQueue<TraceContext>(1024);
    this.appenderQueue = new ArrayBlockingQueue<Runnable>(queueSize);
    if (!UtilAll.isBlank(traceTopicName)) {
        this.traceTopicName = traceTopicName;
    } else {
        this.traceTopicName = MixAll.RMQ_SYS_TRACE_TOPIC;
    }                   // @1
    this.traceExecuter = new ThreadPoolExecutor(// :
        10, //
        20, //
        1000 * 60, //
        TimeUnit.MILLISECONDS, //
        this.appenderQueue, //
        new ThreadFactoryImpl("MQTraceSendThread_"));
    traceProducer = getAndCreateTraceProducer(rpcHook);      // @2
}

代碼@1:初始化核心屬性,該版本這些值都是“固化”的,用戶無法修改。

  • queueSize
    隊列長度,默認為2048,異步線程池能夠積壓的消息軌跡數量。
  • batchSize
    一次向Broker批量發送的消息條數,默認為100.
  • maxMsgSize
    向Broker彙報消息軌跡時,消息體的總大小不能超過該值,默認為128k。
  • discardCount
    整個運行過程中,丟棄的消息軌跡數據,這裏要說明一點的是,如果消息TPS發送過大,異步轉發線程處理不過來時,會主動丟棄消息軌跡數據。
  • traceContextQueue
    traceContext積壓隊列,客戶端(消息發送、消息消費者)在收到處理結果后,將消息軌跡提交到噶隊列中,則會立即返回。
  • appenderQueue
    提交到Broker線程池中隊列。
  • traceTopicName
    用於接收消息軌跡的Topic,默認為RMQ_SYS_TRANS_HALF_TOPIC。
  • traceExecuter
    用於發送到Broker服務的異步線程池,核心線程數默認為10,最大線程池為20,隊列堆積長度2048,線程名稱:MQTraceSendThread_。、
  • traceProducer
    發送消息軌跡的Producer。

代碼@2:調用getAndCreateTraceProducer方法創建用於發送消息軌跡的Producer(消息發送者),下面詳細介紹一下其實現。

1.3.2 getAndCreateTraceProducer詳解

private DefaultMQProducer getAndCreateTraceProducer(RPCHook rpcHook) {
        DefaultMQProducer traceProducerInstance = this.traceProducer;
        if (traceProducerInstance == null) {  //@1
            traceProducerInstance = new DefaultMQProducer(rpcHook);
            traceProducerInstance.setProducerGroup(TraceConstants.GROUP_NAME);
            traceProducerInstance.setSendMsgTimeout(5000);
            traceProducerInstance.setVipChannelEnabled(false);
            // The max size of message is 128K
            traceProducerInstance.setMaxMessageSize(maxMsgSize - 10 * 1000);
        }
        return traceProducerInstance;
    }

代碼@1:如果還未建立發送者,則創建用於發送消息軌跡的消息發送者,其GroupName為:_INNER_TRACE_PRODUCER,消息發送超時時間5s,最大允許發送消息大小118K。

1.3.3 start

public void start(String nameSrvAddr) throws MQClientException {
    if (isStarted.compareAndSet(false, true)) {     // @1
        traceProducer.setNamesrvAddr(nameSrvAddr);
        traceProducer.setInstanceName(TRACE_INSTANCE_NAME + "_" + nameSrvAddr);
        traceProducer.start();
    }
    this.worker = new Thread(new AsyncRunnable(), "MQ-AsyncTraceDispatcher-Thread-" + dispatcherId);   // @2
    this.worker.setDaemon(true);
    this.worker.start();                                                                                   
    this.registerShutDownHook();
}

開始啟動,其調用的時機為啟動DefaultMQProducer時,如果啟用跟蹤消息軌跡,則調用之。

代碼@1:如果用於發送消息軌跡的發送者沒有啟動,則設置nameserver地址,並啟動着。

代碼@2:啟動一個線程,用於執行AsyncRunnable任務,接下來將重點介紹。

1.3.4 AsyncRunnable

class AsyncRunnable implements Runnable {
         private boolean stopped;
    public void run() {
        while (!stopped) {
            List<TraceContext> contexts = new ArrayList<TraceContext>(batchSize);     // @1
            for (int i = 0; i < batchSize; i++) {
                TraceContext context = null;
                try {
                    //get trace data element from blocking Queue — traceContextQueue
                    context = traceContextQueue.poll(5, TimeUnit.MILLISECONDS);        // @2
                } catch (InterruptedException e) {
                }
                if (context != null) {
                    contexts.add(context);
                } else {
                    break;
                }
            }
            if (contexts.size() > 0) {                                                                               :
                AsyncAppenderRequest request = new AsyncAppenderRequest(contexts);  // @3
                traceExecuter.submit(request);                                                               
            } else if (AsyncTraceDispatcher.this.stopped) {
                this.stopped = true;
            }
        }
    }
}

代碼@1:構建待提交消息跟蹤Bean,每次最多發送batchSize,默認為100條。

代碼@2:從traceContextQueue中取出一個待提交的TraceContext,設置超時時間為5s,即如何該隊列中沒有待提交的TraceContext,則最多等待5s。

代碼@3:向線程池中提交任務AsyncAppenderRequest。

1.3.5 AsyncAppenderRequest#sendTraceData

public void sendTraceData(List<TraceContext> contextList) {
    Map<String, List<TraceTransferBean>> transBeanMap = new HashMap<String, List<TraceTransferBean>>();
    for (TraceContext context : contextList) {        //@1
        if (context.getTraceBeans().isEmpty()) {
            continue;
        }
        // Topic value corresponding to original message entity content
        String topic = context.getTraceBeans().get(0).getTopic();     // @2
        // Use  original message entity's topic as key
        String key = topic;
        List<TraceTransferBean> transBeanList = transBeanMap.get(key);
        if (transBeanList == null) {
            transBeanList = new ArrayList<TraceTransferBean>();
            transBeanMap.put(key, transBeanList);
        }
        TraceTransferBean traceData = TraceDataEncoder.encoderFromContextBean(context);    // @3
        transBeanList.add(traceData);
    }
    for (Map.Entry<String, List<TraceTransferBean>> entry : transBeanMap.entrySet()) {       // @4
        flushData(entry.getValue());
    }
}

代碼@1:遍歷收集的消息軌跡數據。

代碼@2:獲取存儲消息軌跡的Topic。

代碼@3:對TraceContext進行編碼,這裡是消息軌跡的傳輸數據,稍後對其詳細看一下,了解其上傳的格式。

代碼@4:將編碼后的數據發送到Broker服務器。

1.3.6 TraceDataEncoder#encoderFromContextBean

根據消息軌跡跟蹤類型,其格式會有一些不一樣,下面分別來介紹其合適。

1.3.6.1 PUB(消息發送)
case Pub: {
    TraceBean bean = ctx.getTraceBeans().get(0);
    //append the content of context and traceBean to transferBean's TransData
    sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getTopic()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getTags()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getStoreHost()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getBodyLength()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getMsgType().ordinal()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getOffsetMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
     .append(ctx.isSuccess()).append(TraceConstants.FIELD_SPLITOR);
}

消息軌跡數據的協議使用字符串拼接,字段的分隔符號為1,整個數據以2結尾,感覺這個設計還是有點“不可思議”,為什麼不直接使用json協議呢?

1.3.6.2 SubBefore(消息消費之前)
for (TraceBean bean : ctx.getTraceBeans()) {
    sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getRetryTimes()).append(TraceConstants.CONTENT_SPLITOR)//
      .append(bean.getKeys()).append(TraceConstants.FIELD_SPLITOR);//
    }
}

軌跡就是按照上述順序拼接而成,各個字段使用1分隔,每一條記錄使用2結尾。

1.3.2.3 SubAfter(消息消費后)
case SubAfter: {
    for (TraceBean bean : ctx.getTraceBeans()) {
        sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//
          .append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)//
          .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//
          .append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)//
          .append(ctx.isSuccess()).append(TraceConstants.CONTENT_SPLITOR)//
          .append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)//
          .append(ctx.getContextCode()).append(TraceConstants.FIELD_SPLITOR);
        }
    }
}

格式編碼一樣,就不重複多說。

經過上面的源碼跟蹤,消息發送端的消息軌跡跟蹤流程、消息軌跡數據編碼協議就清晰了,接下來我們使用一張序列圖來結束本部分的講解。

其實行文至此,只關注了消息發送的消息軌跡跟蹤,消息消費的軌跡跟蹤又是如何呢?其實現原理其實是一樣的,就是在消息消費前後執行特定的鈎子函數,其實現類為ConsumeMessageTraceHookImpl,由於其實現與消息發送的思路類似,故就不詳細介紹了。

2、 消息軌跡數據如何存儲

其實從上面的分析,我們已經得知,RocketMQ的消息軌跡數據存儲在到Broker上,那消息軌跡的主題名如何指定?其路由信息又怎麼分配才好呢?是每台Broker上都創建還是只在其中某台上創建呢?RocketMQ支持系統默認與自定義消息軌跡的主題。

2.1 使用系統默認的主題名稱

RocketMQ默認的消息軌跡主題為:RMQ_SYS_TRACE_TOPIC,那該Topic需要手工創建嗎?其路由信息呢?

{
    if (this.brokerController.getBrokerConfig().isTraceTopicEnable()) {    // @1
        String topic = this.brokerController.getBrokerConfig().getMsgTraceTopicName();
        TopicConfig topicConfig = new TopicConfig(topic);
        this.systemTopicList.add(topic);
        topicConfig.setReadQueueNums(1);                                              // @2
        topicConfig.setWriteQueueNums(1);
        this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
    }
}

上述代碼出自TopicConfigManager的構造函數,在Broker啟動的時候會創建topicConfigManager對象,用來管理topic的路由信息。

代碼@1:如果Broker開啟了消息軌跡跟蹤(traceTopicEnable=true)時,會自動創建默認消息軌跡的topic路由信息,注意其讀寫隊列數為1。

2.2 用戶自定義消息軌跡主題

在創建消息發送者、消息消費者時,可以显示的指定消息軌跡的Topic,例如:

public DefaultMQProducer(final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace,final String customizedTraceTopic)

public DefaultMQPushConsumer(final String consumerGroup, RPCHook rpcHook,
        AllocateMessageQueueStrategy allocateMessageQueueStrategy, boolean enableMsgTrace, final String customizedTraceTopic)

通過customizedTraceTopic來指定消息軌跡Topic。

溫馨提示:通常在生產環境上,將不會開啟自動創建主題,故需要RocketMQ運維管理人員提前創建好Topic。

好了,本文就介紹到這裏了,本文詳細介紹了RocktMQ消息軌跡的實現原理,下一篇,我們將進入到多副本的學習中。

作者介紹:
丁威,《RocketMQ技術內幕》作者,RocketMQ 社區佈道師,公眾號: 維護者,目前已陸續發表源碼分析Java集合、Java 併發包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等源碼專欄。

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

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

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

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

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