在 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實例有兩個已註冊的處理程序,RegistrationProcessManagerRouterOrderViewModelGenerator處理程序類,所以會在兩個裡都觸發調用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后系統功能正常。

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

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

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

備註:ConferenceViewModelGenerator類不使用SeatCreatedSeatUpdated事件。

訂單和註冊限界上下文中的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方法從消息日誌中檢索所有事件,然後使用ConferenceViewModelGeneratorPricedOrderViewModelUpdater類來處理消息。

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類來獲取關於座位的信息。現在,它是自治的,並直接處理SeatCreatedSeatUpdated事件來維護這些信息。作為遷移的一部分,必須將此信息添加到讀模型中。在前面的代碼示例中,PricedOrderViewModelUpdater類只處理SeatCreatedSeatUpdated事件,並將缺失的信息添加到價格訂單讀模型中。

從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版本時加強系統。

【精選推薦文章】

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

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

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

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

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

『開發技巧』Python音頻操作工具PyAudio上手教程

『開發技巧』Python音頻操作工具PyAudio上手教程

0.引子

當需要使用Python處理音頻數據時,使用python讀取與播放聲音必不可少,下面介紹一個好用的處理音頻PyAudio工具包。

PyAudio是Python開源工具包,由名思義,是提供對語音操作的工具包。提供錄音播放處理等功能,可以視作語音領域的OpenCv。

 

1.簡介

 

PyAudio為跨平台音頻I / O庫PortAudio提供Python 綁定。使用PyAudio,您可以輕鬆地使用Python在各種平台上播放和錄製音頻,例如GNU / Linux,Microsoft Windows和Apple Mac OS X / macOS。

PyAudio的靈感來自:

  • pyPortAudio / fastaudio:PortAudio v18 API的Python綁定。
  • tkSnack:Tcl / Tk和Python的跨平台聲音工具包。

 

2.安裝

 

目前的版本是PyAudio v0.2.11。在大多數平台上使用pip安裝PyAudio。對於v0.2.9之前的版本,PyAudio分發安裝二進制文件,這些文件 存檔在這裏。

 

微軟Windows 

使用pip安裝:

python -m pip install pyaudio

筆記:

  • 如果pip尚未與您的Python安裝捆綁在一起,請在此處獲取 。
  • pip將獲取並安裝PyAudio輪(預先打包的二進制文件)。目前,有車輪兼容Python 2.7,3.4,3.5和3.6 的 官方發行版。對於這些版本,可以使用32位和64位車輪。
  • 這些二進制文件包括使用MinGW構建的PortAudio v19 v190600_20161030。它們僅支持Windows MME API,包括對DirectX,ASIO等的支持。如果需要支持未包含的API,則需要編譯PortAudio和PyAudio。

 

Apple Mac OS X.

使用Homebrew安裝必備的portaudio庫,然後使用pip安裝PyAudio:

brew install portaudio 
pip install pyaudio

筆記:

  • 如果尚未安裝,請下載 Homebrew。
  • pip將下載PyAudio源代碼併為您的Python版本構建它。
  • Homebrew和構建PyAudio還需要安裝Xcode命令行工具(更多信息)。

 

Debian / Ubuntu

使用包管理器安裝PyAudio:

sudo apt-get install python-pyaudio python3-pyaudio

如果沒有最新版本的PyAudio,請使用pip安裝它:

pip install pyaudio

筆記:

  • pip將下載PyAudio源併為您的系統構建它。請務必事先安裝portaudio庫開發包(portaudio19-dev)和python開發包(python-all-dev)。
  • 為了更好地隔離系統包,請考慮在virtualenv中安裝PyAudio 。

 

PyAudio來源

源代碼可從Python Package Index(PyPI)下載:pypi.python.org/pypi/PyAudio。

或克隆git存儲庫:

git clone https://people.csail.mit.edu/hubert/git/pyaudio.git

要從源代碼構建PyAudio,您還需要構建 PortAudio v19。有關為各種平台構建PyAudio的一些說明,請參閱編譯提示。要使用Microsoft Visual Studio構建PyAudio,請查看Sebastian Audet的說明。

 

 

3.示例

1).採集音頻

下面以一段代碼演示如何從計算機麥克風採集一段音頻,採集音頻時長 4s,保存文件 output.wav

使用了tqdm模塊,可以方便显示出來讀取過程,如下:

* recording
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 172/172 [00:03<00:00, 43.40it/s] 
* done recording

import pyaudio import wave from tqdm import tqdm def record_audio(wave_out_path,record_second): CHUNK = 1024 FORMAT = pyaudio.paInt16 CHANNELS = 2 RATE = 44100 p = pyaudio.PyAudio() stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) wf = wave.open(wave_out_path, 'wb') wf.setnchannels(CHANNELS) wf.setsampwidth(p.get_sample_size(FORMAT)) wf.setframerate(RATE) print("* recording") for i in tqdm(range(0, int(RATE / CHUNK * record_second))): data = stream.read(CHUNK) wf.writeframes(data) print("* done recording") stream.stop_stream() stream.close() p.terminate() wf.close() record_audio("output.wav",record_second=4)

要使用PyAudio,首先使用pyaudio.PyAudio()(1)實例化PyAudio ,它設置portaudio系統。

要錄製或播放音頻,請使用pyaudio.PyAudio.open() (2)在所需設備上打開所需音頻參數的流。這設置了pyaudio.Stream播放或錄製音頻。

