一次向linux開源社區提交補丁的經歷

背景

在開發過程中,偶然發現了spinand驅動的一個bug,滿懷欣喜地往社區提補丁。這是怎麼樣的一個bug呢?

static int spinand_mtd_read(struct mtd_info *mtd, loff_t from,
                            struct mtd_oob_ops *ops)
{
        ......
        nanddev_io_for_each_page(nand, from, ops, &iter) {
                ......
                ret = spinand_read_page(spinand, &iter.req, enable_ecc);
                if (ret < 0 && ret != -EBADMSG)     /* 讀取數據出錯 */
                        break;

                if (ret == -EBADMSG) {
                        /* -EBADMSG 返回表示壞塊 */
                        ecc_failed = true;
                        mtd->ecc_stats.failed++;
                        ret = 0;
                } else {
                        /* 出現位翻轉或者讀取正常,則記錄歷史位翻轉最大值 */
                        mtd->ecc_stats.corrected += ret;
                        max_bitflips = max_t(unsigned int, max_bitflips, ret);
                }

                ops->retlen += iter.req.datalen; 
                ops->oobretlen += iter.req.ooblen;
        }

        if (ecc_failed && !ret)
                ret = -EBADMSG;

        return ret ? ret : max_bitflips;
}

代碼邏輯如下:

  1. 遍歷讀取每一個page
  2. 如果讀出錯則直接返回
  3. 如果出現壞塊,則置位ecc_failed,在函數最後會檢查此標誌
  4. 如果出現位翻轉,則暫存最大位翻轉的bit位數量
  5. 全部讀取完后,如果有置位ecc_failed,則返回壞塊錯誤碼;如果出現位翻轉,則返回最大位翻轉;否則返回0,表示正常

問題出在於,如果剛好最後一次讀取出現位翻轉,此時ret != 0就直接退出循環,此時會導致壞塊標識無效,且返回最後的位翻轉量而非歷史位翻轉最大值。這是代碼不嚴謹的地方。

第一次提交

修改補丁如下,補丁邏輯不再解釋。

In function spinand_mtd_read, if the last page to read occurs bitflip,
this function will return error value because veriable ret not equal to 0.

Signed-off-by: liaoweixiong <liaoweixiong@allwinnertech.com>
---
 drivers/mtd/nand/spi/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/mtd/nand/spi/core.c b/drivers/mtd/nand/spi/core.c
index 556bfdb..6b9388d 100644
--- a/drivers/mtd/nand/spi/core.c
+++ b/drivers/mtd/nand/spi/core.c
@@ -511,12 +511,12 @@ static int spinand_mtd_read(struct mtd_info *mtd, loff_t from,
        if (ret == -EBADMSG) {
            ecc_failed = true;
            mtd->ecc_stats.failed++;
-           ret = 0;
        } else {
            mtd->ecc_stats.corrected += ret;
            max_bitflips = max_t(unsigned int, max_bitflips, ret);
        }
 
+       ret = 0;
        ops->retlen += iter.req.datalen;
        ops->oobretlen += iter.req.ooblen;
    }

21:13分發出的郵件,21:45分陸續收到兩個回復:

<maintainer A>:

Actually, that's exactly what the MTD core expects (see [1]), so you're
the one introducing a regression here.
<maintainer B>:

To me it looks like the patch description is somewhat incorrect, but the 
fix itself looks okay, unless I'm getting it wrong.

In case of the last page containing bitflips (ret > 0), 
spinand_mtd_read() will return that number of bitflips for the last 
page. But to me it looks like it should instead return max_bitflips like 
it does when the last page read returns with 0.

以及隔天回復

<maintainer A>:

Oh, you're right. liaoweixiong, can you adjust the commit message
accordingly?

好吧,問題出在與我沒把問題描述清楚,改改再提交

第二次提交

只改了comment和補丁標題:

Subject: [PATCH v2] mtd: spinand: read return badly if the last page has bitflips

In case of the last page containing bitflips (ret > 0), 
spinand_mtd_read() will return that number of bitflips for the last 
page. But to me it looks like it should instead return max_bitflips like 
it does when the last page read returns with 0.

然後嘩啦啦收到兩個Reviewed-by,附帶一個建議:

Reviewed-by: <maintainer B>

This should probably be resent with the following tags:

Cc: stable@vger.kernel.org
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI 
NANDs")

得,再提交一次吧

第三次提交

此時的我提交補丁到社區經驗並不多,Maintainer讓我resend,我就忐忑開始胡思亂想了:

版本號需要累加么?該怎麼標記是重新發送?有兩個maintainer已經”認可”了我的補丁(reviewed-by),我改怎麼體現到新的郵件中?

仔細想想內容並沒改,因此不需要累加版本號;查詢前人提交,在郵件標題可以加上RESEND字樣;搜索含RESEND字樣的前人郵件,剛好找到一個在maintainer reviewed后resend為acked,寫在signed-off-by區。

OK,確定下來就重新發吧

Subject: [RESEND PATCH v2] mtd: spinand: read return badly if the last page has bitflips

......
Signed-off-by: liaoweixiong <liaoweixiong@allwinnertech.com>
Acked-by: <maintainer A>
Acked-by: <maintainer B>
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

很快,就挨批了…

第四次提交

晚上10點多,收到回復:

<maintainer B>

Why did you change our Reviewed-by tags to Acked-by tags?

額…我也是看別人這麼做我才這麼做的,大佬生氣了!趕緊補救

......
Reviewed-by: <maintainer A>
Reviewed-by: <maintainer B>
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

第五次提交

埋下的坑終究是要踩的,很快,再次挨批了

<maintainer C>

This is not the correct way to submit patches for inclusion in the
stable kernel tree.  Please read:
    https://www.kernel.org/doc/html/latest/process/stable-kernel-rules.html
for how to do this properly.
<maintainer A>

FYI, you should not send the patch to stable@vger.kernel.org, but 
instead, as I said in my other reply, add the tag "Cc: 
stable@vger.kernel.org". See "Option 1" in the document Greg referred to.

小白趕緊狠補基礎操作規範…

第六次提交

......
Reviewed-by: <maintainer A>
Reviewed-by: <maintainer B>
Cc: stable@vger.kernel.org
Fixes: 7529df465248 ("mtd: nand: Add core infrastructure to support SPI NANDs")

總結

哎,我只是挪了一行代碼的位置而已啊,Maintainer嚴審下,我竟然提交了6次!6次!突然感覺心好累。

累歸累,問題總結還是需要的

  1. 新手不具備提交代碼的一些常識,包括 a) 提交中各個tag的含義,在什麼時候加這些tag,例如Reviewed-by和Acked-by的差別 b) 提交補丁到stable的注意事項
  2. 對補丁的問題描述不夠仔細清楚,導致 無法理解,幸好 幫我澄清了

解決方法:

  1. linux提交有規範文檔的,抽時間擼一遍,並翻譯發博客
  2. 在發補丁之前,讓身邊的人幫忙看一下補丁說明是否足夠清晰

希望我的經歷能幫助到正在或者準備向Linux內核開源社區的小夥伴

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

多線程的問題都曾經困擾過每個開發人員,今天將從全新視角來解說,希望讀者都能明白。

強烈建議去運行下文章中的示例代碼,自己體會下。



問題究竟出在哪裡?

一個線程執行,固然是安全的,但是有時太慢了,怎麼辦?

老祖宗告訴我們,“一方有難,八方支援”,那不就是多叫幾個線程來幫忙嘛,好辦呀,多new幾個不就行了,又不要錢。這樣能管用嗎?繼續往下看。

俗話說,“在家靠父母,出門靠朋友”。有了朋友的幫助,就會事半功倍。是這樣的嗎?

不一定,如果朋友“不靠譜”,結果竟是在“添亂”。於是就演變為,“不怕神一樣的對手,就怕豬一樣的隊友”。可見“人多力量大”縱然是對的,但也要配合好才能成事。

人和人是朋友,那線程和線程也是“朋友”,如果多線程之間不能配合好的話,最終也會變為“豬一樣的隊友”。事實證明,這也不是一件易事。且容我慢慢道來。

開發是一門技術,管理是一門藝術。也許你正想帶着兄弟們大幹一場,可偏偏就有人要辭職。或者你付出了這麼多,但別人從來沒有感動過。為什麼會這樣呢?

因為你面對的是人。每個人都是獨立的個體,有思想,有靈魂,有情感,有三觀。能夠接受外界的“輸入”,經過“處理”后,能夠產生“輸出”。

說白了就是會自主的分析問題,並做出決定。這叫什麼呢?答案就是,主觀能動性。

擁有主觀能動性的物體(比如人),你需要和它協商着或配合著來共同完成一件事情,而不能“強迫”它去做什麼,因為這樣往往不會有好的結果。

費了這麼多口舌,就是希望把問題盡量的簡單化。終於可以回到程序了,那線程的情況是不是類似的呢?答案是肯定的。

一個線程準備好后,經過CPU的調度,就可以自主的運行了。此時它儼然成了一個獨立的個體,且具有主觀能動性。

這本是一件好事,但卻也有不好的一面,那就是你對它的“掌控”能力變弱了,頗有一種“將在外,君命有所不受”的感覺。

可能你不同意這種看法,說我可以“強迫”它停止運行,調用Thread類的stop()方法來直接把它“掐死”,不好意思,該方法已廢棄。

因為線程可能在運行一些“關鍵”代碼(比如轉賬),此刻不能被終止。Thread類還有一些其它的方法也都廢棄了,大抵原因其實都差不多。

講了這麼多,相信你已經明白了,簡單總結一下:

事情起因:線程可以獨立自主的運行,可以認為它具有主觀能動性。

造成結果:對它的掌控能力變弱了,而且又不能直接把它“幹掉”。

解決方案:凡事商量着來,互相配合著把事情完成。

作者觀點:其實就是把線程當作人來對待。



小試牛刀一下

一旦把線程當成人,就來到了人類的世界,這我們太熟悉了,所以很多問題都會變得非常簡單明了。一起來看看吧。

場景一,停止

“大胖,大胖,12點了,該去吃飯了,別寫了”

“好的,好的,稍等片刻,把這幾行代碼寫完就走”

要點:把停止的信號傳達給別人,別人處理完手頭的事情就自己主動停止了。

 static void stopByFlag() {
    ARunnable ar = new ARunnable();
    new Thread(ar).start();
    ar.tellToStop();
  }

  static class ARunnable implements Runnable {

    volatile boolean stop;

    void tellToStop() {
      stop = true;
    }

    @Override
    public void run() {
      println("進入不可停止區域 1。。。");
      doingLongTime(5);
      println("退出不可停止區域 1。。。");
      println("檢測標誌stop = %s", String.valueOf(stop));
      if (stop) {
        println("停止執行");
        return;
      }
      println("進入不可停止區域 2。。。");
      doingLongTime(5);
      println("退出不可停止區域 2。。。");
    }

  }

 

解說:線程在預設的地點檢測flag,來決定是否停止。


場景二,暫停/恢復

“大胖,大胖,先別發請求了,對方服務器快掛了”

“好的,好的,等這個執行完就不發了”

過了一會

“大胖,大胖,可以重新發請求了”

“好的,好的”

要點:把暫停的信號傳達給別人,別人處理完手頭的事情就自己主動暫停了。但是恢復是無法自主進行的,只能由操作系統來恢複線程的執行。

 

static void pauseByFlag() {
    BRunnable br = new BRunnable();
    new Thread(br).start();
    br.tellToPause();
    sleep(8);
    br.tellToResume();
  }

  static class BRunnable implements Runnable {

    volatile boolean pause;

    void tellToPause() {
      pause = true;
    }

    void tellToResume() {
      synchronized (this) {
        this.notify();
      }
    }

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
      println("檢測標誌pause = %s", String.valueOf(pause));
      if (pause) {
        println("暫停執行");
        try {
          synchronized (this) {
            this.wait();
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        println("恢復執行");
      }
      println("進入不可暫停區域 2。。。");
      doingLongTime(5);
      println("退出不可暫停區域 2。。。");
    }

  }

解說:還是在預設的地點檢測flag。然後就是wait/notify配合使用。


場景三,插隊

“大胖,大胖,讓我站到你前面,不想排隊了”

“好吧”

要點:別人插隊到你前面,必須等他完事後才輪到你。

static void jqByJoin() {
    CRunnable cr = new CRunnable();
    Thread t = new Thread(cr);
    t.start();
    sleep(1);
    try {
      t.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    println("終於輪到我了");
  }

  static class CRunnable implements Runnable {

    @Override
    public void run() {
      println("進入不可暫停區域 1。。。");
      doingLongTime(5);
      println("退出不可暫停區域 1。。。");
    }

  }

 

解說:join方法可以讓某個線程插到自己前面,等它執行完,自己才會繼續執行。


場景四,叫醒

“大胖,大胖,醒醒,醒醒,看誰來了”

“誰啊,我去”

要點:要把別人從睡夢中叫醒,一定要採取稍微暴力一點的手段。

static void stopByInterrupt() {
    DRunnable dr = new DRunnable();
    Thread t = new Thread(dr);
    t.start();
    sleep(2);
    t.interrupt();
  }

  static class DRunnable implements Runnable {

    @Override
    public void run() {
      println("進入暫停。。。");
      try {
        sleep2(5);
      } catch (InterruptedException e) {
        println("收到中斷異常。。。");
        println("做一些相關處理。。。");
      }
      println("繼續執行或選擇退出。。。");
    }

  }

 

解說:線程在sleep或wait時,是處於無法交互的狀態的,此時只能使用interrupt方法中斷它,線程會被激活並收到中斷異常。



常見的協作配合

上面那些場景,其實都是對一個線程的操作,下面來看多線程間的一些配合。

事件一,考試

假設今天考試,20個學生,1個監考老師。規定學生可以提前交卷,即把卷子留下,直接走人就行了。

但老師必須等到所有的學生都走後,才可以收卷子,然後裝訂打包。

如果把學生和老師都看作線程,就是1個線程和20個線程的配合問題,即等20個線程都結束了,這1個線程才開始。

比如20個線程分別在計算數據,等它們都結束后得到20个中間結果,最後這1個線程再進行後續匯總、處理等。

  static final int COUNT = 20;
  static CountDownLatch cdl = new CountDownLatch(COUNT);

  public static void main(String[] args) throws Exception {
    new Thread(new Teacher(cdl)).start();
    sleep(1);
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Student(i, cdl)).start();
    }
    synchronized (ThreadCo1.class) {
      ThreadCo1.class.wait();
    }
  }

  static class Teacher implements Runnable {

    CountDownLatch cdl;

    Teacher(CountDownLatch cdl) {
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("老師髮捲子。。。");
      try {
        cdl.await();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      println("老師收卷子。。。");
    }

  }

  static class Student implements Runnable {

    CountDownLatch cdl;
    int num;

    Student(int num, CountDownLatch cdl) {
      this.num = num;
      this.cdl = cdl;
    }

    @Override
    public void run() {
      println("學生(%d)寫卷子。。。", num);
      doingLongTime();
      println("學生(%d)交卷子。。。", num);
      cdl.countDown();
    }

  }

 

解說:每完成一個線程,計數器減1,當減到0時,被阻塞的線程自動執行。


事件二,旅遊

最近景色宜人,公司組織去登山,大夥都來到了山腳下,登山過程自由進行。

但為了在特定的地點拍集體照,規定1個小時后在半山腰集合,誰最後到的,要給大家表演一個節目。

然後繼續登山,在2個小時后,在山頂集合拍照,還是誰最後到的表演節目。

接着開始下山了,在2個小時后在山腳下集合,點名回家,最後到的照例表演節目。

  static final int COUNT = 5;
  static CyclicBarrier cb = new CyclicBarrier(COUNT, new Singer());

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < COUNT; i++) {
      new Thread(new Staff(i, cb)).start();
    }
    synchronized (ThreadCo2.class) {
      ThreadCo2.class.wait();
    }
  }

  static class Singer implements Runnable {

    @Override
    public void run() {
      println("為大家唱歌。。。");
    }

  }

  static class Staff implements Runnable {

    CyclicBarrier cb;
    int num;

    Staff(int num, CyclicBarrier cb) {
      this.num = num;
      this.cb = cb;
    }

    @Override
    public void run() {
      println("員工(%d)出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點一。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點二。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)再出發。。。", num);
      doingLongTime();
      println("員工(%d)到達地點三。。。", num);
      try {
        cb.await();
      } catch (Exception e) {
        e.printStackTrace();
      }
      println("員工(%d)結束。。。", num);
    }

  }

 