通過使用流式傳輸pyaudio.Stream.write()音頻數據或使用流式傳輸音頻數據來播放音頻 pyaudio.Stream.read()。(3)

請注意,在“阻止模式”中,每個pyaudio.Stream.write()或 pyaudio.Stream.read()阻止直到所有給定/請求的幀都被播放/記錄。或者,要動態生成音頻數據或立即處理錄製的音頻數據,請使用下面概述的“回調模式”。

使用pyaudio.Stream.stop_stream()暫停播放/錄製,並pyaudio.Stream.close()終止流。(4)

最後,使用pyaudio.PyAudio.terminate()(5)終止portaudio會話

 

2).播放音頻

下面使用播放的功能來播放1)中保存的音頻 output.wav

通過tqdm,显示播放進度條,如下:


100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 172/172 [00:03<00:00, 43.40it/s] 

"""PyAudio Example: Play a WAVE file.""" import pyaudio import wave from tqdm import tqdm def play_audio(wave_path): CHUNK = 1024 wf = wave.open(wave_path, 'rb') # instantiate PyAudio (1) p = pyaudio.PyAudio() # open stream (2) stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), channels=wf.getnchannels(), rate=wf.getframerate(), output=True) # read data data = wf.readframes(CHUNK) # play stream (3) datas = [] while len(data) > 0: data = wf.readframes(CHUNK) datas.append(data) for d in tqdm(datas): stream.write(d) # stop stream (4) stream.stop_stream() stream.close() # close PyAudio (5) p.terminate() play_audio("output.wav")

2).以回調方式播放音頻

當需要在執行其他程序時同時播放音頻,可以使用回調的方式播放,示例代碼如下:

"""PyAudio Example: Play a WAVE file.""" import pyaudio import wave from tqdm import tqdm import time def play_audio_callback(wave_path): CHUNK = 1024 wf = wave.open(wave_path, 'rb') # instantiate PyAudio (1) p = pyaudio.PyAudio() def callback(in_data, frame_count, time_info, status): data = wf.readframes(frame_count) return (data, pyaudio.paContinue) # open stream (2) stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), channels=wf.getnchannels(), rate=wf.getframerate(), output=True, stream_callback=callback) # read data stream.start_stream() while stream.is_active(): time.sleep(0.1) # stop stream (4) stream.stop_stream() stream.close() # close PyAudio (5) p.terminate() play_audio_callback("output.wav")

 

Reference:

1.http://people.csail.mit.edu/hubert/pyaudio/

 

 

【精選推薦文章】

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

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

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

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

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

聽說你在為天天寫業務代碼而煩惱?

寫業務代碼一般就是完成業務應用的功能,天天寫業務代碼的程序員也被戲稱為CURD程序員,CURD就是增(create)、改(update)、查(read)、刪(delete)的意思。CURD程序員每天的工作內容就是根據業務邏輯需要對數據庫數據進行增刪改查,這在很多人看來是沒有技術含量的,尤其是工作了多年的程序員,認為這無法提高他們的技術能力,寫了十年的業務代碼,卻和寫一年業務代碼的年輕人差別不大,可能對某個框架更為熟悉一點罷了,此外,少有其他方面的優勢。

實際上,每個能正常被使用的業務系統都需要CURD的工作,但僅僅是CURD也是無法完成一個比較複雜的業務系統的。一個業務系統除了需要編寫功能代碼,還要有需求分析、架構設計、詳細設計、功能編寫、測試、集成部署等等工作內容,CURD頂多是功能編寫的子集。因此,如果你的任務只是CURD,那時間長了以後確實可能會厭煩,並且覺得技能得不到提高。也因此,論壇里常有人求助於高手,問怎樣才能脫離這種CURD工作:

高手們的答案也不一致,有的說寫業務代碼同樣牛逼,CURD是核心競爭力呢,有的建議換工作,擺脫CURD,也有的說要做個有心人,多解決實際問題,自然就會提高,等你水平到一定程度了,就可以不做這類工作了。

而在我看來,不寫業務代碼的人,也不一定有多牛掰,他們也不過是根據需求實現一些東西,也需要領域知識和編程技能,只不過有些領域知識和我們常見的上層應用業務知識有比較大的區別。即使是內核開發人員,如果只是負責實現某個模塊,而且他並沒有多少進取心,每天只是讀讀文檔和協議,調調接口來實現功能,沒有深挖原理,也不關注其他方面的技術,沒有全局視角,那他其實頂多也算是一個搬磚的,離高手還是有很大的距離。

而做CURD工作的,也並不是完全學不到東西。CURD從小的方面來說,是老闆的需求,從大的方面來說,是社會需求,需要大量的人來從事這個工作。CURD程序員離業務比較近,有機會可能也必須去更多地理解業務,而業務知識也是一種領域知識,具有深刻的領域知識的人,在職場中是有競爭優勢的。因此,你可以在CURD的同時,多了解業務知識,或者多思考怎麼把CURD做得更好,比如製作一些模板工具,想辦法通過各種方式來提高工作效率,這樣面對同樣的工作時,你會更輕鬆因而也更具有競爭力。如果你真想擺脫或者基本擺脫,那麼在平時就應該注意積累其它方面的知識,能完成其他CURD程序員難以完成的任務,在工作中,你要懂得合理地越俎代庖、份內份外