解說:某個線程到達預設點時就在此等待,等所有的線程都到達時,大家再一起向下個預設點出發。如此循環反覆下去。


事件三,勞動

大胖和小白去了創業公司,公司為了節約開支,沒有請專門的保潔人員。讓員工自己掃地和擦桌。

大胖覺得擦桌輕鬆,就讓小白去掃地。可小白覺得掃地太累,也想擦桌。

為了公平起見,於是決定,每人先干一半,然後交換工具,再接着干對方剩下的那一個半。

  static Exchanger<Tool> ex = new Exchanger<>();

  public static void main(String[] args) throws Exception {
    new Thread(new Staff("大胖", new Tool("笤帚", "掃地"), ex)).start();
    new Thread(new Staff("小白", new Tool("抹布", "擦桌"), ex)).start();
    synchronized (ThreadCo3.class) {
      ThreadCo3.class.wait();
    }
  }

  static class Staff implements Runnable {

    String name;
    Tool tool;
    Exchanger<Tool> ex;

    Staff(String name, Tool tool, Exchanger<Tool> ex) {
      this.name = name;
      this.tool = tool;
      this.ex = ex;
    }

    @Override
    public void run() {
      println("%s拿的工具是[%s],他開始[%s]。。。", name, tool.name, tool.work);
      doingLongTime();
      println("%s開始交換工具。。。", name);
      try {
        tool = ex.exchange(tool);
      } catch (Exception e) {
        e.printStackTrace();
      }

      println("%s的工具變為[%s],他開始[%s]。。。", name, tool.name, tool.work);
    }

  }

  static class Tool {

    String name;
    String work;

    Tool(String name, String work) {
      this.name = name;
      this.work = work;
    }

  }

 

解說:兩個線程在預設點交換變量,先到達的等待對方。


事件四,魔性遊戲

這是一個充滿魔性的小遊戲,由一個團隊一起參加。所有人每隔5秒鐘抽一次簽,每個人有50%的概率留下來或被淘汰。

留下來的人下次抽籤時同樣有50%的概率被淘汰。被淘汰的人下次抽籤時同樣有50%的概率復活。

團隊所有成員都被淘汰完,為挑戰失敗,團隊所有成員都回到遊戲中(除剛開始外),為挑戰成功。

比如一開始10人參與遊戲,第一輪抽籤后,6人留下,4人淘汰。

第二輪抽籤后,留下的6人中4人被淘汰,淘汰的4人中2人復活,那麼目前是4人在遊戲中,6人被淘汰。

一直如此繼續下去,直到10人全部被淘汰,或全部回到遊戲中。

可見,人數越多,全部被淘汰的概率越小,但全部回到遊戲中的概率也越小。

反之,人數越少,全部回到遊戲中的概率越大,但全部被淘汰的概率也越大。

是不是很有魔性啊。哈哈。

  static final int COUNT = 6;
  static Phaser ph = new Phaser() {
    protected boolean onAdvance(int phase, int registeredParties) {
      println2("第(%d)局,剩餘[%d]人", phase, registeredParties);
      return registeredParties == 0 ||
          (phase != 0 && registeredParties == COUNT);
    };
  };

  public static void main(String[] args) throws Exception {
    new Thread(new Challenger("張三")).start();
    new Thread(new Challenger("李四")).start();
    new Thread(new Challenger("王五")).start();
    new Thread(new Challenger("趙六")).start();
    new Thread(new Challenger("大胖")).start();
    new Thread(new Challenger("小白")).start();
    synchronized (ThreadCo4.class) {
      ThreadCo4.class.wait();
    }
  }

  static class Challenger implements Runnable {

    String name;
    int state;

    Challenger(String name) {
      this.name = name;
      this.state = 0;
    }

    @Override
    public void run() {
      println("[%s]開始挑戰。。。", name);
      ph.register();
      int phase = 0;
      int h;
      while (!ph.isTerminated() && phase < 100) {
        doingLongTime(5);
        if (state == 0) {
          if (Decide.goon()) {
            h = ph.arriveAndAwaitAdvance();
            if (h < 0)
              println("No%d.[%s]繼續,但已勝利。。。", phase, name);
            else
              println("No%d.[%s]繼續at(%d)。。。", phase, name, h);
          } else {
            state = -1;
            h = ph.arriveAndDeregister();
            println("No%d.[%s]退出at(%d)。。。", phase, name, h);
          }
        } else {
          if (Decide.revive()) {
            state = 0;
            h = ph.register();
            if (h < 0)
              println("No%d.[%s]復活,但已失敗。。。", phase, name);
            else
              println("No%d.[%s]復活at(%d)。。。", phase, name, h);
          } else {
            println("No%d.[%s]沒有復活。。。", phase, name);
          }
        }
        phase++;
      }
      if (state == 0) {
        ph.arriveAndDeregister();
      }
      println("[%s]結束。。。", name);
    }

  }

  static class Decide {

    static boolean goon() {
      return random(9) > 4;
    }

    static boolean revive() {
      return random(9) < 5;
    }
  }

 

解說:某個線程到達預設點后,可以選擇等待同伴或自己退出,等大家都到達后,再一起向下一個預設點出發,隨時都可以有新的線程加入,退出的也可以再次加入。



生產與銷售的問題

在現實中,工廠生產出來的產品會先放到倉庫存儲,銷售人員簽了單子后,會從倉庫把產品發給客戶。

如果生產的過快,倉庫里產品越堆越多,直到把倉庫堆滿,那就必須停止生產,因為沒地方放了。

此時只能讓銷售人員趕緊出去簽單子,把產品發出去,倉庫就有了空間,可以恢復生產了。

如果銷售的過快,倉庫里產品越來越少,直到把倉庫清空,那就必須停止銷售,因為沒產品了。

此時只能讓生產人員趕緊生產產品,把產品放到倉庫里,倉庫里就有了產品,可以恢復銷售了。

可能會有人問,為什麼不讓生產和銷售直接掛鈎呢,把倉庫這個環節去掉?

這樣會造成兩種不好的情況:

一是突然來了很多單子,生產人員累成死Dog也生產不出來。

二是很長時間沒有單子,生產人員閑成廢Dog也無事可做。

用稍微“專業”點的術語就是此時的生產和銷售是一種強耦合的關係,銷售的波動對生產影響太大。

倉庫就是一個緩衝區,能有效的吸收波動,很大程度上減少波動的傳遞,起到一種解耦作用,由強耦合變成一種鬆散耦合。

這其實就對應計算機里經典的生產者和消費者問題。

經典的生產者和消費者

一到多個線程充當生產者,生產元素。一到多個線程充當消費者,消費元素。

在兩者之間插入一個隊列(Queue)充當緩衝區,建立起生產者和消費者的鬆散耦合。

正常情況下,即生產元素的速度和消費元素的速度差不多時,生產者和消費者其實是不需要去關注對方的。

生產者可以一直生產,因為隊列里總是有空間。消費者可以一直消費,因為隊列里總是有元素。即達到一個動態的平衡。

但在特殊情況下,比如生產元素的速度很快,隊列里沒有了空間,此時生產者必須自我“ba工”,開始“睡大覺”。

一旦消費者消費了元素之後,隊列里才會有空間,生產者才可以重啟生產,所以,消費者在消費完元素後有義務去叫醒生產者復工。

更準確的說法應該是,只有在生產者“睡大覺”時,消費者消費完元素后才需要去叫醒生產者。否則,其實可以不用叫醒,因為人家本來就沒睡。

反之,如果消費元素的速度很快,隊列里沒有了元素,只需把上述情況顛倒過來即可。

但這樣的話就會引入一個新的問題,就是要能夠準備的判斷出對方有沒有在睡大覺,為此就必須定義一個狀態變量,在自己即將開始睡大覺時,自己設置下這個變量。

對方通過檢測這個變量,來決定是否進行叫醒操作。當自己被叫醒后,首先要做的就是清除一下這個變量,表明我已經醒來複工了。

這樣就需要多維護一個變量和多了一部分判斷邏輯。可能有些人會覺得可以通過判斷隊列的“空”或“滿”(即隊列中的元素數目)來決定是否進行叫醒操作。

在高併發下,可能剛剛判斷隊列不為空,瞬間之後隊列可能已經變為空的了,這樣會導致邏輯出錯。線程可能永遠無法被叫醒。

因此,綜合所有,生產者每生產一個元素后,都會通知消費者,“現在有元素的,你可以消費”。

同樣,消費者每消費一個元素后,也會通知生產者,“現在有空間的,你可以生產”。

很明顯,這些通知很多時候(即對方沒有睡大覺時)是沒有真正意義的,不過無所謂,只要忽略它們就行了。