越俎代庖本來是個貶義詞,指的是越權辦事、多管閑事的行為,但在這裡是褒義詞。其他同事遇到難題時,你主動幫忙解決,在你自己任務已經完成的情況下,可以研究其他人的工作內容,這樣可以在其他同事只有不太好的實現方案時,適時給出你自己的方案,這樣也不出現搶活邀功的現象。即便你的方案使用不上,你有過自己的思考研究,對自己的成長也是有利的。

可能有同學會說了,你這是站着說話不腰疼,平時加班加點才能完成任務,哪有時間去做這樣的事情。如果是這樣,那你的確難以擺脫這種境況,要不你就安心地每天CURD,要不就換個更適合自己的工作。就我自己而言,工作這麼多年,和行業里其他人相比,加班真的很少,不過我花在學習上的時間,可能會比大部分人都多。這個學習,包括工作的時候去學習其他人的任務所涉及的技能、整個項目的架構原理,以及其它自己認為有用或感興趣的技術。一般來說,工作上的事情,我工作時間就解決可能也順便理解其原理了,而要拓寬知識和技術面,一般就靠下班時間。下班時間學習的東西,有時候也是跟工作內容相關的,即便是不相關的內容,可能也會在你工作時給你帶來靈感,或者有助於你更快理解工作上的事情,這樣的話也使得你能更快速完成工作任務,於是又有更多的時間去學習和擴寬技能,形成一個良性循環。關於良性循環和惡性循環,可以參考我之前的文章:停止無謂抱怨,構建你的良性循環系統。

總之,如果你覺得自己目前就是CURD程序員並且不滿足於此,那你可以先思考如果把CURD做得更好更高效更少出bug,同時盡可能地熟悉業務,爭取在某個業務方向上比普通人更熟悉。因為你最熟悉CURD,可能在換工作時,人家還是傾向於給你提供CURD的崗位,因此如果要擺脫這種境況,就需要你在業餘時間加倍地學習、實踐新技能,然後在機會到來時,才有可能抓住它。“機會永遠只留給有準備的人”,以我的親身體會,這句話在99.9%的情況下應該是正確的,希望我們都記住它!

 

原文發表於:聽說你在為天天寫業務代碼而煩惱?

歡迎關注公眾號:

【精選推薦文章】

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

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

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

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

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

手把手教你學會 基於JWT的單點登錄

  最近我們組要給負責的一個管理系統 A 集成另外一個系統 B,為了讓用戶使用更加便捷,避免多個系統重複登錄,希望能夠達到這樣的效果——用戶只需登錄一次就能夠在這兩個系統中進行操作。很明顯這就是單點登錄(Single Sign-On)達到的效果,正好可以明目張膽的學一波單點登錄知識。

本篇主要內容如下:

  • SSO 介紹
  • SSO 的幾種實現方式對比
  • 基於 JWT 的 spring boot 單點登錄實戰

注意:
  SSO 這個概念已經出現很久很久了,目前各種平台都有非常成熟的實現,比如OpenSSOOpenAMKerberosCAS等,當然很多時候成熟意味着複雜。本文不討論那些成熟方案的使用,也不考慮 SSO 在 CS 應用中的使用。

什麼是 SSO

  單點點說就是:一次登錄后可免登陸訪問其他的可信平台。比如我們登錄淘寶網后,再打開天貓首頁可以發現已經是登錄狀態了。SSO 是一種比較流行的服務於企業業務整合的一種解決方案。

如何實現 SSO

  我們都知道目前的 http 協議是無狀態的,也就是第一次請求和第二次請求是完全獨立,不相關的,但現實中我們的業務邏輯都是有狀態的,這樣就引入了 cookie-session 的機制來維護狀態,瀏覽器端存儲一個 sessionId,後台存儲跟該 sessionId 相關的數據。每次向後台發起請求時都攜帶此 sessionId 就能維持狀態了。然後就有了 cookie,瀏覽器在發送請求時自動將 cookie 中的數據放到請求中,發給服務端,無需手動設置。

  然後我們可以考慮考慮實現 SSO 的核心是什麼?答案就是如何讓一個平台 A 登錄后,其他的平台也能獲取到平台 A 的登錄信息(在 cookie-session 機制中就是 sessionId)。

方案一 共享 cookie

  基於 cookie-session 機制的系統中,登錄系統後會返回一個 sessionId 存儲在 cookie 中,如果我們能夠讓另外一個系統也能獲取到這個 cookie,不就獲取到憑證信息了,無需再次登錄。剛好瀏覽器的 cookie 可以實現這樣的效果(詳見web 跨域及 cookie 學習)。

  cookie 允許同域名(或者父子域名)的不同端口中共享 cookie,這點和 http 的同域策略不一樣(http 請求只要協議、域名、端口不完全相同便認為跨域)。因此只需將多個應用前台頁面部署到相同的域名(或者父子域名),然後共享 session 便能夠實現單點登錄。架構如下:

  上面方案顯而易見的限制就是不僅前台頁面需要共享 cookie,後台也需要共享 session(可以用jwt來幹掉 session,但是又會引入新的問題,這裏不展開).這個方案太簡單了,不作進一步說明。

方案二 基於回調實現

  通過上文可以知道,要實現單點登錄只需將用戶的身份憑證共享給各個系統,讓後台知道現在是在訪問。就能實現一次登錄,到處訪問的效果,實在是非常方便的。在 session 機制中是共享 sessionId,然後多個後台使用同一個 session 源即可。這裏我們用一種新的基於 JWT 的 token 方式來實現,不了解 JWT 的可以看這篇:java-jwt 生成與校驗,簡單來說 jwt 可以攜帶無法篡改的信息(一段篡改就會校驗失敗),所以我們可以將用戶 id 等非敏感信息直接放到 jwt 中,幹掉了後台的 session。然後我們要做的就是將 jwt 共享給各個平台頁面即可。系統架構如下:

  此架構中,業務系統 A 和業務系統 B 之間不需要有任何聯繫,他們都只和 SSO 認證平台打交道,因此可以任意部署,沒有同域的限制。你可能就要問了這樣要怎麼共享身份憑證(也就是 jwt 字符串)?這裏就要通過 url 參數來進行騷操作了。文字總結來說是這樣的:jwt 存到認證平台前端的 localStore(不一定是 localStore,cookie,sessionStore 都可以),然後業務平台攜帶自己的回調地址跳轉到認證中心的前台,認證中心的前台再將 ujwt 作為 url 參數,跳回到那個回調地址上,這樣就完成了 jwt 的共享。

  文字很可能看不懂,下面是整個過程的路程圖:

相信通過上面的流程圖你應該能大概看明白,jwt 是如何共享了的吧,還看不懂的繼續看下來,下面上一個 spring boot 實現的簡易 SSO 認證。主要有兩個系統:SSO 認證中心,系統 A(系統 A 換不同端口運行就是系統 A、B、C、D 了).

實戰

實現 SSO 認證中心

  spring boot 框架先搭起來,由於是簡易項目,除 spring boot web 基本依賴,只需要如下的額外依賴:

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

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
  <version>1.2.4</version>
</dependency>
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.7.0</version>
</dependency>

完整的 POM 文件,請到 github 上查看.

後台實現

  後台做的事情並不多,只有以下 5 個方法:

  • /login : 登錄成功后簽發一個 jwt token
    在 demo 中只是簡單對比用戶名密碼如果是一樣的認為登錄成功,返回 token
  • /checkJwt : 檢查 jwt 的有效性
    檢查傳來的 jwt-token 是否有效,返回失效的 jwt 列表
  • /refreshjwt : 刷新 jwt
    判斷該 jwt 是否快要過期,如果快要過期,生成一個新的 jwt 返回
  • /inValid : 讓某個 jwt 失效
    jwt 如何失效一直是一個比較麻煩的問題,各有利弊。本例中採用的是為每個 jwt 生成一個隨機的秘鑰 secret,將 jwt–secret 保存到 redis 中,想要讓某個 jwt 失效,只需將該記錄在 redis 中刪除即可(這樣在解密時便無法獲取到 secret)。但是這樣讓無狀態的認證機制變成有狀態了(記錄了 jwt 和 secret 的對應關係)。

  總結來說 SSO 後台主要只做了兩件事:驗證用戶名密碼返回 jwt;驗證 jwt 是否合法。具體代碼查看 github 上 sso 目錄下的代碼。

前台實現

  前台的邏輯較為複雜,不是那麼容易理解,不明白的多看幾遍上面的流程圖。

  再次回到 SSO 的重點:分享登錄狀態。要如何在前台將登錄狀態(在這裏就是 jwt 字符串)分享出去呢?由於瀏覽器的限制,除了 cookie 外沒有直接共享數據的辦法。既然沒有直接共享,那肯定是有間接的辦法的!

  這個辦法就是回調。系統 A 的前台在跳轉到 SSO 的前台時,將當前路徑作為 url 參數傳遞給 sso 前台,sso 前台在獲取到 jwt 后,再跳轉到系統 A 傳過來的 url 路徑上,並帶上 jwt 作為 url 參數。這就完成了 jwt 的一次共享,從 sso 共享到系統 A。

打個比方:你點了個外賣,別人要怎麼把外賣給你呢?顯然你會留下的地址,讓別人帶上飯送到這個地址,然後你就能享用美食了。這和 jwt 的傳遞非常相識了。

系統 A 說:我要 jwt,快把它送到http://localhost:8081/test1/這個地址上。

SSO 說:好嘞,這個地址是合法的可以送 jwt 過去,這就跳轉過去:http://localhost:8081/test1/?jwt=abcdefj.asdf.asdfasf

系統 A 說:不錯不錯,真香。

  要注意這裏有個坑就是:如果另外一個惡意系統 C 安裝相同的格式跳轉到 SSO,想要獲取 jwt,這顯然是不應該給它的。所以在回跳回去的時候要判斷一下這個回調地址是不是合法的,能不能給 jwt 給它,可以向後台請求判斷也可以在 sso 前台直接寫死合法的地址。在 demo 是沒有這個判斷過程的。

實現業務系統

  業務系統代碼非常簡單,主要是用了一個攔截器,攔截 http 請求,提取出 token 向 sso 認證中心驗證 token 是否有效,有效放行,否則返回錯誤給前端。太簡單也不貼代碼了,到 github 上看看就明白了。

效果

  上面說了一大串都是原理了,其實這個難也就難在原理部分,代碼實現並沒有那麼複雜。這裏就不貼代碼了,有需要直接到 github 上看。

  這裏上幾個效果圖:

  • 系統 A 首次登陸系統