就是“寧可錯殺一千,也不放過一個”。首先要保證是正確的,然後才有資格去BB別的。

  public static void main(String[] args) {
    Queue queue = new Queue();
    new Thread(new Producer(queue)).start();
    new Thread(new Producer(queue)).start();
    new Thread(new Consumer(queue)).start();
  }

  static class Producer implements Runnable {

    Queue queue;

    Producer(Queue queue) {
      this.queue = queue;
    }

    @Override
    public void run() 
{
      try {
        for (int i = 0; i < 10000; i++) {
          doingLongTime();
          queue.putEle(random(10000));
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

  }

  static class Consumer implements Runnable {

    Queue queue;

    Consumer(Queue queue) {
      this.queue = queue;
    }

    @Override
    public void run() 
{
      try {
        for (int i = 0; i < 10000; i++) {
          doingLongTime();
          queue.takeEle();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }

  }

  static class Queue {
    Lock lock = new ReentrantLock();
    Condition prodCond  = lock.newCondition();
    Condition consCond = lock.newCondition();

    final int CAPACITY = 10;
    Object[] container = new Object[CAPACITY];
    int count = 0;
    int putIndex = 0;
    int takeIndex = 0;

    public void putEle(Object ele) throws InterruptedException {
      try {
        lock.lock();
        while (count == CAPACITY) {
          println("隊列已滿:%d,生產者開始睡大覺。。。", count);
          prodCond.await();
        }
        container[putIndex] = ele;
        println("生產元素:%d", ele);
        putIndex++;
        if (putIndex >= CAPACITY) {
          putIndex = 0;
        }
        count++;
        println("通知消費者去消費。。。");
        consCond.signalAll();
      } finally {
        lock.unlock();
      }
    }

    public Object takeEle() throws InterruptedException {
      try {
        lock.lock();
        while (count == 0) {
          println("隊列已空:%d,消費者開始睡大覺。。。", count);
          consCond.await();
        }
        Object ele = container[takeIndex];
        println("消費元素:%d", ele);
        takeIndex++;
        if (takeIndex >= CAPACITY) {
          takeIndex = 0;
        }
        count--;
        println("通知生產者去生產。。。");
        prodCond.signalAll();
        return ele;
      } finally {
        lock.unlock();
      }
    }
  }

 

解說:其實就是對await/signalAll的應用,幾乎面試必問。


源代碼:

https://github.com/coding-new-talking/java-code-demo.git

 

 

(END)

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!

       

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

Python深入淺出property特性屬性

導語

在Java中,通常在類中定義的成員變量為私有變量,在類的實例中不能直接通過對象.屬性直接操作,而是要通過getter和setter來操作私有變量。
而在Python中,因為有property這個概念,所以不需要寫getter和setter一堆重複的代碼來操作私有變量。Python“私有變量”通常在變量前加上“_”或者“__”,例如_attr或者__attr,這是約定俗成的規範。

把私有屬性變成只讀特性

class MyClass:

    def __init__(self, x):
        self._x = x

這裏定義了一個MyClass類,它有一個實例變量_x,綁定了用戶傳來的x值。_x是私有變量,通過obj._x獲取私有變量不符合語言規範,進而我們要使_x變成property(特性),通過obj.x直接訪問。

改造后的代碼如下:

class MyClass:

    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x
    
>>> obj = MyClass(10)
>>> obj.x
10

我們把_x變成了property特性,以只讀的方式獲取x的值。

我們現在想為x賦值該怎樣做呢?

>>> obj.x = 999
Traceback (most recent call last):
  File "xxx", line 14, in <module>
    obj.x = 23
AttributeError: can't set attribute

可以看到,拋出了AttributeError: can’t set attribute。顯然,只讀方法不支持賦值。

把私有變量變成可賦值的特性

我們只需要在上述代碼改造成:

class MyClass:

    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x
    
    @x.setter
    def x(self, value):
        self._x = value

>>> obj = MyClass(10)
>>> obj.x = 999
>>> obj.x
999

可以看到,我們為x添加了setter,可以直接為obj.x賦值操作

property屬性能夠遮蓋實例屬性

繼續上面的代碼,我們看看以下的操作:

>>> obj = MyClass(10)
>>> obj.__dict__
{'_x': 999}  #此時實例變量中有_x的值
>>> obj.__dict__['x'] = 99999  #設置obj的實例變量有x值,跟property屬性重名!
>>> obj.__dict__
{'_x': 999, 'x': 99999}  #此時實例變量中有_x和x的值

>>> obj.x     #結果是obj的實例變量還是property屬性?
10

如上代碼所示,obj對象有一個_x實例變量和一個x的property屬性,我們又強行為obj增加了一個x實例變量,這個實例變量x和property屬性x同名!
通過obj.x我們得知,返回的是property屬性,說明property屬性會遮蓋實例屬性!也可以理解為property屬性的優先級更大!

property類解析

我們通常使用內置的@property裝飾器。但其實property是一個類,python中類和函數的調用方式都差不多,他們都是可調用對象
property的構造方法如下:

class property(object):
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        """"""

它最大接受4個參數,都可以為空。
第一個為getter,第二個為setter,第三個為delete函數,第四個為文檔。

上述代碼的另一種寫法

class MyClass:

    def __init__(self, x):
        self._x = x

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    x = property(get_x, set_x)

>>> obj = MyClass(10)
>>> obj.x
10

如上,x是property的實例,設置了getter和setter,作為類變量放在MyClass類中。

以上就是property屬性的解析。

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

docker (2)—存儲、網絡(利用docker容器上線靜態網站)

一、docker底層依賴的核心技術

1、命名空間 (Namespaces)

2、控制組 (Control Groups)

3、聯合文件系統 (Union File System)

4、Linux 虛擬網絡支持:本地和容器內創建虛擬接口

(1) 命名空間(Namespaces):

實現了容器間資源的隔離,每個容器擁有自己獨立的命名空間 , 運行其中的應用就像是運行在獨立的操作系統中一樣 , 我們都可以看到文件系統,網卡等資源保證了容器之間互不影響,namesaces管理進程號 , 每個進程命名空間有一套自己的進程號管理方法 , 進

程命名空間是一個父子關係的結構 , 子空間中的進程對於父空間是可見的。

(2) 控制組 (Control Groups) :

  控制組 (Control groups)–CGroups 是 Linux 內核的一個特性 ,主要用來對共享資源進行隔離、限制、審計等 。cgroups 允許對於進程或進程組公平( 不公平 ) 的分配 CPU 時間、內存分配和 I/O 帶寬。

容器通過 cgroups 來得到所能夠管理資源的分配和使用。因此容器所獲得資源僅為所有系統資源的一個部分

1、資源限制 : 內存子系統為進程組設置內存使用上限,內存達到上限后再申請內存,就會發出 Out of Memory

2、 優先級 : 通過優先級讓一些組得到更多 CPU 等資源

3、 資源審計 : 用來統計系統上實際把多少資源用到適合的目的上 , 可以使用 cpuacct 子系統記錄某個進程組使用的 CPU 時間

4、 隔離 : 為組隔離名字空間 , 這樣一個組不會看到其他組的進程 .網絡連接和文件系統

5、 控制 : 掛起 . 恢復和啟動等操作

(3)聯合文件系統 (Union File System) :

  docker 中使用AUFS(another Union File System 或 v2 版本以後的Advanced multi-layered Unification File System) 控製為每一個成員目錄設定只讀 / 讀寫 / 寫出權限 , 同時 AUFS 有一個類似分層的概念 , 對只讀權限的分支可以邏輯上進行增量的修改.

二、docker的存儲

docker兩種存儲資源類型:

1、Data Volume (數據卷)

2、Data Volume Dontainers — 數據卷容器

(1) Data Volume (數據卷):

Data Volume 本質上是 Docker Host 文件系統中的目錄或文件,使用類似與 Linux 下對目錄或者文件進行 mount 操作。數據卷可以在容器之間共享和重用,對數據卷的更改會立馬生效,對數據卷的更新不會影響鏡像,卷會一直存在,直到沒有容器使用。

Data Volume(數據卷)的特點:

1、Data Volume 是目錄或文件,而非沒有格式化的磁盤(塊設備)。

2、容器可以讀寫 volume 中的數據。

3、volume 數據可以被永久的保存,即使使用它的容器已經銷毀。

 Data Volume 的使用:

1、在宿主機根目錄下創建一個目錄(數據卷)

 

2、啟動一個容器並將數據卷掛載到容器的目錄下

 

3、驗證  ( 持久化的需要映射目錄)

 

#

 

(2)Data Volume Dontainers — 數據卷容器

數據卷容器就是一個普通的容器,只不過是專門用它提供數據卷供其他容器掛載使用

Data Volume Dontainers使用:

1、創建一個名為 dbdata 的數據卷,並在其中創建一個數據卷掛載到 /dbdata

docker run -dti -v /dbdata --name dbser centos:latest

2、再啟動兩個容器,並使用數據卷容器

docker run -dti --volumes-from dbser --name db1 centos:latest

#

3、驗證

 

#

容器 db1 和 db2 同時掛載了同一個數據卷到本地相同 /dbdata目錄。三個容器任何一個目錄下的寫入,都可以時時同步到另外兩個

三、docker 三種網絡

  docker 網絡從覆蓋範圍可分為單個 host 上的容器網絡和跨多個 host 的網絡,docker 目前提供了映射容器端口到宿主主機和容器互聯機制來為容器提供網絡服務,在啟動容器的時候,如果不指定參數,在容器外部是沒有辦法通過網絡來訪問容器內部的網絡應用和服務的

docker 安裝時會自動在host上創建三個網絡

docker   network  ls   (查看docker  網絡)

(1) docker–none網絡

none 網絡就是什麼都沒有的網絡。掛在這個網絡下的容器除了 lo,沒有其他 任何網卡。容器創建時,可以通過 –network=none 指定使用 none 網絡

 

none網絡的應用

封閉的網絡意味着隔離,一些對安全性要求高並且不需要聯網的應用可以使用 none 網絡。

(2)docker–host網絡

連接到 host 網絡的容器,共享 docker host 的網絡棧,容器的網絡配置與host 完全一樣。可以通過 –network=host 指定使用 host 網絡

 

host 網絡的應用

  直接使用 Docker host 的網絡最大的好處就是性能,如果容器對網絡傳輸效率有較高要求,就可以選擇 host 網絡。當然不便之處就是犧牲一些靈活性,比如要考慮端口衝突問題,Docker host上已經使用的端口就不能再用了。

Docker host 的另一個用途是讓容器可以直接配置 host 網路。比如某些跨host 的網絡解決方案,其本身也是以容器方式運行的,這些方案需要對網絡進行配置,比如管理 iptables

(3) docker–bridge 網絡

docker 安裝時會創建一個 命名為 docker0 的 linux bridge。如果不指定–network,創建的容器默認都會掛到 docker0 上

 

#

 

eth0@if29      與 veth04c5851 是一對 veth pair

#

  veth pair 是一種成對出現的特殊網絡設備,可以把它們想象成由一根虛擬網線連接起來的一對網卡,網卡的一頭(eth0@if29)在容器中,另一頭( veth04c5851)掛在網橋 docker0 上,其效果就是將 eth0@if29也掛在了docker0 上。

# 查看網絡配置信息 ( 設置容器ip 網段、網關)

docker network inspect bridge

 

#

注:容器創建時,docker 會自動從 172.17.0.0/16 中分配一個 IP,這裏 16 位的掩碼保證有足夠多的 IP 可以供容器。

四、創建 user-defined網絡 (自定義網絡)

通過 bridge 驅動創建類似前面默認的 bridge 網絡

1、利用bridge驅動創建名為my-net2網橋(docker會自動分配網段)

docker network create --driver bridge my-net2

# 查看網絡配置信息

 

# 查看網橋

 

2、利用bridge驅動創建名為my-net3網橋(user-defined (自定義)網段及網關)

docker network create --driver bridge --subnet 172.33.1.0/24 --gateway 172.33.1.1 my-net3

# 查看網絡配置信息

 

# 查看網橋

 

3、啟動容器使用新建的my-net3網絡

docker run -it  --network=my-net3  busybox:latest

4、啟動容器使用my-net3網絡並指定ip(只有使用 –subnet 創建的網絡才能指定靜態 IP,如果是docker自動分配的網段不可以指定ip)

docker run -it --network=my-net3  --ip 172.33.1.100  busybox:latest

 

5、讓已啟動不同vlan的busybox容器,可以連接到my-net2(其實在busybox中新建了my-net2的網卡)(添加網卡。訪問不同的網段)

 

# #docker network connect my-net3  08493ae30117   ( 連接)

6、使用–name指定啟動容器名字,可以使用docker自帶DNS通信,但只能工作在user-defined 網絡,默認的 bridge 網絡是無法使用 DNS 的。

#docker run -it --network=my-net3 --name=bbox1 busybox:latest

#docker run -it --network=my-net3 --name=bbox2 busybox:latest

7、容器之間的網絡互聯

&1、創建一個 db 容器

docker run -dti --name db centos:latest

&2、創建一個 web 容器,並使其連接到 容器db

docker run -dti --name web --link db:dblink centos:latest /bin/bash

–link db:dblink 實際是連接對端的名字和這個鏈接的名字,也就是和 db 容器建立一個叫做 dblink 的鏈接

 

# 測試  

 

注:此鏈接通信是單向的

8、容器端口映射

在啟動容器的時候,如果不指定參數,在容器外部是沒有辦法通過網絡來訪問容器內部的網絡應用和服務的,當容器需要通信時,我們可以使用 -P (大) &&-p (小)來指定端口映射

(1)   -P : Docker 會隨機映射一個 49000 ~ 49900 的端口到容器內部開放的網絡端口

(2)   -p :則可以指定要映射的端口,並且在一個指定的端口上只可以綁定一個容器。

支持的格式

 IP : HostPort : ContainerPort

 IP : : ContainerPort

 IP : HostPort :

&1、 查看映射

docker port   CONTAINER ID/NAMES

&2、映射所有接口地址,此時綁定本地所有接口上的 5200 到容器的 5200 接口,訪問任何一個本地接口的 5000 ,都會直接訪問到容器內部

docker run -dti -p 5200:5200 centos:latest  /bin/bash

&3、多次使用可以實現多個接口的映射

docker run -dti -p 5400:5400  -p 5300:5300 centos:latest  /bin/bash

&4、映射到指定地址的指定接口

此時會綁定本地 192.168.226.147 接口上的 5100 到容器的 5100 接口

docker run -dti -p 192.168.226.147:5100:5100 centos:latest /bin/bash

 

&5、映射到指定地址的任意接口

此時會綁定本地 192.168.226.147 接口上的任意一個接口到容器的 5500 接口

docker run -dti -p 192.168.226.147::5500 centos:latest /bin/bash

實驗、通過端口映射實現訪問本地的 IP:PORT 可以訪問到容器內的 web

1、將容器80端口映射到主機8090端口

docker run -itd -p 192.168.226.147:8090:80 --name http-test httpd:latest

2、查看剛運行docker容器

docker  ps

 

3、進入 容器

 

4、容器內部編輯網頁文件 index.html

 

5、到宿主機上打開瀏覽器輸入 IP:PORT 訪問驗證

http://192.168.226.147:8090/

 

6、宿主機上傳靜態網站測試文件

 

7、解壓

 

8、把解壓的目錄上傳至容器下的網站根目錄

docker cp jd 67b3daf15a40:/usr/local/apache2/htdocs

9、進入容器,刪除原來的index.html 文件

 

10、展開目錄

 

11、web 訪問

http://192.168.226.147:8090/

 

 

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

Git的使用 — 用git玩翻github,結尾有驚喜!有驚喜!有驚喜!林妙妙看了說:牛呲呼啦帶閃電 (三)(超詳解)

簡介

      上一篇主要講解的是Git安裝及配置,這一篇就詳細的從無到有的來用Git玩翻github。

一、什麼是Github

Github是全球最大的社交編程及代碼託管網站(https://github.com/)。

Github可以託管各種git庫,並提供一個web界面(用戶名.github.io/倉庫名)

二、Github和Git是什麼關係 

Git是版本控制軟件

Github是項目代碼託管的平台,藉助git來管理項目代碼

1、 使用Github

目的:藉助github託管項目代碼

2、基本概念

a、倉庫(Repository)

 倉庫的意思,即你的項目,你想在 GitHub 上開源一個項目,那就必須要新建一個 Repository ,如果你開源的項目多了,你就擁有了多個 Repositories 。

 倉庫用來存放項目代碼,每個項目對應一個倉庫,多個開源項目則有多個倉庫。

b、收藏(Star)

倉庫主頁star按鈕,意思為收藏項目的人數,收藏項目,方便下次查看,在 GitHub 上如果你有一個項目獲得100個star都算很不容易了!

【如何收藏】

 操作:打開對應項目主頁,點擊右上角  star 按鈕即可收藏

 情景:張三無意訪問到李四的開源項目感覺不錯並進行收藏

 

【如何查看自己得收藏】

 

c、複製項目(Fork)派生

這個不好翻譯,如果實在要翻譯我把他翻譯成分叉,什麼意思呢?你開源了一個項目,別人想在你這個項目的基礎上做些改進,然後應用到自己的項目中,這個時候他就可以 Fork 你的項目(打開項目主頁點擊右上角的fork按鈕即可),然後他的 GitHub 主頁上就多了一個項目,只不過這個項目是基於你的項目基礎(本質上是在原有項目的基礎上新建了一個分支),他就可以隨心所欲的去改進,但是絲毫不會影響原有項目的代碼與結構。

注意:該fork的項目時獨立存在的

比如:張三fork了李四的項目,相當於張三複制了李四的項目,所以自己也單獨有了一個一樣名稱的倉庫(注:該倉庫會聲明來自於李四,但是獨立存在)

d、發起請求(Pull Request)

發起請求,這個其實是基於 Fork 的,還是上面那個例子,如果別人在你基礎上做了改進,後來覺得改進的很不錯,應該要把這些改進讓更多的人收益,於是就想把自己的改進合併到原有項目里,這個時候他就可以發起一個 Pull Request(簡稱PR) ,原有項目創建人,也就是你,就可以收到這個請求,這個時候你會仔細review他的代碼,並且測試覺得OK了,就會接受他的PR,這個時候他做的改進原有項目就會擁有了。

e、關注(Watch)

這個也好理解就是觀察,如果你 Watch 了某個項目,那麼以後只要這個項目有任何更新,你都會第一時間收到關於這個項目的通知提醒。

f、問題(Issue)

發現代碼BUG,但是目前沒有成型代碼,需要討論時用; 問題的意思,舉個例子,就是你開源了一個項目,別人發現你的項目中有bug,或者哪些地方做的不夠好,他就可以給你提個 Issue ,即問題,提的問題多了,也就是 Issues ,然後你看到了這些問題就可以去逐個修復,修復ok了就可以一個個的 Close 掉。

g、Github主頁

賬號創建成功或點擊網址導航欄github圖標都可進入github主頁:該頁左側主要显示用戶動態以及關注用戶或關注倉庫的動態;右側显示所有的git庫

h、倉庫主頁

倉庫主頁主要显示項目的信息,如:項目代碼,版本,收藏/關注/fork情況等

i、個人主頁

個人信息:頭像,個人簡介,關注我的人,我關注的人,我關注的git庫,我的開源項目,我貢獻的開源項目等信息

3、註冊github賬號

官方網址:https://github.com

注意:

a、因為github在國外服務器所以訪問較慢或者無法訪問,需要FQ(***)

b、私有倉庫只能自己或者指定的朋友才有權限操作(私有倉庫是收費的)

c、新註冊的用戶必須驗證郵箱后才可以創建git庫倉庫

4、創建倉庫/創建新項目

說明:一個git庫(倉庫)對應一個開源項目。通過git管理git庫

a、創建倉庫

1)點擊【Start a project】創建一個倉庫

2)問題:點擊【Start a project】創建一個倉庫,后出現該頁面

2)原因:未驗證郵箱,點擊下圖框框中的鏈接進行驗證

 

3)點擊【resend】發送郵件驗證郵箱

 

 4)點擊【verify email address】驗證郵箱

   說明:驗證成功後會自動跳轉github主頁,重新點擊【Start a project】即可創建倉庫

 

5) 驗證郵箱后,點擊【Start a project】進入下圖界面

b、倉庫主頁說明

 

 

 注意:qq郵箱需要設置白名單才可以收到郵件

設置QQ郵箱白名單

1、打開QQ郵箱、點擊【設置】

2、點擊【反垃圾】

3、點擊【設置域名白名單】

4、在新頁面的input框中輸入【github.com】添加即可

5、倉庫管理

a、新建文件:倉庫主頁,點擊【create new file】創建倉庫文件

 

 

 

   

b、編輯文件:倉庫主頁,點擊【需要修改的文件】進入文件詳情頁

 

    

c、刪除文件

 

d、被刪除文件如何查看信息

答案:點擊commits按鈕查看

e、上傳文件

 

f、搜索倉庫文件:快捷鍵(t)

6、下載/檢出項目

7、Github Issues

作用:發現代碼BUG,但是目前沒有成型代碼,需要討論時用;或者使用開源項目出現問題時使用

情景:張三發現李四開源git庫,則發提交了一個issue;李四隔天登錄在github主頁看到通知並和張三交流,最後關閉issue

三、基本概念(實戰操作)

1、Github主頁

 

2、個人主頁

 

四、開源項目貢獻流程

1、新建Issue

提交使用問題或者建議或者想法

2、Pull Request

步驟:

a、 fork項目

b、 修改自己倉庫的項目代碼

c、 新建 pull request

d、 等待作者操作審核

五、下面就是驚喜:Github  Pages搭建網站

1、個人站點

訪問:https://用戶名.github.io

搭建步驟:

a、創建個人站點-》新建倉庫(倉庫名必須是【用戶名.github.io】)

 

b、在倉庫下新建index.html的文件即可

注意:a、Github Page僅支持靜態頁面

   b、倉庫裏面只能是.html文件

   c、個人主頁也可以設置主題

2、Project Pages 項目站點

訪問:https://用戶名.github.io/倉庫名

原理:gh-pages 用於構建和發布

搭建步驟

a、進入項目主頁,點擊settings

b、在settings頁面,點擊【Choose a theme 】來自動生成主題頁面

c、新建站點基礎信息設置

d、選擇主題

e、發布網頁(publish page)

六、小結

Clone和Fork的區別:

fork(派生):將別人的倉庫複製一份到自己的倉庫。
clone(克隆):將倉庫克隆到自己本地電腦中。

Fork的主要應用場景:
1.在A的倉庫中fork項目B (此時我們自己的github就有一個一模一樣的倉庫B,但是URL不同)
2.將我們修改的代碼push到自己github中的倉庫B中
3.pull request ,主人就會收到請求,並決定要不要接受你的代碼

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

在 Ubuntu 開啟 GO 程序編譯之旅

本文將使用 putty 連接到一台阿里雲 Ubuntu 16.04 服務器,在其上安裝 go 語言的編譯環境,旨在呈現從安裝到“你好,世界!”涉及的方方面面,希望完成這個過程無須覓它處。

1. 安裝

方式一使用 apt-get

apt-get install golang-go

執行完成之後,會把 golang 安裝在這個位置:/usr/lib/go-1.6/,go 命令會在該目錄的 bin 子目錄下,同時,/usr/bin 下會有該命令的文件鏈接。

當然,也許你並不知道到底安裝在哪,可以通過以下命令找找觀察判斷一下。

# 找名字為 go 的文件
find / -name go

執行 /usr/bin/go version,結果如下,显示的版本號為 go1.6.2,版本比較低。

是不是想卸載?使用以下命令可以完成卸載,跟安裝一一對應。

apt-get --purge remove golang-go

方式二使用 wget

直接下載想要的版本進行安裝,一切皆在掌控之中。通過以下兩條命令,我們把 golang 安裝在 /usr/local/go 下。

# 下載
wget https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz
# 解壓
tar -xzf go1.9.1.linux-amd64.tar.gz -C /usr/local

2. 設置環境變量

這裡會涉及到3個環境變量,分別是 PATH、GOROOT、GOPATH。
PATH,是為了讓 go 命令隨處可敲。
GOROOT,代表 golang 的根目錄,在設置PATH時可以用一下,如 export PATH=$GOROOT/bin。
GOPATH,特別重要,單獨做一節(2.2)來講。

2.1 設置

環境變量可以設置在不同的文件中。
etc/profile : 對所有用戶生效
~/.profile : 對當前用戶生效

配置在哪都行,能用到即可。在配置文件末尾加上以下文本。

export GOROOT=/usr/local/go
export GOPATH=/usr/goprojs
export PATH=$GOROOT/bin:$PATH:$GOPATH/bin

GOPATH、PATH 多個路徑,中間使用冒號分隔。
配置完成后,使用source ~/.profile 讓其立即生效。

2.2 GOPATH

GOPATH 是GO程序找依賴包的路徑。
其子目錄 src 中可放置各個包的源碼,編譯時會通過 GOPATH 去引用它們。
子目錄 bin 則是編譯之後的可執行文件,在PATH 里要加上各$GOPATH/bin 可以讓編譯的運行文件在執行搜索路徑範圍內方便執行。
子目錄 pkg,編譯包的中間文件,不太關心它。

GOPATH 的第一個路徑特別重要。
使用 go get 下載的包都會安裝在第一個路徑,所以如果想讓公共包統一在某處,應該要為它單獨建立一個路徑作為GOPATH的第一個路徑,從而使得 go get 總去向那裡。實際項目最好另建路徑加入GOPATH,這樣即在引用範圍 go get 又影響不到。

附 go get 可帶參數:
|參數|描述|
|——|——|
| -v |显示操作流程的日誌及信息 |
| -u |僅下載丟失的包,不更新已存在的 |
| -d | 只下載,不安裝 |
| -insecure | 允許使用HTTP,而不一定要HTTPS |

3. 你好,世界!

3.1 編寫代碼

建立代碼文件。點此可以在線嘗鮮 GO 代碼

vi hello.go
// 輸入以下代碼保存
package main
import "fmt"

func main(){
    fmt.Println("Hello world!")
}

3.2 執行

直接在文件目錄執行以下命令運行。

go run hello.go
// 或者
go build hello.go
./hello

4. 附件

設置環境變量的配置文件,有網友總結:

/etc/profile,/etc/bashrc 是系統全局環境變量設定
~/.profile,~/.bashrc用戶家目錄下的私有環境變量設定
當登入系統時候獲得一個shell進程時,其讀取環境設定檔有三步
1).首先讀入的是全局環境變量設定檔/etc/profile,然後根據其內容讀取額外的設定的文檔,如
/etc/profile.d和/etc/inputrc
2).然後根據不同使用者帳號,去其家目錄讀取~/.bash_profile,如果這讀取不了就讀取~/.bash_login,這個也讀取不了才會讀取
~/.profile,這三個文檔設定基本上是一樣的,讀取有優先關係
3).然後在根據用戶帳號讀取~/.bashrc
~/.profile與~/.bashrc的區別
都具有個性化定製功能
~/.profile可以設定本用戶專有的路徑,環境變量,等,它只能登入的時候執行一次
~/.bashrc也是某用戶專有設定文檔,可以設定路徑,命令別名,每次shell script的執行都會使用它一次

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

由老同事學習SAP所想到的

前段時間一位老同事在微信上跟我說他們公司正計劃導SAP系統,但整個IT中心幾乎無人使用過SAP,知道我在這行業幹了多年了,所以想問我怎麼開始學習。於是我約他今天出來聊聊,順便把手裡的SAP ECC EHP6版本的虛擬機拷給他自己先自學。 

他們公司一直都是在用九二年版的QAD系統(美國ERP廠商),跟之前我們同事的那家企業系統一致,非常古老的系統,不支持鼠標操作,基本上現在ERP系統該有的功能它都沒有,唯一好處的是開源可開發。公司老闆不知道從哪裡交流了一下,然後打算大刀闊斧大幹一場,改革目前信息化現狀,為將來業務擴展做信息化支撐。 

一直以來他都是做ERP行業,接觸過多個模塊,現在這個公司可能是因為體量小的原因,一個人幾乎全管了所有的模塊,業務能力很紮實,對企業的流程和供應鏈非常熟悉。看我給他演示了一下基礎的SAP操作和邏輯,一直驚呼SAP的強大。

 

 SAP的龐大複雜對於一個從來沒接觸到人來說門檻還是相當高的,這個門檻並不是看幾本PDF、看幾個視頻、上上培訓機構就能越過得了的,其中包含的後台邏輯配置和各種強關聯絕對會把一個人打蒙。想起前幾年碰到一個啥都不懂的信息化管理者,在ERP選型會議上跟演示系統的供應商要求在企業內部安裝一套空白的ERP試用,想想這真是一大笑柄。

 這持續枯燥乏味的學習過程絕對非常考驗一個人的毅力。想起十多年前,為了學習SAP,我從騰訊拍拍上花了600元買SAP ECC的安裝包,含視頻教程差不多三十多張DVD光盤,升級了老爺筆記本配置(酷睿雙核、4G內存、500G机械硬盤),安裝Windows Server,安裝Java,安裝MSSQL,安裝SAP,通宵安裝了十五六個小時才搞定,佔用硬盤空間220G,一開啟SAP服務整個電腦就得卡死半個小時,CPU直接100%,內存爆滿。

之後對着SAP GUI界面一臉懵逼,根本不知道怎麼下手。雖然我知道部分ERP的流程和功能,但我根本不知道怎麼弄。看購買回來的視頻也是一臉懵逼,因為系統裏面的組織配置跟視頻教程里根本就不一樣,真要操作起來困難重重,各種紅燈錯誤,這也不行那也不行,那種深深的絕望感至今歷歷在目。

 

後來跌跌撞撞學了一點ABAP開發,由於沒有實際的工作經歷,也只是懂個ABAP開發的一絲絲皮毛而已。那時候沒有SAP前輩先驅可以交流,沒有QQ群,連熱鬧一點的論壇都沒有,夜以繼日枯燥得學習才進步這麼點,支撐起我這份毅力恆心的大概就是“生存”壓力吧。一心想離開那時候的工作環境,不願被溫水煮死。

後來在廈門面試了一家正在實施SAP的企業,面試的主管給我出了一道SAP開發的題目,非常簡單的數據查詢我都沒能做出來,好在他們給了我機會讓我回去用自己的電腦做題。回去之後我狂惡補知識,當晚做題到凌晨,將源碼發郵件給那位主管,第二天早上接到他們複試的通知,於是第二輪面試的時候我也很幸運成功解決了ABAP的問題,就這樣開始跟SAP結緣了。

 

為了不讓主管失望,覺得我SAP技術是半桶水,那時候我瘋狂加班,下班回來也利用自己電腦的SAP狂學習,不停研究顧問開發的代碼,看到不熟悉的語法就記下來百度,做各種嘗試測試。恰好那時候公司要開發三支程序,顧問那邊報價十多萬台幣。於是我自告奮勇,跟主管說我來開發。然後就是瘋狂的查閱資料,查看SAP官方英文文檔,系統測試,順利得完成了任務。短短2個月就給公司省了十多萬的開發費用,且提前了一個月轉正。不得不說,不逼一下自己都不知道自己原來可以如此優秀。

再後來跳槽去做業務模塊做項目了,開始是做MM模塊,實施和運維過程中遇到過各種各樣的問題,也深深感受到了SAP的強大,後來又接觸了SD模塊,Basis模塊等。我覺得一個SAP顧問如果不精通一兩個模塊,其他模塊如果不熟悉的話,是很沒優勢的。這個過程中累積的各種筆記和實施運維實錄有五六百兆,上千篇文檔。

就這樣曲曲折折這麼些年,非常成功的項目也有,失敗的項目也有,見識到了形形色色的SAP顧問和關鍵用戶,這些都變成了自己非常寶貴的經驗。一個顧問如果沒有經歷過失敗的項目,那就是失敗的!

 

當然,之前兩年半的QAD運維並非全是沒用的,至少讓我懂得了部分業務,知道了如何敏捷高效開發(這點得感謝那時候的主管領導,至今讓我受益無窮,很遺憾現在絕大多數只是有開發的語法並沒有開發的思維觀念),也讓我明白系統固然重要但企業流程和業務分析能力更重要。我曾經不止一次說過考驗一個SAP顧問的能力並不在於他會多少事務代碼,知道後台表是什麼,不在於他知道SAP這個功能如何配置,而是他對業務的分析水平的高低以及需求溝通的能力大小,這才是一個資深的SAP顧問跟一個培訓機構培訓出來的人的區別。

很多人來信問我該如何入行SAP這個行業,每個人成長的道路不同,但我還是很忌諱培訓機構的,他們只會弄虛作假,投機取巧,教你如何在簡歷上謊報項目經驗,也只會教一些系統層級的東西,隨便甲方稍微面試一下就露馬腳了。我覺得時刻準備着,好好學習,找機會入職甲方或者乙方才是正道,別去花冤枉錢。

老同事如今也面臨“生存”壓力,我想他應該是有毅力堅持下去的,但能學到什麼程度就不知道了。不過他懂開發,懂業務,學起SAP應該可以輕鬆不少。要知道一個人能集業務分析、開發、項目管理、系統配置於一身,那真的不得了!

 

 

 

 

  本文作者 | SAP夢心

  聯繫方式 | 微信:W150112458(瘋狂的程序員)

  特別敬告 | 歡迎轉載,轉載請註明出處並保持原文不動,謝謝

 

 

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

flink DataStream API使用及原理

傳統的大數據處理方式一般是批處理式的,也就是說,今天所收集的數據,我們明天再把今天收集到的數據算出來,以供大家使用,但是在很多情況下,數據的時效性對於業務的成敗是非常關鍵的。

Spark 和 Flink 都是通用的開源大規模處理引擎,目標是在一個系統中支持所有的數據處理以帶來效能的提升。兩者都有相對比較成熟的生態系統。是下一代大數據引擎最有力的競爭者。

Spark 的生態總體更完善一些,在機器學習的集成和易用性上暫時領先。

Flink 在流計算上有明顯優勢,核心架構和模型也更透徹和靈活一些。

本文主要通過實例來分析flink的流式處理過程,並通過源碼的方式來介紹流式處理的內部機制。

DataStream整體概述

主要分5部分,下面我們來分別介紹:

 1.運行環境StreamExecutionEnvironment

StreamExecutionEnvironment是個抽象類,是流式處理的容器,實現類有兩個,分別是

LocalStreamEnvironment:
RemoteStreamEnvironment:
/**
 * The StreamExecutionEnvironment is the context in which a streaming program is executed. A
 * {@link LocalStreamEnvironment} will cause execution in the current JVM, a
 * {@link RemoteStreamEnvironment} will cause execution on a remote setup.
 *
 * <p>The environment provides methods to control the job execution (such as setting the parallelism
 * or the fault tolerance/checkpointing parameters) and to interact with the outside world (data access).
 *
 * @see org.apache.flink.streaming.api.environment.LocalStreamEnvironment
 * @see org.apache.flink.streaming.api.environment.RemoteStreamEnvironment
 */

2.數據源DataSource數據輸入

包含了輸入格式InputFormat

    /**
     * Creates a new data source.
     *
     * @param context The environment in which the data source gets executed.
     * @param inputFormat The input format that the data source executes.
     * @param type The type of the elements produced by this input format.
     */
    public DataSource(ExecutionEnvironment context, InputFormat<OUT, ?> inputFormat, TypeInformation<OUT> type, String dataSourceLocationName) {
        super(context, type);

        this.dataSourceLocationName = dataSourceLocationName;

        if (inputFormat == null) {
            throw new IllegalArgumentException("The input format may not be null.");
        }

        this.inputFormat = inputFormat;

        if (inputFormat instanceof NonParallelInput) {
            this.parallelism = 1;
        }
    }

 flink將數據源主要分為內置數據源和第三方數據源,內置數據源有 文件,網絡socket端口及集合類型數據;第三方數據源實用Connector的方式來連接如kafka Connector,es connector等,自己定義的話,可以實現SourceFunction,封裝成Connector來做。

 

3.DataStream轉換

DataStream:同一個類型的流元素,DataStream可以通過transformation轉換成另外的DataStream,示例如下

@link DataStream#map

@link DataStream#filter

 StreamOperator:流式算子的基本接口,三個實現類

AbstractStreamOperator:

OneInputStreamOperator:

TwoInputStreamOperator:

/**
 * Basic interface for stream operators. Implementers would implement one of
 * {@link org.apache.flink.streaming.api.operators.OneInputStreamOperator} or
 * {@link org.apache.flink.streaming.api.operators.TwoInputStreamOperator} to create operators
 * that process elements.
 *
 * <p>The class {@link org.apache.flink.streaming.api.operators.AbstractStreamOperator}
 * offers default implementation for the lifecycle and properties methods.
 *
 * <p>Methods of {@code StreamOperator} are guaranteed not to be called concurrently. Also, if using
 * the timer service, timer callbacks are also guaranteed not to be called concurrently with
 * methods on {@code StreamOperator}.
 *
 * @param <OUT> The output type of the operator
 */

 4.DataStreamSink輸出

    /**
     * Adds the given sink to this DataStream. Only streams with sinks added
     * will be executed once the {@link StreamExecutionEnvironment#execute()}
     * method is called.
     *
     * @param sinkFunction
     *            The object containing the sink's invoke function.
     * @return The closed DataStream.
     */
    public DataStreamSink<T> addSink(SinkFunction<T> sinkFunction) {

        // read the output type of the input Transform to coax out errors about MissingTypeInfo
        transformation.getOutputType();

        // configure the type if needed
        if (sinkFunction instanceof InputTypeConfigurable) {
            ((InputTypeConfigurable) sinkFunction).setInputType(getType(), getExecutionConfig());
        }

        StreamSink<T> sinkOperator = new StreamSink<>(clean(sinkFunction));

        DataStreamSink<T> sink = new DataStreamSink<>(this, sinkOperator);

        getExecutionEnvironment().addOperator(sink.getTransformation());
        return sink;
    }

5.執行

/**
     * Executes the JobGraph of the on a mini cluster of ClusterUtil with a user
     * specified name.
     *
     * @param jobName
     *            name of the job
     * @return The result of the job execution, containing elapsed time and accumulators.
     */
    @Override
    public JobExecutionResult execute(String jobName) throws Exception {
        // transform the streaming program into a JobGraph
        StreamGraph streamGraph = getStreamGraph();
        streamGraph.setJobName(jobName);

        JobGraph jobGraph = streamGraph.getJobGraph();
        jobGraph.setAllowQueuedScheduling(true);

        Configuration configuration = new Configuration();
        configuration.addAll(jobGraph.getJobConfiguration());
        configuration.setString(TaskManagerOptions.MANAGED_MEMORY_SIZE, "0");

        // add (and override) the settings with what the user defined
        configuration.addAll(this.configuration);

        if (!configuration.contains(RestOptions.BIND_PORT)) {
            configuration.setString(RestOptions.BIND_PORT, "0");
        }

        int numSlotsPerTaskManager = configuration.getInteger(TaskManagerOptions.NUM_TASK_SLOTS, jobGraph.getMaximumParallelism());

        MiniClusterConfiguration cfg = new MiniClusterConfiguration.Builder()
            .setConfiguration(configuration)
            .setNumSlotsPerTaskManager(numSlotsPerTaskManager)
            .build();

        if (LOG.isInfoEnabled()) {
            LOG.info("Running job on local embedded Flink mini cluster");
        }

        MiniCluster miniCluster = new MiniCluster(cfg);

        try {
            miniCluster.start();
            configuration.setInteger(RestOptions.PORT, miniCluster.getRestAddress().get().getPort());

            return miniCluster.executeJobBlocking(jobGraph);
        }
        finally {
            transformations.clear();
            miniCluster.close();
        }
    }

6.總結

  Flink的執行方式類似於管道,它借鑒了數據庫的一些執行原理,實現了自己獨特的執行方式。

7.展望

Stream涉及的內容還包括Watermark,window等概念,因篇幅限制,這篇僅介紹flink DataStream API使用及原理。

下篇將介紹Watermark,下下篇是windows窗口計算。

參考資料

【1】https://baijiahao.baidu.com/s?id=1625545704285534730&wfr=spider&for=pc

【2】https://blog.51cto.com/13654660/2087705

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