可以看到首次登陸是需要跳到 sso 認證中心輸入用戶名密碼進行登陸驗證的。登陸成功回跳後接口請求成功。

  • 將 A 的啟動端口改為 8082 后再次啟動,當作系統 B

可以看到這次是無需登陸的,跳到認證中心后就馬上跳回了,如果去掉 alert 一般是看不出跳轉過程的。

最後在任意一個系統註銷,都會讓所有的系統推出登陸。

可以看到,在系統 A 登錄系統后,系統 B,系統 C 都不再需要輸入用戶名密碼進行登錄。如果速度足夠快甚至都注意不到調到 SSO 再跳回來的過程。

源碼:github

本篇原創發佈於:www.tapme.top/blog/detail/2019-03-01-18-52

【精選推薦文章】

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

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

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

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

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

設計模式之迭代器與組合模式(四)

因為這系列篇幅較長,所以在這裏也不進行任何鋪墊,直奔主題去啦。

利用組合設計菜單

我們要如何在菜單上應用組合模式呢?一開始,我們需要創建一個組件接口來作為菜單和菜單項的共同接口,讓我們能夠用統一的做法來處理菜單和菜單項。換句話說,我們可以針對菜單或菜單項調用相同的方法。

讓我們從頭來看看如何讓菜單能夠符合組合模式的結構:

實現菜單組件

好了,我們開始編寫菜單組件的抽象類;請記住,菜單組件的角色是為恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘節點和組合節點提供一個共同的接口。

public abstract class MenuComponent {
   
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }
    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }
    public MenuComponent getChild(int i) {
        throw new UnsupportedOperationException();
    }
  
    public String getName() {
        throw new UnsupportedOperationException();
    }
    public String getDescription() {
        throw new UnsupportedOperationException();
    }
    public double getPrice() {
        throw new UnsupportedOperationException();
    }
    public boolean isVegetarian() {
        throw new UnsupportedOperationException();
    }

    public abstract Iterator<MenuComponent> createIterator();
 
    public void print() {
        throw new UnsupportedOperationException();
    }
}

讓我們來看菜單類。別忘了,這是組合類圖裡的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘類,它實現組合內元素的行為。

public class MenuItem extends MenuComponent {
 
    String name;
    String description;
    boolean vegetarian;
    double price;
    
    public MenuItem(String name, 
                    String description, 
                    boolean vegetarian, 
                    double price) 
    { 
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }
  
    public String getName() {
        return name;
    }
  
    public String getDescription() {
        return description;
    }
  
    public double getPrice() {
        return price;
    }
  
    public boolean isVegetarian() {
        return vegetarian;
    }

    public Iterator<MenuComponent> createIterator() {
        return new NullIterator();
    }
 
    public void print() {
        System.out.print("  " + getName());
        if (isVegetarian()) {
            System.out.print("(v)");
        }
        System.out.println(", " + getPrice());
        System.out.println("     -- " + getDescription());
    }

}

我們已經有了菜單項,還需要組合類,這就是我們叫做菜單的。別忘了,此組合類可以持有菜單項或其他菜單。

public class Menu extends MenuComponent {
    Iterator<MenuComponent> iterator = null;
    ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
    String name;
    String description;
  
    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }
 
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }
 
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }
 
    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }
 
    public String getName() {
        return name;
    }
 
    public String getDescription() {
        return description;
    }

  
    public Iterator<MenuComponent> createIterator() {
        if (iterator == null) {
            iterator = new CompositeIterator(menuComponents.iterator());
        }
        return iterator;
    }
 
 
    public void print() {
        System.out.print("\n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("---------------------");
    }
}

因為菜單是一個組合,包含了菜單項和其他的菜單,所以它的print()應該打印出它所包含的一切。如果它不這麼做,我們就必須遍歷整個組合的每個節點,然後將每一項打印出來。這麼一來,也就失去了使用組合結構的意義

所以,print還得進行優化,如下:

public void print() {
    System.out.print("\n" + getName());
    System.out.println(", " + getDescription());
    System.out.println("---------------------");
 
    Iterator<MenuComponent> iterator = menuComponents.iterator();
    while (iterator.hasNext()) {
        MenuComponent menuComponent = iterator.next();
        menuComponent.print();
    }
}

看到上面了沒,我們用了迭代器。用它遍歷所有菜單組件,遍歷過程中,可能遇到其他菜單,或者是遇到菜單項。由於菜單和菜單項都實現了print,那我們只要調用print即可。

開始測試數據之前,我們了解一下,在運行時菜單組合是什麼樣的:

開始運行我們的測試程序啦:

public class MenuTestDrive {
    public static void main(String args[]) {

        MenuComponent pancakeHouseMenu = 
            new Menu("PANCAKE HOUSE MENU", "Breakfast");
        MenuComponent dinerMenu = 
            new Menu("DINER MENU", "Lunch");
        MenuComponent cafeMenu = 
            new Menu("CAFE MENU", "Dinner");
        MenuComponent dessertMenu = 
            new Menu("DESSERT MENU", "Dessert of course!");
  
        MenuComponent allMenus = new Menu("ALL MENUS", "All menus combined");
  
        allMenus.add(pancakeHouseMenu);
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);
  