JS數據結構第二篇—鏈表

一、什麼是鏈表 

鏈表是一種鏈式存儲的線性表,是由一組節點組成的集合,每一個節點都存儲了下一個節點的地址;指向另一個節點的引用叫鏈;和數組中的元素內存地址是連續的相比,鏈表中的所有元素的內存地址不一定是連續的。結構模擬如圖:

一般來說,說到鏈表,就要提下數組,一般鏈表都是和數組進行對比。

在很多編程語言中,數組的長度時固定的,所以數組中的增加和刪除比較麻煩,需要頻繁的移動數組中的其他元素。

然而,JavaScript中的數組並不存在上述問題,JS中的數組相對其他語言使用上更方便,因為JS中的數組本質是一個類似數組的對象,這就使得JS的數組雖然使用更方便,但比其他語言(C++、Java、C#)的數組效率要低。

所以,在實際應用中如果發現數組很慢,就可以考慮使用鏈表來替代它。除了對數據的隨機訪問,鏈表幾乎可以用在任何可以使用一維數組的情況中。如果需要隨機訪問,數組仍然是更好的選擇。

 

二、鏈表的設計

為了對鏈表更好的使用,我們設計了類LinkedList, 對鏈表中節點的增刪改查方法進行了封裝。結構如圖:

其中size和head為LinkedList構造函數私有屬性,size記錄鏈表中有多少個節點,head指向鏈表的頭結點。

根據需要對外暴露了以下方法(可以根據需要自定義其他方法):

 單向LinkedList完整設計代碼:

/**
 * 自定義鏈表:對外公開的方法有
 * append(element) 在鏈表最後追加節點
 * insert(index, element) 根據索引index, 在索引位置插入節點
 * remove(element)  刪除節點
 * removeAt(index)  刪除指定索引節點
 * removeAll(element) 刪除所有匹配的節點
 * set(index, element) 根據索引,修改對應索引的節點值
 * get(index)  根據索引獲取節點信息
 * indexOf(element) 獲取某個節點的索引位置
 * clear()  清空所有節點
 * length()   返回節點長度
 * print() 打印所有節點信息
 * toString() 打印所有節點信息,同print
 * */
const LinkedList = function(){
    let head = null;
    let size = 0;   //記錄鏈表元素個數

    //Node模型
    function LinkNode(element, next){
        this.element = element;
        this.next = next;
    }

    //元素越界檢查, 越界拋出異常
    function outOfBounds(index){
        if (index < 0 || index >= size){
            throw("抱歉,目標位置不存在!");
        }
    }

    //根據索引,獲取目標對象
    function node(index){
        outOfBounds(index);

        let obj = head;
        for (let i = 0; i < index; i++){
            obj = obj.next;
        }

        return obj;
    }

    //新增一個元素
     function append(element){
        if (size == 0){
            head = new LinkNode(element, null);
        }
        else{
            let obj = node(size-1);
            obj.next = new LinkNode(element, null);
        }
         size++;
    }

    //插入一個元素
     function insert(index, element){
        if (index == 0){
            head = new LinkNode(element, head);
        }
        else{
            let obj = node(index-1);
            obj.next = new LinkNode(element, obj.next);
        }
         size++;
    }

    //修改元素
    function set(index, element){
        let obj = node(index);
        obj.element = element;
    }

    //根據值移除節點元素
    function remove(element){
        if (size < 1) return null;

        if (head.element == element){
            head = head.next;
            size--;
            return element;
        }
        else{
            let temp = head;
            while(temp.next){
                if (temp.next.element == element){
                    temp.next = temp.next.next;
                    size--;
                    return element;
                }
                else{
                    temp = temp.next;
                }
            }
        }
        return null;
    }

    //根據索引移除節點
     function removeAt(index){
         outOfBounds(index);
         let element = null;

         if (index == 0){
             element = head.element;
             head = head.next;
         }
         else{
             let prev = node(index-1);
             element = prev.next.element;
             prev.next = prev.next.next;
         }
         size--;
        return element;
    }

    //移除鏈表裡面的所有匹配值element的元素
     function removeAll(element){

        let virHead = new LinkNode(null, head); //創建一個虛擬頭結點,head為次節點
         let tempNode = virHead, ele = null;

         while(tempNode.next){
             if (tempNode.next.element == element){
                 tempNode.next = tempNode.next.next;
                 size--;
                 ele = element;
             }
             else{
                tempNode = tempNode.next;
             }
         }

         //重新賦值
         head = virHead.next;

        return ele;
    }

    //獲取某個元素
    function get(index){
        return node(index).element;
    }

    //獲取元素索引
    function indexOf(element){
        let obj = head, index = -1;

        for (let i = 0; i < size; i++){
            if (obj.element == element){
                index = i;
                break;
            }
            obj = obj.next;
        }
        return index;
    }

    //清除所有元素
    function clear(){
        head = null;
        size = 0;
    }

    //屬性轉字符串
    function getObjString(obj){

        let str = "";

        if (obj instanceof Array){
            str += "[";
            for (let i = 0; i < obj.length; i++){
                str += getObjString(obj[i]);
            }
            str = str.substring(0, str.length - 2);
            str += "], "
        }
        else if (obj instanceof Object){
            str += "{";
            for (var key in obj){
                let item = obj[key];
                str += "\"" + key + "\": " + getObjString(item);
            }
            str = str.substring(0, str.length-2);
            str += "}, "
        }
        else if (typeof obj == "string"){
            str += "\"" + obj + "\"" + ", ";
        }
        else{
            str += obj + ", ";
        }

        return str;
    }
    function toString(){
        let str = "", obj = head;
        for (let i = 0; i < size; i++){
            str += getObjString(obj.element);
            obj = obj.next;
        }
        if (str.length > 0) str = str.substring(0, str.length -2);
        return str;
    }
    //打印所有元素
    function print(){
        console.log(this.toString())
    }

    //對外公開方法
    this.append = append;
    this.insert = insert;
    this.remove = remove;
    this.removeAt = removeAt;
    this.removeAll = removeAll;
    this.set = set;
    this.get = get;
    this.indexOf = indexOf;
    this.length = function(){
        return size;
    }
    this.clear = clear;
    this.print = print;
    this.toString = toString;
}


////測試
// let obj = new LinkedList();
// let obj1 = { title: "全明星比賽", stores: [{name: "張飛vs岳飛", store: "2:3"}, { name: "關羽vs秦瓊", store: "5:5"}]};
//
// obj.append(99);
// obj.append("hello")
// obj.append(true)
// obj.insert(3, obj1);
// obj.insert(0, [12, false, "Good", 81]);
// obj.print();
// console.log("obj1.index: ", obj.indexOf(obj1));
// obj.remove(0);
// obj.removeAll(obj1);
// obj.print();

////測試2
console.log("\n\n......test2.....")
var obj2 = new LinkedList();
obj2.append(8); obj2.insert(1,99); obj2.append('abc'); obj2.append(8); obj2.append(false);
obj2.append(12); obj2.append(8); obj2.append('123'); obj2.append(8);
obj2.print();
obj2.removeAll(8); //刪除所有8
obj2.print();

View Code

 

另外,可以在LinkedList中增加一個虛擬節點,即在頭結點之前增加一個節點,一直保留,結構如圖:

這裏代碼就不提供了,在上一份鏈表代碼中的removeAll(刪除鏈表中指定值的所有節點)方法中有用到虛擬頭結點, 下面的練習題中也有應用到虛擬頭結點,應用場景還是蠻多的。

 

三、鏈表練習題

推薦一個神奇的網站,可以以動畫的方式演示各種數據結構增刪改查變化,先來張展示鏈表的增刪效果圖看看:

網址:https://visualgo.net/zh

 

接下來做幾個鏈表的練習題,題目來自力扣,可以先自己先做一下,看看自己得分,再對比下官方提供的代碼demo

3.1 刪除排序鏈表中的重複元素_第83題

參考demo:

/**
 * 給定一個排序鏈表,刪除所有重複的元素,使得每個元素只出現一次。
 示例 1:
 輸入: 1->1->2
 輸出: 1->2

 示例 2:
 輸入: 1->1->2->3->3
 輸出: 1->2->3

 力扣得分:
 執行用時 :108 ms, 在所有 JavaScript 提交中擊敗77.12%的用戶
 內存消耗 :37.4 MB, 在所有 JavaScript 提交中擊敗了5.03%的用戶
 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {

    let virHead = new ListNode(0); //增加一個虛擬節點
    virHead.next = head;
    let temp = virHead, obj = {};

    while(temp.next){
        if (obj[temp.next.val]){ //表示為重複節點,刪除這個節點
            temp.next = temp.next.next;
        }
        else{ //
            obj[temp.next.val] = 1;
            temp = temp.next;
        }
    }
    return virHead.next;
}

//測試
var obj = new ListNode(1);
obj.next = new ListNode(2);
obj.next.next = new ListNode(1);
obj.next.next.next = new ListNode(3);
obj.next.next.next.next = new ListNode(1);
obj.next.next.next.next.next = new ListNode(2);
obj.next.next.next.next.next.next = new ListNode(3);
console.log(obj);
console.log(".>>>>>>刪除重複節點:")
console.log(deleteDuplicates(obj));

View Code

 

3.2 判斷是否環形鏈表_第141題

參考demo:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    //快慢指針,快指針每次走兩步,慢指針每次走一步
    let obj1 = head, obj2 = head; //obj1快指針,obj2為慢指針

    while(obj2){
      obj2 = obj2.next;

      if (obj1){
          obj1 = obj1.next;
      }

      if (obj1){
          obj1 = obj1.next;
      }

      if (obj2 == obj1 && obj1) return true;
    }
    return false;
};

function ListNode(val){
    this.val = val;
    this.next = null;
}

//測試
console.log(">>>>>>環形鏈表》》測試》》")
let node1 = new ListNode(1);
let node2 = new ListNode(2);
let node3 = new ListNode(3);
let node4 = new ListNode(4);

node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2;

let res = hasCycle(node1);
console.log("res: ", res);

View Code

 

3.3 移除鏈表中給定值的所有元素_第203題

 

參考demo1:

/**
 刪除鏈表中等於給定值 val 的所有節點。
 示例:
 輸入: 1->2->6->3->4->5->6, val = 6
 輸出: 1->2->3->4->5

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/**
 * 在力扣中得分:耗時160ms, 打敗Javascript中17.87%; 內存消耗37.5M, 打敗JavaScript中24.79% , 更優化的寫法是?
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    let newHead = null, curNode = null;
    while(head){
        if (head.val != val){
            if (curNode){
                curNode.next = new ListNode(head.val);
                curNode = curNode.next;
            }
            else{
                curNode = new ListNode(head.val);
                newHead = curNode;
            }
        }
        head = head.next;
    }
    return newHead;
}

function ListNode(val){
    this.val = val;
    this.next = null;
}


//測試
console.log(">>>>移除鏈表元素測試》》》")
var node = new ListNode(1);
node.next = new ListNode(2);
// node.next.next = new ListNode(5);
// node.next.next.next = new ListNode(4);
// node.next.next.next.next = new ListNode(6);
// node.next.next.next.next.next = new ListNode(8);
// node.next.next.next.next.next.next = new ListNode(4);

// var newNode = removeElements(node, 6);
// console.log(newNode);

var newNode = removeElements(node, 2);
console.log(newNode);

View Code

參考demo2:

/**
 刪除鏈表中等於給定值 val 的所有節點。
 示例:
 輸入: 1->2->6->3->4->5->6, val = 6
 輸出: 1->2->3->4->5

 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
/** 第二種寫法
 * 在力扣中得分:耗時112ms, 打敗Javascript中90.28%; 內存消耗37.5M, 打敗JavaScript中24.79%
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    if (!head) return head;

    let newHead = new ListNode(-1);
    newHead.next = head; //把head作為newHead的下一個
    let tmpNode = newHead;

    while(tmpNode.next){
        if (tmpNode.next.val == val){
            tmpNode.next = tmpNode.next.next;
        }
        else{
            tmpNode = tmpNode.next;
        }
    }
    return newHead.next; //返回newHead的下一個,就是我們想要的結果
}

function ListNode(val){
    this.val = val;
    this.next = null;
}


//測試
console.log(">>>>移除鏈表元素測試》》》")
var node = new ListNode(1);
node.next = new ListNode(2);
// node.next.next = new ListNode(5);
// node.next.next.next = new ListNode(4);
// node.next.next.next.next = new ListNode(6);
// node.next.next.next.next.next = new ListNode(8);
// node.next.next.next.next.next.next = new ListNode(4);

// var newNode = removeElements(node, 6);
// console.log(newNode);

var newNode = removeElements(node, 2);
console.log(newNode);

View Code

 

3.4 反轉鏈表_第206題

 

參考demo1_迭代方式:

/*
 反轉一個單鏈表。使用迭代方式實現
 示例:
 輸入: 1->2->3->4->5->NULL
 輸出: 5->4->3->2->1->NULL

 力扣中測試執行用時 : 76 ms, 在所有 JavaScript 提交中擊敗了97.74%的用戶
 內存消耗 :36 MB, 在所有 JavaScript 提交中擊敗了6.92%的用戶
 * */

function ListNode(val){
    this.val = val;
    this.next = null;
}
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let newHead = null;
    while(head){
        let tmpNode= newHead;
        newHead = new ListNode(head.val);
        newHead.next = tmpNode;
        head = head.next;
    }
    return newHead;
}


////測試
var node = new ListNode(9);
node.next = new ListNode(99);
node.next.next = new ListNode(999);
node.next.next.next = new ListNode(33);

console.log("原鏈表:", node);
console.log(".....反轉....")
console.log(reverseList(node))

View Code

參考demo2_遞歸方式:

/*
 反轉一個單鏈表。 使用遞歸方式實現
 示例:
 輸入: 1->2->3->4->5->NULL
 輸出: 5->4->3->2->1->NULL

 力扣測試得分:
 執行用時 :80 ms, 在所有 JavaScript 提交中擊敗了95.56%的用戶
 內存消耗 :36.3 MB, 在所有 JavaScript 提交中擊敗了5.03%的用戶
* */

function ListNode(val){
    this.val = val;
    this.next = null;
}
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    return getNewNode(head).first;
}

/**
 * 遞歸,好繞啊:
 * 推演:加入2->3->4->5 遞歸:
 * @param node
 */
function getNewNode(node){

    if (!node) return {first: null, cur: null };

    var cur = new ListNode(node.val);

    ////一直遞歸遞歸,拿到原鏈表最後一個元素開始返回
    var res = getNewNode(node.next);

    if (res.first) {
        res.cur.next = cur; //設置

        return {
            first: res.first, //反轉鏈表的第一個元素
            cur: cur
        }
    }

    console.log("666_node.val: ", node.val);
    /**
     * 原鏈表最後一個元素會執行到這裏,最後一個元素作為反轉鏈表的第一個元素返回
     */

    return {
        first: cur, //反轉鏈表的第一個元素
        cur: cur    //每次遞歸返回的一個元素
    };
}

//測試
var node = new ListNode(2);
node.next = new ListNode(3);
node.next.next = new ListNode(4);
node.next.next.next = new ListNode(5);
console.log("\n\n*****原鏈表****")
console.log(node);
console.log("......反轉.....")
console.log(reverseList(node));

View Code

 

3.5 查找鏈表的中間結點_第876題

參考代碼demo1_迭代方式:

/**
 * 給定一個帶有頭結點 head 的非空單鏈表,返回鏈表的中間結點。
 如果有兩个中間結點,則返回第二个中間結點。

 示例 1:
 輸入:[1,2,3,4,5]
 輸出:此列表中的結點 3 (序列化形式:[3,4,5])
 返回的結點值為 3 。 (測評系統對該結點序列化表述是 [3,4,5])。
 注意,我們返回了一個 ListNode 類型的對象 ans,這樣:
 ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

 示例 2:
 輸入:[1,2,3,4,5,6]
 輸出:此列表中的結點 4 (序列化形式:[4,5,6])
 由於該列表有兩个中間結點,值分別為 3 和 4,我們返回第二個結點。
  

 提示:
 給定鏈表的結點數介於 1 和 100 之間。

 力扣得分:
 執行用時 :108 ms, 在所有 JavaScript 提交中擊敗了19.44%的用戶
 內存消耗 :33.6 MB, 在所有 JavaScript 提交中擊敗了74.60%的用戶
 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var middleNode = function(head) {

    if (!head) return head;

    let arr = [];
    while(head){
        arr.push(head);
        head = head.next;
    }

    let len = arr.length;
    return len % 2 == 0 ? arr[len/2] : arr[(len-1)/2];
};

//測試
var obj = new ListNode(1), temp = obj;
for (let i = 0; i < 6; i++){
    temp.next = new ListNode(2+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));

View Code

參考代碼demo2_快慢指針:

/**
 * 給定一個帶有頭結點 head 的非空單鏈表,返回鏈表的中間結點。
 如果有兩个中間結點,則返回第二个中間結點。

 示例 1:
 輸入:[1,2,3,4,5]
 輸出:此列表中的結點 3 (序列化形式:[3,4,5])
 返回的結點值為 3 。 (測評系統對該結點序列化表述是 [3,4,5])。
 注意,我們返回了一個 ListNode 類型的對象 ans,這樣:
 ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

 示例 2:
 輸入:[1,2,3,4,5,6]
 輸出:此列表中的結點 4 (序列化形式:[4,5,6])
 由於該列表有兩个中間結點,值分別為 3 和 4,我們返回第二個結點。
  

 提示:
 給定鏈表的結點數介於 1 和 100 之間。

 力扣得分:
 執行用時 :120 ms, 在所有 JavaScript 提交中擊敗了12.22%的用戶
 內存消耗 :34.1 MB, 在所有 JavaScript 提交中擊敗了11.11%的用戶

 官方答案,官方這個確實簡潔:
 let slow = fast = head;
 while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
 return slow;

 官方力扣得分:
 執行用時 :64 ms, 在所有 JavaScript 提交中擊敗了99.44%的用戶
 內存消耗 :34.1 MB, 在所有 JavaScript 提交中擊敗了11.11%的用戶

 */
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

function ListNode(val){
    this.val = val;
    this.next = null;
}

/** 用快慢指針來處理下
 * @param {ListNode} head
 * @return {ListNode}
 */
var middleNode = function(head) {
    // let slow = head, fast = head;
    // while(slow){
    //     if (fast){
    //         fast = fast.next;
    //         if (fast){
    //             fast = fast.next;
    //         }
    //         else{
    //             return slow;
    //         }
    //     }
    //     else{
    //         return slow;
    //     }
    //     slow = slow.next;
    // }
    // return head;

    //官方答案:簡潔明了
    let slow = fast = head;
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
};

//測試
var obj = new ListNode(1), temp = obj;
for (let i = 0; i < 6; i++){
    temp.next = new ListNode(2+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));

obj = new ListNode(90), temp = obj;
for (let i = 0; i < 5; i++){
    temp.next = new ListNode(91+i);
    temp = temp.next;
}
console.log(obj);
console.log("獲取中間節點:")
console.log(middleNode(obj));

View Code

 

參考Demo地址:https://github.com/xiaotanit/Tan_DataStruct

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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

CQRS之旅——旅程6(我們系統的版本管理)

旅程6:我們系統的版本管理

準備下一站:升級和遷移

“變化是生活的調味品。”威廉·考珀

此階段的最高目標是了解如何升級包含實現CQRS模式和事件源的限界上下文的系統。團隊在這一階段實現的用戶場景包括對代碼的更改和對數據的更改:更改了一些現有的數據模式並添加了新的數據模式。除了升級系統和遷移數據外,團隊還計劃在沒有停機時間的情況下進行升級和遷移,以便在Microsoft Azure中運行實時系統。

本章的工作術語定義:

本章使用了一些術語,我們將在下面進行描述。有關更多細節和可能的替代定義,請參閱參考指南中的“深入CQRS和ES”。

  • Command(命令):命令是要求系統執行更改系統狀態的操作。命令是必須服從(執行)的一種指令,例如:MakeSeatReservation。在這個限界上下文中,命令要麼來自用戶發起請求時的UI,要麼來自流程管理器(當流程管理器指示聚合執行某個操作時)。單個接收方處理一個命令。命令總線(command bus)傳輸命令,然後命令處理程序將這些命令發送到聚合。發送命令是一個沒有返回值的異步操作。

  • 事件(Event):一個事件,比如OrderConfirmed,描述了系統中發生的一些事情,通常是一個命令的結果。領域模型中的聚合引發事件。事件也可以來自其他限界上下文。多個訂閱者可以處理特定的事件。聚合將事件發布到事件總線。處理程序在事件總線上註冊特定類型的事件,然後將事件傳遞給訂閱服務器。在訂單和註冊限界上下文中,訂閱者是流程管理器和讀取模型生成器。

  • 冪等性(Idempotency):冪等性是一個操作的特性,這意味着該操作可以多次應用而不改變結果。例如,“將x的值設置為10”的操作是冪等的,而“將x的值加1”的操作不是冪等的。在消息傳遞環境中,如果消息可以多次傳遞而不改變結果,則消息是冪等的:這可能是因為消息本身的性質,也可能是因為系統處理消息的方式。

用戶故事:

在這個過程的這個階段,團隊實現了下面描述的用戶故事。

不停機升級

V2版本的目標是升級系統,包括任何必要的數據遷移,而不需要把系統停機。如果這在當前實現中不可行,那麼停機時間應該最小化,並且應該修改系統,以便在將來支持零停機時間升級(從V3版本開始)。

Beth(業務經理)發言:

確保我們能夠在不停機的情況下進行升級,這對我們在市場中的信譽至關重要。

显示剩餘座位數量

目前,當註冊者創建一個訂單時,沒有显示每種座位類型的剩餘座位數量。當註冊者選擇購買座位時,UI應該显示此信息。

處理不需要付費的座位

目前,當註冊者選擇不需要付費的座位時,UI流仍然會將註冊者帶到支付頁面,即使不需要支付任何費用。系統應該檢測什麼時候沒有支付,並調整流程,讓註冊者直接進入訂單的確認頁面。

架構

該應用程序旨在部署到Microsoft Azure。在旅程的那個階段,應用程序由兩個角色組成,一個包含ASP.Net MVC Web應用程序的web角色和一個包含消息處理程序和領域對象的工作角色。應用程序在寫端和讀端都使用Azure SQL DataBase實例進行數據存儲。應用程序使用Azure服務總線來提供其消息傳遞基礎設施。下圖展示了這個高級體繫結構。

在研究和測試解決方案時,可以在本地運行它,可以使用Azure compute emulator,也可以直接運行MVC web應用程序,並運行承載消息處理程序和領域域對象的控制台應用程序。在本地運行應用程序時,可以使用本地SQL Server Express數據庫,並使用一個在SQL Server Express數據庫實現的簡單的消息傳遞基礎設施。

有關運行應用程序的選項的更多信息,請參見附錄1“發布說明”。

模式和概念

在旅程的這個階段,團隊處理的大多數關鍵挑戰都與如何最好地執行從V1到V2的遷移有關。本節將介紹其中的一些挑戰。

處理“事件定義發生更改”的情況

當團隊檢查V2的發布需求,很明顯,我們需要改變在訂單和註冊限界上下文中使用的一些事件來適應一些新特性:RegistrationProcessManager將會改變,當訂單有一個不需要付費的座位時系統將提供一個更好的用戶體驗。

訂單和註冊限界上下文使用事件源,因此在遷移到V2之後,事件存儲將包含舊事件,但將開始保存新事件。當系統事件被重放時,系統必須能正確處理所有的舊事件和新事件。

團隊考慮了兩種方法來處理系統中的這類更改。

在基礎設施中進行事件映射或過濾

在基礎設施中映射和過濾事件消息是一種選擇。此方法是對舊的事件消息和消息格式進行處理,在它們到達領域之前在基礎設施的某個位置處理它們。您可以過濾掉不再相關的舊消息,並使用映射將舊格式的消息轉換為新格式。這種方法最初比較複雜,因為它需要對基礎設施進行更改,但是它可以保持領域域的純粹,領域只需要理解當前的新事件集合就可以了。

在聚合中處理多個版本的消息

在聚合中處理多個版本的消息是另一種選擇。在這種方法中,所有消息類型(包括舊消息和新消息)都傳遞到領域,每個聚合必須能夠處理舊消息和新消息。從短期來看,這可能是一個合適的策略,但它最終會導致域模型受到遺留事件處理程序的污染。

團隊為V2版本選擇了這個選項,因為它包含了最少數量的代碼更改。

Jana(軟件架構師)發言:

當前在聚合中處理舊事件和新事件並不妨礙您以後使用第一種選擇:在基礎設施中使用映射/過濾機制。

履行消息冪等性

V2版本中要解決的一個關鍵問題是使系統更加健壯。在V1版本中,在某些場景中,可能會多次處理某些消息,導致系統中的數據不正確或不一致。

Jana(軟件架構師)發言:

消息冪等性在任何使用消息傳遞的系統中都很重要,這不僅僅是在實現CQRS模式或使用事件源的系統中。

在某些場景中,設計冪等消息是可能的,例如:使用“將座位配額設置為500”的消息,而不是“在座位配額中增加100”的消息。您可以安全地多次處理第一個消息,但不能處理第二個消息。

然而,並不總是能夠使用冪等消息,因此團隊決定使用Azure服務總線的重複刪除特性,以確保它只傳遞一次消息。團隊對基礎設施進行了一些更改,以確保Azure服務總線能夠檢測重複消息,並配置Azure服務總線來執行重複消息檢測。

要了解Contoso是如何實現這一點的,請參閱下面的“不讓命令消息重複”一節。此外,我們需要考慮系統中的消息處理程序如何從隊列和Topic檢索消息。當前的方法使用Azure服務總線peek/lock機制。這是一個分成三個階段的過程:

  1. 處理程序從隊列或Topic檢索消息,並在其中留下消息的鎖定副本。其他客戶端無法看到或訪問鎖定的消息。
  2. 處理程序處理消息。
  3. 處理程序從隊列中刪除鎖定的消息。如果鎖定的消息在固定時間后沒有解鎖或刪除,則解鎖該消息並使其可用,以便再次檢索。

如果步驟由於某種原因失敗,這意味着系統可以不止一次地處理消息。

Jana(軟件架構師)發言:

該團隊計劃在旅程的下一階段解決這個問題(步驟失敗的問題)。更多信息,請參見第7章“添加彈性和優化性能”。

阻止多次處理事件

在V1中,在某些場景里,如果在處理事件時發生錯誤,系統可能多次處理事件。為了避免這種情況,團隊修改了體繫結構,以便每個事件處理程序都有自己對Azure Topic的訂閱。下圖显示了兩個不同的模型。

在V1中,可能發生以下行為:

  1. EventProcessor實例從服務總線中的所有訂閱者那裡接收到OrderPlaced事件。
  2. EventProcessor實例有兩個已註冊的處理程序,RegistrationProcessManagerRouter和OrderViewModelGenerator處理程序類,所以會在兩個裡都觸發調用Handle方法。
  3. 在OrderViewModelGenerator類中的Handle方法執行成功。
  4. 在RegistrationProcessManagerRouter類中的Handle方法拋出異常。
  5. EventProcessor實例捕獲到異常然後拋棄掉事件消息。消息將自動放回訂閱中。
  6. EventProcessor實例第二次從所有訂閱者那裡接收到OrderPlaced事件。
  7. 事件又觸發兩個處理方法,導致RegistrationProcessManagerRouter類和OrderViewModelGenerator第二次處理事件消息。
  8. 每當RegistrationProcessManagerRouter類拋出異常時,OrderViewModelGenerator類都會觸發處理該事件。

在V2模型中,如果處理程序類拋出異常,EventProcessor實例將事件消息放回與該處理程序類關聯的訂閱。重試邏輯現在只會導致EventProcessor實例重試引發異常的處理程序,因此沒有其他處理程序會重新處理消息。

集成事件的持久化

在V1版本中提出的一個問題是,系統如何持久化從會議管理限界上下文發送到訂單和註冊限界上下文的集成事件。這些事件包括關於會議創建和發布的信息,以及座位類型和配額更改的詳細信息。

在V1版本中,訂單和註冊上下文中的ConferenceViewModelGenerator類通過更新視圖模型並向SeatsAvailability聚合發送命令來處理這些事件,以告訴它更改座位配額值。

這種方法意味着訂單和註冊限界上下文不存儲任何歷史記錄,這可能會導致問題。例如,其他視圖從這裏中查找座椅類型描述時,這裏只包含座椅類型描述的最新值。因此,在其他地方重播一組事件可能會重新生成另一個包含不正確座椅類型描述的讀取模型投影。

團隊考慮了以下五個方法來糾正這種情況:

  • 將所有事件保存在原始限界上下文中(會議管理限界上下文中),並使用共享的事件存儲,訂單和註冊限界上下文中可以訪問該存儲來重播這些事件。接收限界上下文可以重放事件流,直到它需要查看的之前的座椅類型描述時為止。
  • 當所有事件到達接收限界上下文(訂單和註冊限界上下文)時保存它們。
  • 讓視圖模型生成器中的命令處理程序保存事件,只選擇它需要的那些。
  • 讓視圖模型生成器中的命令處理程序保存不同的事件,實際上就是為此視圖模型使用事件源。
  • 將來自所有限界上下文的所有命令和事件消息存儲在消息日誌中。

第一種選擇並不總是可行的。在這種特殊情況下,它可以工作,因為同一個團隊同時實現了限界上下文和基礎設施,使得使用共享事件存儲變得很容易。

Gary(CQRS專家)發言:

儘管從純粹主義者的角度來看,第一個選項破壞了限界上下文之間的嚴格隔離,但在某些場景中,它可能是一個可接受的實用解決方案。

第三種選擇可能存在的風險是,所需的事件集合可能在未來發生變化。如果我們現在不保存事件,它們將永遠丟失。

儘管第五個選項存儲了所有命令和事件,其中一些可能永遠都不需要再次引用,但它確實提供了一個完整的日誌,記錄了系統中發生的所有事情。這對於故障診斷很有用,還可以幫助您滿足尚未確定的需求。該團隊選擇了這個選項而不是選項二,因為它提供了一個更通用的機制,可能具有未來的好處。

持久化事件的目的是,當訂單和註冊上下文需要有關當前座位配額的信息時,可以回放這些事件,以便計算剩餘座位的數量。要一致地計算這些数字,必須始終以相同的順序回放事件。這種順序有幾種選擇:

  • 會議管理限界上下文發送事件的順序。
  • 訂單和註冊上下文接收事件的順序。
  • 訂單和註冊上下文處理事件的順序。

大多數情況下,這些順序是相同的。沒有什麼正確的順序。你只需要選擇一個和它保持一致就行了。因此,選擇由簡單性決定。在本例中,最簡單的方法是按照訂單和註冊限界上下文中處理程序接收事件的順序持久化事件(第二個選項)。

Markus(軟件開發人員)發言:

這種選擇通常不會出現在事件源中。每個聚合會都以固定的順序創建事件,這就是系統用於持久存儲事件的順序。在此場景中,集成事件不是由單個聚合創建的。

為這些事件保存時間戳也有類似的問題。如果將來需要查看特定時間剩餘的座位數量,那麼時間戳可能會很有用。這裏的選擇是,當事件在會議管理限界上下文中創建時,還是在訂單和註冊限界上下文中接收時,應該創建時間戳?當會議管理限界上下文創建事件時,訂單和註冊限界上下文可能由於某種原因離線。因此,團隊決定在會議管理有界上下文發布事件時創建時間戳。

消息排序

團隊創建並運行來驗證V1版本的驗收測試,凸顯出了消息排序的一個潛在問題:執行會議管理限界上下文的驗收測試向訂單和註冊限界上下文發送了一系列命令,這些命令有時會出現順序錯誤。

Markus(軟件開發人員)發言:

當人類用戶真實測試系統的這一部分時,不太會注意到這種效果,因為發出命令的時間間隔要長得多,這使得消息不太可能無序地到達。

團隊考慮了兩種方法來確保消息以正確的順序到達。

  • 第一個方法是使用消息會話,這是Azure服務總線的一個特性。如果您使用消息會話,這將確保會話內的消息以與它們發送時相同的順序傳遞。
  • 第二種方法是修改應用程序中的處理程序,通過使用發送消息時添加到消息中的序列號或時間戳來檢測無序消息。如果接收處理程序檢測到一條無序消息,它將拒絕該消息,並在處理了在被拒絕消息之前發送的消息之後,將其放回稍後處理的隊列或Topic。

在這種情況下,首選的解決方案是使用Azure服務總線消息會話,因為這隻需要對現有代碼進行更少的更改。這兩種方法都會給消息傳遞帶來一些額外的延遲,但是團隊並不認為這會對系統的性能產生顯著的影響。