        pancakeHouseMenu.add(new MenuItem(
            "K&B's Pancake Breakfast", 
            "Pancakes with scrambled eggs, and toast", 
            true,
            2.99));
        pancakeHouseMenu.add(new MenuItem(
            "Regular Pancake Breakfast", 
            "Pancakes with fried eggs, sausage", 
            false,
            2.99));
        pancakeHouseMenu.add(new MenuItem(
            "Blueberry Pancakes",
            "Pancakes made with fresh blueberries, and blueberry syrup",
            true,
            3.49));
        pancakeHouseMenu.add(new MenuItem(
            "Waffles",
            "Waffles, with your choice of blueberries or strawberries",
            true,
            3.59));

        dinerMenu.add(new MenuItem(
            "Vegetarian BLT",
            "(Fakin') Bacon with lettuce & tomato on whole wheat", 
            true, 
            2.99));
        dinerMenu.add(new MenuItem(
            "BLT",
            "Bacon with lettuce & tomato on whole wheat", 
            false, 
            2.99));
        dinerMenu.add(new MenuItem(
            "Soup of the day",
            "A bowl of the soup of the day, with a side of potato salad", 
            false, 
            3.29));
        dinerMenu.add(new MenuItem(
            "Hotdog",
            "A hot dog, with saurkraut, relish, onions, topped with cheese",
            false, 
            3.05));
        dinerMenu.add(new MenuItem(
            "Steamed Veggies and Brown Rice",
            "A medly of steamed vegetables over brown rice", 
            true, 
            3.99));
 
        dinerMenu.add(new MenuItem(
            "Pasta",
            "Spaghetti with Marinara Sauce, and a slice of sourdough bread",
            true, 
            3.89));
   
        dinerMenu.add(dessertMenu);
  
        dessertMenu.add(new MenuItem(
            "Apple Pie",
            "Apple pie with a flakey crust, topped with vanilla icecream",
            true,
            1.59));
        dessertMenu.add(new MenuItem(
            "Cheesecake",
            "Creamy New York cheesecake, with a chocolate graham crust",
            true,
            1.99));
        dessertMenu.add(new MenuItem(
            "Sorbet",
            "A scoop of raspberry and a scoop of lime",
            true,
            1.89));

        cafeMenu.add(new MenuItem(
            "Veggie Burger and Air Fries",
            "Veggie burger on a whole wheat bun, lettuce, tomato, and fries",
            true, 
            3.99));
        cafeMenu.add(new MenuItem(
            "Soup of the day",
            "A cup of the soup of the day, with a side salad",
            false, 
            3.69));
        cafeMenu.add(new MenuItem(
            "Burrito",
            "A large burrito, with whole pinto beans, salsa, guacamole",
            true, 
            4.29));
 
        Waitress waitress = new Waitress(allMenus);
   
        waitress.printVegetarianMenu();
 
    }
}

結果這裏就不附上了,請大家自行去跑代碼實現吧。相信你們又對組合模式也已經有了一個大概了吧。下一篇,還有更犀利的,組合迭代器等着我們。小編馬上回去搞起來,安排上。

愛生活,愛學習,愛感悟,愛挨踢

【精選推薦文章】

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

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

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

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

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

​解讀浪潮“元腦”:不是AI大腦,欲引爆AI應用的工業化量產

​解讀浪潮“元腦”:不是AI大腦,欲引爆AI應用的工業化量產

2019-04-24 15:26    來源:廠商稿  作者: 廠商動態 編輯:
0購買

  文章來源:雲科技時代

  2016年以來,AI人工智能遍地開花。以神經元網絡為代表的深度學習成為了風靡大街小巷的熱詞,人工智能和深度學習技術創業公司如雨後春筍般湧現,各種深度學習開源框架、人工智能大腦和服務以及國際AI大賽跑分成為常態。進入2019年,全國共有35所高校獲首批“人工智能”新專業建設資格、96所高校獲批“智能科學與技術”專業、101所高校獲批“機器人工程”專業,人工智能也入選了《普通高中信息技術課程標準》新課標。

  儘管全民對於人工智能的熱情高漲,但人工智能的產業化以及在各產業和行業中推進人工智能應用,仍面臨着重重阻礙。“當前人工智能計算的業態還是相當分散的,需要買不同的機器、服務和加速卡等,還要自己配置人工智能應用環境、搭建人工智能框架,處理數據、模型和計算的管理、共享、調度等,這對客戶來說太複雜了,不知道怎麼做。”浪潮AI與HPC總經理劉軍在4月16日2019浪潮雲數據中心合作夥伴大會(IPF2019)上接受採訪時坦言,“合作夥伴更怕在幾十種選擇中組合,人工智能的生意很難做。”

  2018年Gartner與中國信通院合作的《2018世界人工智能產業發展藍皮書》也指出:人工智能仍處於早期採用階段,僅有4%的被調研企業已經投資並部署了AI技術,許多企業還處在正在考慮採用AI和規劃AI的階段。在Gartner的AI技術成熟度曲線中,仍有許多AI的相關技術都擁堵在期望膨脹期,要過渡到生產實施階段仍非常困難。此外,AI技術的碎片化十分嚴重,據藍皮書統計在中國市場就有上千家各類AI技術供應商。

  在IPF2019大會上,浪潮發布了“元腦”:打包了計算、算法、框架、PaaS以及服務的一站式人工智能解決方案交付。劉軍表示,“元腦”不是人工智能“大腦”,而是“大腦”的核心,浪潮希望與合作夥伴一起,基於“元腦”快速“量產”各行業的人工智能應用和“大腦”。劉軍表示,“元腦”的意義不亞於當年的福特T型車,福特並不是汽車的發明者,但T型車開創了汽車工業化時代,浪潮也希望“元腦”能開創人工智能應用的工業化時代。

  

  浪潮發布“元腦”

  1908年福特T型車的啟示

  1908年是人類歷史上一個重要的年份,這一年福特推出了歷史上著名的T型車。1903年成立的福特公司,並不是汽車的發明者。從1886年的世界上第一輛四輪汽車,到1893年發明了化油器以及1896年發明了石棉製動片和轉向盤等,汽車的結構越來越趨向現代汽車。但1908年的福特T型車,則首次讓汽車以低廉的價格走入了尋常百姓家,T型車不僅開創了現代汽車工業,更成就了“車輪上的國家”——美國。

  T型車的生產製造過程,開創了革命性的流水裝配線,代替了之前的個體手工製作方式。T型車是世界上第一種以大量通用零部件進行大規模流水線裝配作業而生產製造出來的汽車,1913年到1914年經過優化的流水裝配線可以在93分鐘內生產一輛汽車,超過同期其它所有汽車生產商生產能力的總合。最開始,T型車以競爭車型1/3的價格推動了汽車的普及,1920年後進一步降低到其它競爭車型1/10價格,讓普通生產線工人也能購買屬於自己的汽車。直到1927年停產,共生產了1500萬輛T型車,這一紀錄保持了近一個世紀。

  浪潮“元腦”就是福特T型車這樣的定位:不僅要開創人工智能應用的工業化量產,還要創造T型車這樣的爆款產品。此前,浪潮已經推出了一個AI爆款產品,這就是2017年推出的AGX-2 2U GPU服務器。2017年5月,浪潮推出了第一版的AGX-2 2U GPU服務器,當時是全球首款在2U空間內高速互聯集成8顆最高性能GPU加速器的服務器,2018年3月更新支持了最新的NVIDIA架構,成為全球性能最高的AI 2U服務器。儘管後來浪潮又發布了超級AI服務器AGX-5,但AGX-2仍然是受到廣大用戶喜歡的爆款產品。

  在運營商領域:浪潮AGX-2中標中國電信人工智能服務器集中採購,份額高達40%,大規模用於智能營業廳、智能推薦等場景人工智能訓練;中國移動子公司咪咕文化科技有限公司(簡稱:咪咕)2018年GPU服務器採購項目中,浪潮獲得億元採購訂單一半份額,為咪咕提供涵蓋創新AI服務器AGX-2在內的AI解決方案。在金融領域:AGX-2幫助招商銀行建設了先進的大數據智能平台以及多種人工智能應用場景。在視頻物聯等領域:海康威視的視頻人工智能訓練平台100%基於AGX-2,而大華以AGX-2為核心的人工智能解決方案,在智慧城市、雪亮工程、平安城市等多個領域落地。

  有了前面的這些市場基礎,在2019年,浪潮希望進一步高度集成AI技術、產品和服務於“元腦”,用“元腦”打開人工智能應用的工業化生產變革。

  浪潮“元腦”的構成

  什麼是浪潮“元腦”呢?具體來說包括以下組成部分:

  · 超強AI計算系統:通過浪潮人工智能計算平台、人工智能超高速計算加速卡、極低延遲RDMA網絡與超高帶寬并行存儲,共同提供極致人工智能計算性能;

  · 敏捷AI Paas平台:由極致優化的AI資源平台、極速流程化AI開發平台、開放兼容的AI生態平台和秒速構建AI軟件棧。最新開發的人工智能PaaS平台AIStation面向人工智能企業訓練場景,可實現容器化部署、可視化開發、集中化管理等,有效打通開發環境、計算資源與數據資源,提升開發效率;

  · 高效的Auto ML Suite:最新開發的AutoML Suite可實現非專業人員亦能通過極少操作構建網絡模型並獲得高精度,極大降低了人工智能開發、應用的門檻和成本。在2018年的NIPS 的自動機器學習挑戰賽中,浪潮與北京郵電大學、中南大學團隊合作,獲得自動機器學習領域的國際頂尖賽事的全球第三佳績。

  · 整合一體化交付:計算/存儲/網絡一體化、內置AI Paas平台、內置建模優化工具、預配置系統調優。

  

  中國工程院院士、浪潮集團執行總裁王恩東

  IPF 2019大會的主題為“智慧凝聚”,中國工程院院士、浪潮集團執行總裁王恩東的大會主題演講題目為《人工智能計算 未來核心動力》以及提出了人工智能三大趨勢——融合、開放、敏捷,這兩大主題和三大趨勢都透露了浪潮要“凝聚”過去幾年在人工智能方面的重大投資,並希望用融合、開放和敏捷的人工智能產品及解決方案拉升数字經濟大勢的想法和思路。而浪潮“元腦”就是基於融合、開放和敏捷的人工智能工業化生產線裝配出來的“T型車”。

  激進拉升千億市場到萬億

  “2018年,浪潮與合作夥伴一起創造了諸多令人振奮的成績,見證了服務器產業的快速發展,也體驗了人工智能產業的爆髮式增長。”王恩東在IPF 2019上如是表示。他認為,人工智能發展能為中國經濟帶來巨大增長機遇,據有關預測,到2035年人工智能領域的經濟總量將佔所有經濟的20%,所以“計算力就是生產力,而人工智能計算則是核心計算力”。

  在人工智能時代,浪潮認為有兩大市場:一個是千億的人工智能產業市場,另一個是萬億的產業人工智能市場。一方面,浪潮通過技術產品化、產品市場化,推動AI科技公司的技術成果轉化,也就是AI產業化的過程,創造和實現千億市場價值。另一方面就是產業AI化,即在傳統產業和行業的轉型和升級中,行業ISV和分銷渠道商鏈接着眾多客戶,浪潮通過生態協作方式,把AI計算平台、AI科技公司的技術能力和ISV的行業方案進行整合,通過分銷渠道交付到各行各業,從而撬動萬億級市場。

  那麼,如何普及人工智能計算這種先進的新型生產力?生產力有三大要素:生產力、生產工具和生產資料。其中,浪潮“元腦”就已經交付了先進了生產工具。浪潮集團副總裁彭震強調,浪潮“元腦”提供了一體化的解決方案,把浪潮的人工智能計算能力、存儲能力、網絡能力以及人工智能PaaS平台、AutoML Suite自動算法調優工具等集合在一起,通過浪潮專家的預配置、預調優、預集成,實現了完整平台級解決方案。

  

  浪潮集團副總裁彭震

  接下來就需要把浪潮“元腦”這樣的先進生產工具交付到更多的勞動者手中,讓他們在數據這種新型“生產資料”上,創造更多新型的人工智能應用。為了達成這一目標,光依靠浪潮自己遠遠不夠,還需要動員龐大渠道生態合作夥伴體系。截止至2018年底,浪潮共有9000多家合作夥伴,人工智能百強超過80家與浪潮建立了合作關係;合作夥伴業務增長116%,分銷夥伴增長124%;浪潮與合作夥伴開發聯合解決方案400多個,其中人工智能聯合方案20多個。

  為了把合作夥伴帶入人工智能時代,讓更多的合作夥伴儘早入場人工智能,劉軍表示浪潮將有可能採取激進的策略,包括激進的價格策略以激發整體的人工智能大市場。劉軍強調,人工智能產業和產業人工智能化都處於初期階段,浪潮在這個階段的策略不是自己實現更多的營收,而是要拉動合作夥伴都能獲益。儘管很多合作夥伴對於如何做人工智能生意還有很大的困惑,但劉軍認為“不要在岸上看,一起下來‘游泳’”。

  當然,浪潮也做好了充分的準備。浪潮集團渠道管理部總經理王峰在IPF 2019上提出了浪潮合作生態2.0,將要扮演的三個新角色:生態孵化器、AI試驗場和AI放大器。生態孵化器指的是浪潮將積極幫助AI技術公司、行業ISV以及分銷商,完成AI產品、應用和市場孵化,特別是針對區域行業客戶的AI場景,以及培養更多的AI人才;AI試驗場,指的是浪潮“元腦”與AI技術公司針對不同AI場景應用的聯合方案開發、預裝與轉售,通過浪潮方案創新平台InCloud Lab與行業ISV完成AI產品和方案的POC測試和驗證,以及通過浪潮分銷業務體系面向中小行業客戶的AI應用一體化產品;AI放大器,指的是浪潮將與合作夥伴、AI算法夥伴、AI場景應用+行業ISV夥伴一起,完成多行業的複製、推動多產業的AI化,其中包括面向全行業的行業標桿客戶AI應用以及面向區域市場客戶的一體化AI應用方案產品。

  浪潮在AI渠道總體上的2019目標是:發展 200家AI算法夥伴,每個夥伴要有面向行業場景的算法軟件,至少發布一個AI場景應用、培養AI場景應用認證工程師;發展400家AI方案集成夥伴 ,每個夥伴要有行業應用解決方案、行業AI應用整合和交付能力,要落地三個行業客戶AI應用方案、舉行一次行業客戶AI應用推薦、培養AI行業應用認證工程師;以及80家分銷商和2400經銷商組成的AI方案產品銷售夥伴,每個夥伴要有區域客戶AI應用產品交付和服務能力,每個分銷商要激活30家AI產品銷售經銷商以及通過AI應用產品銷售工程師認證。

  2019年,浪潮渠道還將特別聚焦計算機視覺、語音識別、自然語言識別、量化交易等四大基礎AI應用場景,集中發展100家以上算法合作夥伴,幫助400家以上的行業ISV建立AI能力,在金融、企業、通信、教育、零售、醫療、媒體和互聯網等8大行業落地客戶。

  總體來說,IPF 2019拉開了人工智能應用的工業化時代,是浪潮過去幾年在人工智能計算領域投資的大匯總和高度凝練,是對渠道生態合作夥伴的總動員,其意義不亞於浪潮在2015年所提出的“計算+”戰略及“硬件重構”和“軟件定義”核心。在智慧時代,AI與產業深度融合的大幕已經拉開,“融合、開放、敏捷”將要定義新的計算力——人工智能計算。

,

網站內容來源http://server.it168.com/本站聲明:網站內容來源http://www.it168.com/,如有侵權,請聯繫我們,我們將及時處理

【精選推薦文章】

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

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

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

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

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