實現細節

本節描述訂單和註冊限界上下文的實現的一些重要功能。您可能會發現擁有一份代碼拷貝很有用,這樣您就可以繼續學習了。您可以從Download center下載一個副本,或者在GitHub上查看存儲庫:https://github.com/mspnp/cqrs-journey-code。您可以從GitHub上的Tags頁面下載V2版本的代碼。

備註:不要期望代碼示例與參考實現中的代碼完全匹配。本章描述了CQRS過程中的一個步驟,隨着我們了解更多並重構代碼,實現可能會發生變化。

**添加對“不需要支付的訂單”的支持

做出這一改變有三個具體的目標,它們都是相關的。我們希望:

  • 修改RegistrationProcessManager類和相關聚合,以處理不需要支付的訂單。
  • 修改UI中的導航,當訂單不需要支付時跳過付款步驟。
  • 確保系統在升級到V2之後能夠正確地工作,包括使用新事件和舊事件。

RegistrationProcessManager類的更改

在此之前,RegistrationProcessManager類在收到來自UI的註冊者已完成支付的通知后發送了一個ConfirmOrderPayment命令。現在,如果有一個不需要支付訂單,UI將直接向訂單聚合發送一個ConfirmOrder命令。如果訂單需要支付,RegistrationProcessManager類在從UI接收到成功支付的通知后,再向訂單聚合發送一個ConfirmOrder命令。

Jana(軟件架構師)發言:

注意,命令的名稱已從ConfirmOrderPayment更改為ConfirmOrder。這反映了訂單不需要知道任何關於付款的信息。它只需要知道訂單已經確認。類似地,現在有一個新的OrderConfirmed事件用於替代舊的OrderPaymentConfirmed事件。

當訂單聚合接收到ConfirmOrder命令時,它將引發一個OrderConfirmed事件。除被持久化外,該事件還由以下對象處理:

  • OrderViewModelGenerator類,它在其中更新讀取模型中的訂單狀態。
  • SeatAssignments聚合,在其中初始化一個新的SeatAssignments實例。
  • RegistrationProcessManager類,它在其中觸發一個提交座位預訂的命令。

UI的更改

UI中的主要更改是在RegistrationController MVC控制器類中的SpecifyRegistrantAndPaymentDetails action里的。之前,此action方法返回InitiateRegistrationWithThirdPartyProcessorPayment(action result)。現在,如果Order對象的新IsFreeOfCharge屬性為true,它將返回一個CompleteRegistrationWithoutPayment(action result)。否則,它返回一個CompleteRegistrationWithThirdPartyProcessorPayment(action result)。

[HttpPost]
public ActionResult SpecifyRegistrantAndPaymentDetails(AssignRegistrantDetails command, string paymentType, int orderVersion)
{
    ...

    var pricedOrder = this.orderDao.FindPricedOrder(orderId);
    if (pricedOrder.IsFreeOfCharge)
    {
        return CompleteRegistrationWithoutPayment(command, orderId);
    }

    switch (paymentType)
    {
        case ThirdPartyProcessorPayment:

            return CompleteRegistrationWithThirdPartyProcessorPayment(command, pricedOrder, orderVersion);

        case InvoicePayment:
            break;

        default:
            break;
    }

    ...
}

CompleteRegistrationWithThirdPartyProcessorPayment將用戶重定向到ThirdPartyProcessorPayment action,CompleteRegistrationWithoutPayment方法將用戶直接重定向到ThankYou action。

數據遷移

會議管理限界上下文在其Azure SQL數據庫實例中的PricedOrders表中存儲來自訂單和註冊限界上下文的訂單信息。以前,會議管理限界上下文接收OrderPaymentConfirmed事件,現在它接收OrderConfirmed事件,該事件包含一個附加的IsFreeOfCharge屬性。這將成為數據庫中的一個新列。

Markus(軟件開發人員)發言:

在遷移過程中,我們不需要修改該表中的現有數據,因為布爾值的默認值為false。所有現有條目都是在系統支持不需要付費的訂單之前創建的。

在遷移過程中,任何正在運行的ConfirmOrderPayment命令都可能丟失,因為它們不再由訂單聚合處理。您應該驗證當前的命令總線沒有這些命令。

Poe(IT運維人員)發言:

我們需要仔細計劃如何部署V2版本,以便確保所有現有的、正在運行的ConfirmOrderPayment命令都由運行V1版本的工作角色實例處理。

系統將RegistrationProcessManager類實例的狀態保存到SQL數據庫表中。這個表的架構沒有變化。遷移后您將看到的惟一更改是StateValue列中的一個新添加值。這反映了RegistrationProcessManager類中的ProcessState枚舉中額外的PaymentConfirmationReceived值,如下面的代碼示例所示:

public enum ProcessState
{
    NotStarted = 0,
    AwaitingReservationConfirmation = 1,
    ReservationConfirmationReceived = 2,
    PaymentConfirmationReceived = 3,
}

在V1版本中,事件源系統為訂單聚合保存的事件包括OrderPaymentConfirmed事件。因此,事件存儲區包含此事件類型的實例。在V2版本中,OrderPaymentConfirmed事件被替換為OrderConfirmed事件。

團隊決定在V2版本中,當反序列化事件時,不在基礎設施級別映射和過濾事件。這意味着,當系統從事件存儲中重播這些事件時,處理程序必須同時理解舊事件和新事件。下面的代碼示例在SeatAssignmentsHandler類中显示了這一點:

static SeatAssignmentsHandler()
{
    Mapper.CreateMap<OrderPaymentConfirmed, OrderConfirmed>();
}

public SeatAssignmentsHandler(IEventSourcedRepository<Order> ordersRepo, IEventSourcedRepository<SeatAssignments> assignmentsRepo)
{
    this.ordersRepo = ordersRepo;
    this.assignmentsRepo = assignmentsRepo;
}

public void Handle(OrderPaymentConfirmed @event)
{
    this.Handle(Mapper.Map<OrderConfirmed>(@event));
}

public void Handle(OrderConfirmed @event)
{
    var order = this.ordersRepo.Get(@event.SourceId);
    var assignments = order.CreateSeatAssignments();
    assignmentsRepo.Save(assignments);
}

您還可以在OrderViewModelGenerator類中看到同樣的技術。

Order類中的方法略有不同,因為這是持久化到事件存儲中的事件之一。下面的代碼示例显示了Order類中受保護構造函數的一部分:

protected Order(Guid id)
    : base(id)
{
    ...
    base.Handles<OrderPaymentConfirmed>(e => this.OnOrderConfirmed(Mapper.Map<OrderConfirmed>(e)));
    base.Handles<OrderConfirmed>(this.OnOrderConfirmed);
    ...
}

Jana(軟件架構師)發言:

以這種方式處理舊事件對於這個場景非常簡單,因為惟一需要更改的是事件的名稱。如果事件的屬性也發生了變化,情況會更加複雜。將來,Contoso將考慮在基礎設施中進行映射,以避免遺留事件污染領域模型。

在UI中显示剩餘座位

做出這一改變有三個具體的目標,它們都是相關的。我們想要:

  • 修改系統,在會議系統的讀模型中包含每個座位類型的剩餘座位數量信息。
  • 修改UI以显示每種座位類型的剩餘座位數量。
  • 確保升級到V2后系統功能正常。

向讀模型添加關於剩餘座位數量的信息

系統要能显示剩餘座位數量的信息來自兩個地方:

  • 當業務客戶創建新的座位類型或修改座位配額時,會議管理限界上下文將引發SeatCreated和SeatUpdated事件。
  • 在訂單和註冊限界上下文中,當註冊者創建一個訂單的時候,可用座位(SeatsAvailability)聚合將引發SeatsReserved、SeatsReservationCancelled和AvailableSeatsChanged事件。

備註:ConferenceViewModelGenerator類不使用SeatCreated和SeatUpdated事件。

訂單和註冊限界上下文中的ConferenceViewModelGenerator類現在處理這些事件,並使用它們來計算和存儲讀模型中的座位類型數量。下面的代碼示例显示了ConferenceViewModelGenerator類中的相關處理程序:

public void Handle(AvailableSeatsChanged @event)
{
    this.UpdateAvailableQuantity(@event, @event.Seats);
}

public void Handle(SeatsReserved @event)
{
    this.UpdateAvailableQuantity(@event, @event.AvailableSeatsChanged);
}

public void Handle(SeatsReservationCancelled @event)
{
    this.UpdateAvailableQuantity(@event, @event.AvailableSeatsChanged);
}

private void UpdateAvailableQuantity(IVersionedEvent @event, IEnumerable<SeatQuantity> seats)
{
    using (var repository = this.contextFactory.Invoke())
    {
        var dto = repository.Set<Conference>().Include(x => x.Seats).FirstOrDefault(x => x.Id == @event.SourceId);
        if (dto != null)
        {
            if (@event.Version > dto.SeatsAvailabilityVersion)
            {
                foreach (var seat in seats)
                {
                    var seatDto = dto.Seats.FirstOrDefault(x => x.Id == seat.SeatType);
                    if (seatDto != null)
                    {
                        seatDto.AvailableQuantity += seat.Quantity;
                    }
                    else
                    {
                        Trace.TraceError("Failed to locate Seat Type read model being updated with id {0}.", seat.SeatType);
                    }
                }

                dto.SeatsAvailabilityVersion = @event.Version;

                repository.Save(dto);
            }
            else
            {
                Trace.TraceWarning ...
            }
        }
        else
        {
            Trace.TraceError ...
        }
    }
}

UpdateAvailableQuantity方法將事件上的版本與讀模型的當前版本進行比較,以檢測可能的重複消息。

Markus(軟件開發人員)發言:

此檢查僅檢測重複的消息,而不是超出序列的消息。

修改UI以显示剩餘的座位數量

現在,當UI向會議的讀模型查詢座位類型列表時,列表包括當前可用的座位數量。下面的代碼示例显示了RegistrationController MVC控制器如何使用SeatType類的AvailableQuantity:

private OrderViewModel CreateViewModel()
{
    var seatTypes = this.ConferenceDao.GetPublishedSeatTypes(this.ConferenceAlias.Id);
    var viewModel =
        new OrderViewModel
        {
            ConferenceId = this.ConferenceAlias.Id,
            ConferenceCode = this.ConferenceAlias.Code,
            ConferenceName = this.ConferenceAlias.Name,
            Items =
                seatTypes.Select(
                    s =>
                        new OrderItemViewModel
                        {
                            SeatType = s,
                            OrderItem = new DraftOrderItem(s.Id, 0),
                            AvailableQuantityForOrder = s.AvailableQuantity,
                            MaxSelectionQuantity = Math.Min(s.AvailableQuantity, 20)
                        }).ToList(),
        };

    return viewModel;
}

數據遷移

保存會議讀模型數據的數據庫有一個新列來保存用於檢查重複事件的版本號,而保存座位類型讀模型數據有一個新列來保存可用的座椅數量。

作為數據遷移的一部分,有必要為每個可用座位(SeatsAvailability)聚合重放事件存儲中的所有事件,以便正確計算可用數量。

不讓命令消息重複

系統目前使用Azure服務總線傳輸消息。當系統從ConferenceProcessor類的啟動代碼初始化Azure服務總線時,它配置Topic來檢測重複的消息,如下面的ServiceBusConfig類的代碼示例所示:

private void CreateTopicIfNotExists() 
{     
    var topicDescription =         
        new TopicDescription(this.topic)         
        {             
            RequiresDuplicateDetection = true,
            DuplicateDetectionHistoryTimeWindow = topic.DuplicateDetectionHistoryTimeWindow,         
        };     
    try     
    {         
        this.namespaceManager.CreateTopic(topicDescription);     
    }     
    catch (MessagingEntityAlreadyExistsException) { } 
} 
備註:您可以在Settings.xml文件中配置DuplicateDetectionHistoryTimeWindow
可以向Topic元素添加這個屬性。默認值是1小時。

但是,為了使重複檢測工作正常,您必須確保每個消息都有一個惟一的ID。下面的代碼示例显示了MarkSeatsAsReserved命令:

public class MarkSeatsAsReserved : ICommand
{
    public MarkSeatsAsReserved()
    {
        this.Id = Guid.NewGuid();
        this.Seats = new List<SeatQuantity>();
    }

    public Guid Id { get; set; }

    public Guid OrderId { get; set; }

    public List<SeatQuantity> Seats { get; set; }

    public DateTime Expiration { get; set; }
}

CommandBus類中的BuildMessage方法使用命令Id創建一個惟一的消息Id, Azure服務總線可以使用這個消息Id來檢測重複:

private BrokeredMessage BuildMessage(Envelope command) 
{ 
    var stream = new MemoryStream(); 
    ...

    var message = new BrokeredMessage(stream, true);
    if (!default(Guid).Equals(command.Body.Id))
    {
        message.MessageId = command.Body.Id.ToString();
    }

...

    return message;
} 

保證消息順序

團隊決定使用Azure服務總線消息會話來保證系統中的消息順序。

系統從ConferenceProcessor類中的OnStart方法配置Azure服務總線Topic和訂閱。Settings.xml配置文件中的配置指定了具體的訂閱使用會話。ServiceBusConfig類中的以下代碼示例显示了系統如何創建和配置訂閱。

private void CreateSubscriptionIfNotExists(NamespaceManager namespaceManager, TopicSettings topic, SubscriptionSettings subscription)
{
    var subscriptionDescription =
        new SubscriptionDescription(topic.Path, subscription.Name)
        {
            RequiresSession = subscription.RequiresSession
        };

    try
    {
        namespaceManager.CreateSubscription(subscriptionDescription);
    }
    catch (MessagingEntityAlreadyExistsException) { }
}

以下來自SessionSubscriptionReceiver類的代碼示例演示了如何使用會話接收消息:

private void ReceiveMessages(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        MessageSession session;
        try
        {
            session = this.receiveRetryPolicy.ExecuteAction<MessageSession>(this.DoAcceptMessageSession);
        }
        catch (Exception e)
        {
            ...
        }

        if (session == null)
        {
            Thread.Sleep(100);
            continue;
        }


        while (!cancellationToken.IsCancellationRequested)
        {
            BrokeredMessage message = null;
            try
            {
                try
                {
                    message = this.receiveRetryPolicy.ExecuteAction(() => session.Receive(TimeSpan.Zero));
                }
                catch (Exception e)
                {
                    ...
                }

                if (message == null)
                {
                    // If we have no more messages for this session, exit and try another.
                    break;
                }

                this.MessageReceived(this, new BrokeredMessageEventArgs(message));
            }
            finally
            {
                if (message != null)
                {
                    message.Dispose();
                }
            }
        }

        this.receiveRetryPolicy.ExecuteAction(() => session.Close());
    }
}

private MessageSession DoAcceptMessageSession()
{
    try
    {
        return this.client.AcceptMessageSession(TimeSpan.FromSeconds(45));
    }
    catch (TimeoutException)
    {
        return null;
    }
}

Markus(軟件開發人員)發言:

您可能會發現,將使用消息會話的ReceiveMessages方法的這個版本與SubscriptionReceiver類中的原始版本進行比較是很有用的。

您必須確保當你發送消息包含一個會話ID,這樣才能使用消息會話接收一條消息。系統使用事件的SourceID作為會話ID,如下面的代碼示例所示的EventBus類中的BuildMessage方法:

var message = new BrokeredMessage(stream, true);
message.SessionId = @event.SourceId.ToString();

通過這種方式,您可以確保以正確的順序接收來自單個源的所有消息。

Poe(IT運維人員)發言:

在V2版本中,團隊更改了系統創建Azure服務總線Topic和訂閱的方式。之前,SubscriptionReceiver類創建了它們(如果它們還不存在)。現在,系統在應用程序啟動時使用配置數據創建它們。這發生在啟動過程的早期,以避免在系統初始化訂閱之前將消息發送到Topic時丟失消息的風險。

然而,只有當消息按正確的順序傳遞到總線上時,會話才能保證按順序傳遞消息。如果系統異步發送消息,則必須特別注意確保消息以正確的順序放在總線上。在我們的系統中,來自每個單獨聚合實例的事件按順序到達是很重要的,但是我們不關心來自不同聚合實例的事件的順序。因此,儘管系統異步發送事件,EventStoreBusPublisher實例仍然會在發送下一個事件之前等待前一個事件已發送的確認。以下來自TopicSender類的示例說明了這一點:

public void Send(Func<BrokeredMessage> messageFactory)
{
    var resetEvent = new ManualResetEvent(false);
    Exception exception = null;
    this.retryPolicy.ExecuteAction(
        ac =>
        {
            this.DoBeginSendMessage(messageFactory(), ac);
        },
        ar =>
        {
            this.DoEndSendMessage(ar);
        },
        () => resetEvent.Set(),
        ex =>
        {
            Trace.TraceError("An unrecoverable error occurred while trying to send a message:\r\n{0}", ex);
            exception = ex;
            resetEvent.Set();
        });

    resetEvent.WaitOne();
    if (exception != null)
    {
        throw exception;
    }
}

Jana(軟件架構師)發言:

此代碼示例展示了系統如何使用Transient Fault Handling Application Block來讓異步調用可靠。

有關消息排序和Azure服務總線的更多信息,請參見Microsoft Azure Queues and Microsoft Azure Service Bus Queues – Compared and Contrasted

有關異步發送消息和排序的信息,請參閱博客文章Microsoft Azure Service Bus Splitter and Aggregator

從會議管理限界上下文中持久化事件

團隊決定創建一個包含所有發送的命令和事件的消息日誌。這將使訂單和註冊限界上下文能夠從會議管理限界上下文查詢此日誌,以獲取其構建讀模型所需的事件。這不是事件源,因為我們沒有使用這些事件來重建聚合的狀態,儘管我們使用類似的技術來捕獲和持久化這些集成事件。

Gary(CQRS專家)發言:

此消息日誌確保不會丟失任何消息,以便將來能夠滿足其他需求。

向消息添加額外元數據

系統現在將所有消息保存到消息日誌中。為了方便查詢特定命令或事件,系統現在向每個消息添加了更多的元數據。以前,惟一的元數據是事件類型,現在,事件元數據包括事件類型、命名空間、程序集和路徑。系統將元數據添加到EventBus類中的事件和CommandBus類中的命令中。

捕獲消息並將消息持久化到消息日誌中

系統使用Azure服務總線中對會議/命令和會議/事件topic的額外訂閱來接收系統中每條消息的副本。然後,它將消息附加到Azure表存儲中。下面的代碼示例显示了AzureMessageLogWriter類的實例,它用於將消息保存到表中:

public class MessageLogEntity : TableServiceEntity 
{ 
    public string Kind { get; set; }     
    public string CorrelationId { get; set; }     
    public string MessageId { get; set; }     
    public string SourceId { get; set; }     
    public string AssemblyName { get; set; }     
    public string Namespace { get; set; }     
    public string FullName { get; set; }     
    public string TypeName { get; set; }     
    public string SourceType { get; set; }     
    public string CreationDate { get; set; }     
    public string Payload { get; set; } 
} 

Kind屬性指定消息是命令還是事件。MessageId和CorrelationId屬性由消息傳遞基礎設施設置的,其餘屬性是從消息元數據中設置的。

下面的代碼示例显示了這些消息的分區和RowKey的定義:

PartitionKey = message.EnqueuedTimeUtc.ToString("yyyMM"),
RowKey = message.EnqueuedTimeUtc.Ticks.ToString("D20") + "_" + message.MessageId

注意,RowKey保存了消息最初發送的順序,並添加到消息ID上,以確保惟一性,以防兩條消息同時入隊。

Jana(軟件架構師)發言:

這與事件存儲不同,在事件存儲區中,分區鍵標識聚合實例,而RowKey標識聚合的版本號。

數據遷移

當Contoso將系統從V1遷移到V2時,它將使用消息日誌在訂單和註冊限界上下文中重建會議和價格訂單的讀模型。

Gary(CQRS專家)發言:

Contoso可以在需要重建與聚合無關的事件構建的讀模型時來使用消息日誌,例如來自會議管理限界上下文的集成事件。

會議讀模型包含會議的信息,並包含來自會議管理限界上下文的ConferenceCreated、ConferenceUpdated、ConferencePublished、ConferenceUnpublished、SeatCreated和SeatUpdated事件的信息。

價格訂單讀模型持有來自於SeatCreated和SeatUpdated事件的信息,這些事件來自於會議管理限界上下文。

然而,在V1中,這些事件消息沒有被持久化,因此讀模型不能在V2中重新填充。為了解決這個問題,團隊實現了一個數據遷移實用程序,它使用一種最佳方法來生成包含要存儲在消息日誌中的丟失數據的事件。例如,在遷移到V2之後,消息日誌不包含ConferenceCreated事件,因此遷移實用程序在會議管理限界上下文使用的數據庫中找到這些信息,並創建丟失的事件。您可以在MigrationToV2項目的Migrator類中的GeneratePastEventLogMessagesForConferenceManagement方法中看到這是如何完成的。

Markus(軟件開發人員)發言:

您可以在這個類中看到,Contoso還將所有現有的事件源事件複製到消息日誌中。

如下面所示,Migrator類中的RegenerateViewModels方法重新構建讀取的模型。它通過調用Query方法從消息日誌中檢索所有事件,然後使用ConferenceViewModelGenerator和PricedOrderViewModelUpdater類來處理消息。

internal void RegenerateViewModels(AzureEventLogReader logReader, string dbConnectionString)
{
    var commandBus = new NullCommandBus();

    Database.SetInitializer<ConferenceRegistrationDbContext>(null);

    var handlers = new List<IEventHandler>();
    handlers.Add(new ConferenceViewModelGenerator(() => new ConferenceRegistrationDbContext(dbConnectionString), commandBus));
    handlers.Add(new PricedOrderViewModelUpdater(() => new ConferenceRegistrationDbContext(dbConnectionString)));

    using (var context = new ConferenceRegistrationMigrationDbContext(dbConnectionString))
    {
        context.UpdateTables();
    }

    try
    {
        var dispatcher = new MessageDispatcher(handlers);
        var events = logReader.Query(new QueryCriteria { });

        dispatcher.DispatchMessages(events);
    }
    catch
    {
        using (var context = new ConferenceRegistrationMigrationDbContext(dbConnectionString))
        {
            context.RollbackTablesMigration();
        }

        throw;
    }
}

Jana(軟件架構師)發言:

查詢可能不會很快,因為它將從多個分區檢索實體。

注意這個方法如何使用NullCommandBus實例來接收來自ConferenceViewModelGenerator實例的任何命令,因為我們只是在這裏重新構建讀模型。

以前,PricedOrderViewModelGenerator使用ConferenceDao類來獲取關於座位的信息。現在,它是自治的,並直接處理SeatCreated和SeatUpdated事件來維護這些信息。作為遷移的一部分,必須將此信息添加到讀模型中。在前面的代碼示例中,PricedOrderViewModelUpdater類只處理SeatCreated和SeatUpdated事件,並將缺失的信息添加到價格訂單讀模型中。

從V1遷移到V2

從V1遷移到V2需要更新已部署的應用程序代碼並遷移數據。在生產環境中執行遷移之前,應該始終在測試環境中演練遷移。以下是所需步驟:

  1. 將V2版本部署到Azure的staging環境中。V2版本有一個MaintenanceMode屬性,最初設置為true。在此模式下,應用程序向用戶显示一條消息,說明站點當前正在進行維護,而工作角色將不處理消息。
  2. 準備好之後,將V2版本(仍然處於維護模式,MaintenanceMode為true)切換到Azure生產環境中。
  3. 讓V1版本(現在在staging環境中運行)運行幾分鐘,以確保所有正在運行的消息都完成了它們的處理。
  4. 運行遷移程序來遷移數據(參見下面)。
  5. 成功完成數據遷移后,將每種工作角色的MaintenanceMode屬性更改為false。
  6. V2版本現在運行在Azure中。

Jana(軟件架構師)發言:

團隊考慮使用單獨的應用程序在升級過程中向用戶显示一條消息,告訴他們站點正在進行維護。然而,在V2版本中使用MaintenanceMode屬性提供了一個更簡單的過程,併為應用程序添加了一個潛在有用的新特性。

Poe(IT運維人員)發言:

由於對事件存儲的更改,不可能執行從V1到V2的無停機升級。然而,團隊所做的更改將確保從V2遷移到V3將不需要停機時間。

Markus(軟件開發人員)發言:

團隊對遷移實用程序應用了各種優化,例如批處理操作,以最小化停機時間。

下面幾節總結了從V1到V2的數據遷移。這些步驟中的一些在前面已經討論過,涉及到應用程序的特定更改或增強。

團隊為V2引入的一個更改是,將所有命令和事件消息的副本保存在消息日誌中,以便作為未來的證據,通過捕獲將來可能使用的所有內容來保證應用程序的安全性。遷移過程考慮到了這個新特性。

因為遷移過程複製了大量的數據,所以您應該在Azure工作角色中運行遷移過程,以最小化成本。遷移實用程序是一個控制台應用程序,因此您可以使用Azure和遠程桌面服務。有關如何在Azure角色實例中運行應用程序的信息,請參見Using Remote Desktop with Microsoft Azure Roles。

Poe(IT運維人員)發言:

在一些組織中,安全策略不允許您在Azure生產環境使用遠程桌面服務。但是,您只需要一個在遷移期間承載遠程桌面會話的工作角色,您可以在遷移完成后刪除它。您還可以將遷移代碼作為工作角色而不是控制台應用程序運行,並確保它記錄遷移的狀態,以便您驗證。

為會議管理限界上下文生成過去的日誌消息

遷移過程的一部分是在可能的情況下重新創建V1版本處理后丟棄的消息,然後將它們添加到消息日誌中。在V1版本中,所有從會議管理限界上下文發送到訂單和註冊限界上下文的集成事件都以這種方式丟失了。系統不能重新創建所有丟失的事件,但可以創建表示遷移時系統狀態的事件。

有關更多信息,請參見本章前面的“從會議管理限界上下文中持久化事件”一節。

遷移事件源里的事件

在V2版本中,事件存儲為每個事件存儲額外的元數據,以便於查詢事件。遷移過程將所有事件從現有事件存儲複製到具有新模式的新事件存儲。

Jana(軟件架構師)發言:

原始事件不會以任何方式更新,而是被視為不可變的。

同時,系統將所有這些事件的副本添加到V2版本中引入的消息日誌中。

有關更多信息,請參見MigrationToV2項目中Migrator類中的MigrateEventSourcedAndGeneratePastEventLogs。

重建讀模型**

V2版本包括對訂單和註冊限界上下文中讀模型定義的幾個更改。MigrationToV2項目在訂單和註冊限界上下文中重新構建會議的讀模型和價格訂單的讀模型。

有關更多信息,請參見本章前面的“從會議管理限界上下文中持久化事件”一節。

對測試的影響

在這個過程的這個階段,測試團隊繼續擴展驗收測試集合。他們還創建了一組測試來驗證數據遷移過程。

再說SpecFlow

之前,這組SpecFlow測試以兩種方式實現:通過自動化web瀏覽器模擬用戶交互,或者直接在MVC控制器上操作。這兩種方法都有各自的優缺點,我們在第4章“擴展和增強訂單和註冊限界上下文”中討論過。

在與另一位專家討論了這些測試之後,團隊還實現了第三種方法。從領域驅動設計(DDD)方法的角度來看,UI不是領域模型的一部分,核心團隊的重點應該是在領域專家的幫助下理解領域,並在領域中實現業務邏輯。UI只是机械部分,用於使用戶能夠與領域進行交互。因此,驗收測試應該包括驗證領域模型是否以領域專家期望的方式工作。因此,團隊使用SpecFlow創建了一組驗收測試,這些測試旨在在不影響系統UI部分的情況下測試領域。

下面的代碼示例显示了SelfRegistrationEndToEndWithDomain.feature文件,該文件在Conference.AcceptanceTests項目中的Features\Domain\Registration文件夾里,注意When和Then子句怎麼使用命令和事件的。

Gary(CQRS專家)發言:

通常,如果您的領域模型只使用聚合,您會期望When子句發送命令,Then子句查看事件或異常。然而,在本例中,領域模型包含一個通過發送命令來響應事件的流程管理器。測試將檢查是否發送了所有預期的命令,並引發了所有預期的事件。

Feature: Self Registrant end to end scenario for making a Registration for a Conference site with Domain Commands and Events
    In order to register for a conference
    As an Attendee
    I want to be able to register for the conference, pay for the Registration Order and associate myself with the paid Order automatically


Scenario: Make a reservation with the selected Order Items
Given the list of the available Order Items for the CQRS summit 2012 conference
    | seat type                 | rate | quota |
    | General admission         | $199 | 100   |
    | CQRS Workshop             | $500 | 100   |
    | Additional cocktail party | $50  | 100   |
And the selected Order Items
    | seat type                 | quantity |
    | General admission         | 1        |
    | Additional cocktail party | 1        |
When the Registrant proceeds to make the Reservation
    # command:RegisterToConference
Then the command to register the selected Order Items is received 
    # event: OrderPlaced
And the event for Order placed is emitted
    # command: MakeSeatReservation
And the command for reserving the selected Seats is received
    # event: SeatsReserved
And the event for reserving the selected Seats is emitted
    # command: MarkSeatsAsReserved
And the command for marking the selected Seats as reserved is received
    # event: OrderReservationCompleted 
And the event for completing the Order reservation is emitted
    # event: OrderTotalsCalculated
And the event for calculating the total of $249 is emitted

下面的代碼示例显示了feature文件的一些步驟實現。這些步驟使用命令總線發送命令。

[When(@"the Registrant proceed to make the Reservation")]
public void WhenTheRegistrantProceedToMakeTheReservation()
{
    registerToConference = ScenarioContext.Current.Get<RegisterToConference>();
    var conferenceAlias = ScenarioContext.Current.Get<ConferenceAlias>();

    registerToConference.ConferenceId = conferenceAlias.Id;
    orderId = registerToConference.OrderId;
    this.commandBus.Send(registerToConference);

    // Wait for event processing
    Thread.Sleep(Constants.WaitTimeout);
}

[Then(@"the command to register the selected Order Items is received")]
public void ThenTheCommandToRegisterTheSelectedOrderItemsIsReceived()
{
    var orderRepo = EventSourceHelper.GetRepository<Registration.Order>();
    Registration.Order order = orderRepo.Find(orderId);

    Assert.NotNull(order);
    Assert.Equal(orderId, order.Id);
}

[Then(@"the event for Order placed is emitted")]
public void ThenTheEventForOrderPlacedIsEmitted()
{
    var orderPlaced = MessageLogHelper.GetEvents<OrderPlaced>(orderId).SingleOrDefault();

    Assert.NotNull(orderPlaced);
    Assert.True(orderPlaced.Seats.All(
        os => registerToConference.Seats.Count(cs => cs.SeatType == os.SeatType && cs.Quantity == os.Quantity) == 1));
}

在遷移過程中發現的bug

當測試團隊在遷移之後在系統上運行測試時,我們發現訂單和註冊限界上下文中座位類型的數量與遷移之前的數量不同。調查揭示了以下原因。

如果會議從未發布過,則會議管理限界上下文允許業務客戶刪除座位類型,但不會引發集成事件向訂單和註冊限界上下文報告這一情況。所以,當業務客戶創建新的座位類型時,訂單和註冊限界上下文從會議管理限界上下文接收事件,而不是當業務客戶刪除座位類型時。

遷移過程的一部分創建一組集成事件,以替換V1版本處理后丟棄的事件。它通過讀取會議管理限界上下文使用的數據庫來創建這些事件。此過程沒有為已刪除的座位類型創建集成事件。

總之,在V1版本中,已刪除的座位類型錯誤地出現在訂單和註冊限界上下文的讀模型中。在遷移到V2版本之後,這些已刪除的座位類型沒有出現在訂單和註冊限界上下文的讀模型中。

Poe(IT運維人員)發言:

測試遷移過程不僅驗證遷移是否按預期運行,而且可能揭示應用程序本身的bug。

總結

在我們旅程的這個階段,我們對系統進行了版本控制,並完成了V2偽生產版本。這個新版本包含了一些額外的功能和特性,比如支持不需要付費的訂單和在UI中显示更多信息。

我們還對基礎設施做了一些改變。例如,我們使更多的消息具有冪等性,現在持久化集成事件。下一章將描述我們旅程的最後階段,我們將繼續增強基礎設施,並在準備發布V3版本時加強系統。

【精選推薦文章】

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

想要讓你的商品在網路上成為最夯、最多人討論的話題?

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

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

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