.net core 微服務之 CAP事件總線

概念

什麼是事件

事件就是指事物狀態的變化,每一次事物變化的結果都稱作爲事件

 

 什麼是事件總線

就是用來管理所有的事件的一種機制就稱作爲事件總線

包括事件發佈,事件存儲,事件訂閱,事件處理的統稱

作用:

事件總線是一種機制,它允許不同的組件彼此通信而不彼此瞭解。 組件可以將事件發送到Eventbus,而無需知道是誰來接聽或有多少其他人來接聽。 組件也可以偵聽Eventbus上的事件,而無需知道誰發送了事件。 這樣,組件可以相互通信而無需相互依賴。 同樣,很容易替換一個組件。 只要新組件瞭解正在發送和接收的事件,其他組件就永遠不會知道.

 

 

爲什麼要使用事件總線

 將微服務系統各組件之間進行解耦。使用業務的發展來說,比如說微服務架構中服務之間通信不通過api,而直接通過消息隊列事件總線的方式

事件總線框架---CAP

事件 : 就是一些狀態信息

發佈者:發佈事件的角色 cap

訂閱者:訂閱消費事件的角色 cap

消息傳輸器:傳輸事件

消息存儲器:存儲事件

CAP存儲事件消息隊列類型

Azure

rabbitmq

kafaka

In Memory Queue

CAP存儲事件持久化類型

SQL Server

MySQL

PostgreSQL

MongoDB

InMemoryStorage

環境搭建

RabbitMQ環境安裝

Erlang下載地址:https://www.erlang.org/downloads

​RabbitMQ下載地址:https://www.rabbitmq.com/download.html

安裝RabbitMQ要先安裝Erlang,是因爲RabbitMQ是Erlang開發的,先安裝Erlang環境。

啓動命令:

1、在安裝目錄下添加可視化插件

rabbitmq-plugins enable rabbitmq_management

2、在安裝目錄下啓動

rabbitmq-server 

3、查看rabbitmq狀態

rabbitmqctl status

安裝好了就可以直接啓動: http://127.0.0.1:15672     賬號密碼默認都是 guest

CAP環境

CAP官網地址:https://cap.dotnetcore.xyz/user-guide/zh/monitoring/dashboard/

事件總線示例

簡單使用RabbitMQ(有很多種消息隊列類型,這裏選用的是RabbitMQ作爲傳輸事件)

1.  添加Nuget包

DotNetCore.CAP
DotNetCore.CAP.MySql
DotNetCore.CAP.RabbitMQ

2. 注入服務

services.AddCap(x =>
            {
                x.UseMySql("server=localhost;port=3306;user=root;password=xxx;database=fcbcap;SslMode=none;");
                x.UseRabbitMQ(rb =>
                {
                    rb.HostName = "localhost";
                    rb.UserName = "guest";
                    rb.Password = "guest";
                    rb.Port = 5672;
                    //rb.VirtualHost = "/";
                });

           x.FailedRetryInterval = 60; //重試間隔時間(秒),默認60秒
           x.FailedRetryCount = 50; //重試次數,默認50次;注:這裏會先重試3次,四分鐘之後纔會按照重試間隔去執行剩下重試


 

           x.UseDashboard(dashoptions =>
           {
             dashoptions.PathMatch = "/cap";  //面板地址
           });


            });

3. 依賴注入ICapPublisher,添加測試代碼

private readonly ICapPublisher capPublisher;
public UserController(ICapPublisher capPublisher)
{
    this.capPublisher = capPublisher;
}
[HttpGet("AddUser")]
public async Task<IActionResult> AddUser()
{
    capPublisher.Publish("addcity", new City() { CityName = "廣安市" });
    return Ok("添加成功");
}
[CapSubscribe("addcity")]
public async Task<IActionResult> AddCity(City city)
{
    Console.WriteLine($"添加內容:{city.CityName}");
    return Ok();
}

運行結果,可以看到生成了數據庫記錄

發佈者和訂閱者可以在不同的服務,只要連接的消息隊列地址是一致的就行

 

 

 消息隊列通配符

 

RabbitMQ 中有四種主要類型的交換機(Exchange):

  1. 直連交換機(Direct Exchange):直連交換機根據消息的路由鍵(Routing Key)將消息發送到與之完全匹配的隊列。如果消息的路由鍵與綁定到交換機上的隊列的路由鍵完全匹配,那麼消息將被路由到該隊列。

  2. 主題交換機(Topic Exchange):主題交換機根據消息的路由鍵和主題規則(通配符)將消息發送到一個或多個隊列。主題規則可以使用通配符符號 * 和 # 進行模糊匹配,其中 * 表示匹配一個單詞,# 表示匹配零個或多個單詞。

  3. 扇形交換機(Fanout Exchange):扇形交換機將消息廣播到所有綁定到該交換機上的隊列。無論消息的路由鍵是什麼,扇形交換機都會將消息發送到與之綁定的所有隊列。

  4. 頭交換機(Headers Exchange):頭交換機根據消息的頭部屬性(Headers)進行匹配,並將消息發送到與之匹配的隊列。頭交換機不使用消息的路由鍵進行匹配,而是根據消息的頭部屬性進行匹配。

在Cap中默認爲topic類型的交換機

 

# : 匹配0個或一個或多個詞

* : 只能匹配一個詞
例如:addcity.#既可以匹配addcity.a.b,也可以匹配addcity.a
而 addcity.*只能匹配item.a

 

 如果發佈 addcity.a ,訂閱者會優先匹配 addcity.a ,其次是addcity.* , 最後是addcity.# 。

 

升級用法

 死信隊列

死信隊列(DLX,Dead-Letter-Exchange),利用DLX,當消息在一個隊列中變成無法被消費的消息(dead message)之後,它能被重新publish到另一個Exchange,這個Exchange就是DLX。

消息變成死信的幾種情況:

1、 消息被拒絕(
channel.basicReject/channel.basicNack)並且request=false;

2、 消息在隊列的存活時間超過設置的生存時間(TTL)時間;

3、 隊列達到最大長度(隊列滿了,無法再添加數據到隊列中)。

DLX也是一個正常的Exchange,和一般的Exchange沒有區別,它能在任何的隊列上被指定,實際上就是設置某個隊列的屬性。

Cap沒有看到設置死信隊列,我就在消息重試次數用完後仍然失敗,就加入了一個死信隊列,用於記錄消息的內容,方便後續處理維護,用於模擬死信隊列

1. 設置program.cs,主要是處理FailedThresholdCallback

builder.Services.AddTransient<CapSubscribeService>();
CapConfig config = Configuration.GetSection("CapConfig").Get<CapConfig>();
builder.Services.AddCap(x =>
{
    x.UseMySql(config.CapConnectString);
    x.UseRabbitMQ(rb =>
    {
        rb.HostName = config.HostName;
        rb.UserName = config.UserName;
        rb.Password = config.Password;
        rb.Port = config.Port ;
        //rb.VirtualHost = "/";
    });
    x.FailedRetryInterval = 60;   //重試間隔時間(秒),默認60秒
    x.FailedRetryCount = 1;    //重試次數,默認50次
    x.FailedThresholdCallback = (failedinfo) =>  //如果重試完還是失敗會進入這裏,這裏就處理進死信隊列裏面,後期可以手動處理
    {
        var _capPublisher = failedinfo.ServiceProvider.GetService<ICapPublisher>();
        var header = new Dictionary<string, string>()
        {
            ["header.error.msgid"] =failedinfo.Message.Headers["cap-msg-id"],
            ["header.error.msgname"] = failedinfo.Message.Headers["cap-msg-name"]
        };
        //發佈消息失敗記錄日誌
        if (failedinfo.MessageType == MessageType.Publish)
        {
            _capPublisher.Publish("publish-dead-letter-queue", failedinfo.Message.Value, header);
        }
        if (failedinfo.MessageType == MessageType.Subscribe)
        {
            _capPublisher.Publish("subscribe-dead-letter-queue", failedinfo.Message.Value, header);
        }
    };
    x.UseDashboard(dashoptions =>
    {
        dashoptions.PathMatch = "/cap";
    });
});

2. 添加發布代碼

  [HttpGet("TestPublish")]
        public async Task<IActionResult> TestPublish()
        {
            Console.WriteLine($"已經發布消息");
            capPublisher.Publish("testsubscribe", "hello");
            return Ok();
        }

3. 添加訂閱代碼。這裏添加了兩個死信隊列,分別爲發佈和訂閱的死信隊列,可以分別記錄失敗類型、消息id、失敗的消息內容、失敗的消息方法等等,方便後期手動處理。

    public class CapSubscribeService : ICapSubscribe
    {
        private readonly ICapPublisher _capPublisher;
        public CapSubscribeService(ICapPublisher capPublisher)
        {
            _capPublisher = capPublisher;
        }
        [CapSubscribe("testsubscribe")]
        private void TestSubscribe(string body, [FromCap] CapHeader header)
        {
            Console.WriteLine($"已經訂閱了消息:{body}");
            
            throw new Exception("手動拋出異常");
        }
       
        /// <summary>
        /// 發佈死信隊列監控
        /// </summary>
        /// <param name="body"></param>
        [CapSubscribe("publish-dead-letter-queue")]
        private void PublishDeadQueue(string body, [FromCap] CapHeader header)
        {
            Console.WriteLine("發佈異常");
            Console.WriteLine($"進入了發佈死信隊列,消息內容:{body}");
            Console.WriteLine($"異常的消息id:{header["header.error.msgid"]}");
            Console.WriteLine($"異常的消息方法名:{header["header.error.msgname"]}");
            Console.WriteLine($"當前消費時間:{header["cap-senttime"]}");
            //寫入數據庫和日誌


        }
        /// <summary>
        /// 訂閱死信隊列監控
        /// </summary>
        /// <param name="body"></param>
        [CapSubscribe("subscribe-dead-letter-queue")]
        private void SubscribeDeadQueue(string body, [FromCap] CapHeader header)
        {
            Console.WriteLine("訂閱異常" );
            Console.WriteLine($"進入了訂閱死信隊列,消息內容:{body}");
            Console.WriteLine($"異常的消息id:{header["header.error.msgid"]}");
            Console.WriteLine($"異常的消息方法名:{header["header.error.msgname"]}");
            Console.WriteLine($"當前消費時間:{header["cap-senttime"]}");
            //寫入數據庫和日誌


        }

    }

 

冪等性

 對於一個資源,不管你請求一次還是請求多次,對該資源本身造成的影響應該是相同的,不能因爲重複相同的請求而對該資源重複造成影響。

雖然rabbitmq處理完成功的消息後會刪除,但是可能有其他原因導致重複消費。或者有這樣的情景:訂單業務中,當消費方法處理訂單新增的業務已經成功了,但是寫入日誌的地方報錯了,導致觸發了重試機制一直進行請求消費。

對於普通業務,可以記錄消息id或者自定義的唯一鍵去處理,每次處理都查詢這個消息id或者自定義的唯一鍵是否存在,再去處理。

        [HttpGet("TestPublish2")]
        public async Task<IActionResult> TestPublish2()
        {
            var header = new Dictionary<string, string>()
            {
                ["my.header.id"] = Guid.NewGuid().ToString()
            };
            capPublisher.Publish("testsubscribe2", "hello", header);
            return Ok();
        }

  

        [CapSubscribe("testsubscribe2")]
        private void TestSubscribe2(string body,[FromCap] CapHeader header)
        {
            Console.WriteLine($"已經訂閱了消息:{body}");
            Console.WriteLine($"當前消息id{header["cap-msg-id"]}");
            Console.WriteLine($"當前消費時間:{header["cap-senttime"]}");
            Console.WriteLine("header 中自定義傳送的唯一鍵:" + header["my.header.id"]);

        }

如果是用了集羣或者分佈式服務來說,或者對冪等性要求比較高,這裏可能要處理分佈式鎖,利用redis執行setnx命令 來實現

 

延時發佈

在指定一段時間之後進行訂閱,使用場景比如在下單的時候,超過30分鐘會自動取消訂單,就可以用延時發佈在30分鐘後檢測訂單是否已經完成了支付,如果未完成就取消訂單

  [HttpGet("TestPublish4")]
        public async Task<IActionResult> TestPublish4()
        {
            Console.WriteLine($"已經發布延時消息,將在20秒之後消費,當前時間爲{DateTime.Now}");
            capPublisher.PublishDelay(TimeSpan.FromSeconds(20), "testdelaysubscribe", "hello");
            return Ok();
        }

 

 

補償事務

也就是回調,當消息被訂閱後會執行指定的補償事務,方便執行後續的事務

        [HttpGet("TestPublish3")]
        public async Task<IActionResult> TestPublish3()
        {
            Console.WriteLine($"已經發布消息");
            capPublisher.Publish("testsubscribe3", "hello",callbackName: "callbacksubscribe3");
            return Ok();
        }

 

 

        [CapSubscribe("testsubscribe3")]
        private object TestSubscribe3(string body, [FromCap] CapHeader header)
        {
            Console.WriteLine($"當前消息id{header["cap-msg-id"]}");
            Console.WriteLine($"當前消費時間:{header["cap-senttime"]}");
            return new { city = "四川" , IsSuccess = true };

        }
        /// <summary>
        /// 補償事務,理解爲回調
        /// </summary>
        /// <param name="param"></param>
        /// <param name="header"></param>
        [CapSubscribe("callbacksubscribe3")]
        private void CallbackSubscribe3(JsonElement param, [FromCap] CapHeader header)
        {
            var city = param.GetProperty("city").GetString();
            var IsSuccess = param.GetProperty("IsSuccess").GetBoolean();
            Console.WriteLine($"上一級訂閱的消息id{header["cap-corr-id"]}");
            Console.WriteLine($"當前補償事務消息id{header["cap-msg-id"]}");
            Console.WriteLine($"當前消費時間:{header["cap-senttime"]}");
            Console.WriteLine($"回調城市: {city}");
            Console.WriteLine($"是否成功: {IsSuccess}");
        }

 

 Cap集成 Kafka 

kafka 安裝

1. 安裝ZooKeeper

1.1 要使用 Kafka,通常需要安裝和配置 ZooKeeper。ZooKeeper 是 Kafka 的依賴組件,用於協調和管理 Kafka 集羣的狀態信息。

在 Kafka 中,ZooKeeper 用於存儲元數據、主題和分區的信息,並協調 Kafka 代理之間的通信。Kafka 代理將自己的狀態信息註冊到 ZooKeeper,並通過與 ZooKeeper 保持連接來獲取集羣和主題的元數據。

官網下載地址: 找到適合的版本進行下載

https://zookeeper.apache.org/releases.html#download
https://downloads.apache.org/zookeeper/

如果很卡,這裏有一個國內的下載地址

https://mirrors.bfsu.edu.cn/apache/zookeeper/

 將安裝包下載考拷貝到到Linux服務器中。如果是想通過連接下載,直接用命令

wget <ZooKeeper下載鏈接>

1.2 解壓和配置 ZooKeeper

tar -xzf <ZooKeeper安裝包文件名>

進入解壓後的 ZooKeeper 目錄

cd <ZooKeeper解壓後的目錄>

創建一個用於存儲數據的目錄

mkdir data

在 ZooKeeper 目錄中複製默認配置文件,並進行必要的編輯,根據需要修改 ZooKeeper 的端口、數據目錄等設置。將數據目錄改成創建的data地址  dataDir=<data目錄地址>

cp conf/zoo_sample.cfg conf/zoo.cfg
vim conf/zoo.cfg

 

1.3  啓動 ZooKeeper:

啓動 ZooKeeper 服務器 

./bin/zkServer.sh start

2. 安裝kafka

2.1 官網下載地址: 找到適合的版本進行下載。

https://zookeeper.apache.org/releases.html#download

官網版本中Source download版是包含源碼,除了正常使用功能,還可以進行修改的;Binary downloads 是編譯包,只能用功能,正常我們用這個版本就行了,裏面還有Scala2.12 和 Scala2.13 兩個版本,Scala 是一種運行在 Java 虛擬機上的編程語言,而 Kafka 是使用 Scala 編寫的,兩者區別

Scala 2.12:
   - 這些版本的 Kafka 是使用 Scala 2.12.x 編寫的。
   - Scala 2.12 是 Scala 的一個主要版本,它引入了一些新的特性和改進。
   - 如果你的應用程序或環境使用 Scala 2.12.x,那麼你應該選擇對應的 Scala 2.12 版本的 Kafka。

Scala 2.13:
   - 這些版本的 Kafka 是使用 Scala 2.13.x 編寫的。
   - Scala 2.13 是 Scala 的另一個主要版本,它也帶來了一些新的特性和改進。
   - 如果你的應用程序或環境使用 Scala 2.13.x,那麼你應該選擇對應的 Scala 2.13 版本的 Kafka。

  

如果官網地址很卡,這裏有一個國內的下載地址,找到自己對應的版本,注意:例如 kafka_2.12-3.6.0.tgz  ,其中2.12是Scala版本,後面的3.6.0纔是kafka的版本號

https://downloads.apache.org/kafka/

 

2.2  解壓和配置 Kafka:

tar -xzf <Kafka安裝包文件名>

進入解壓後的 Kafka 目錄

cd <Kafka解壓後的目錄>

進行必要的配置編輯:

vim config/server.properties

在配置文件中,你可以根據需要修改 Kafka 的監聽端口、日誌目錄,分區,線程等設置。

#這個是配置集羣用的,如果需要用到集羣,就需要配置不同的id,比如這裏用0,下一臺kafka就用1
broker.id=0    
#這個監聽地址把ip改成kafka服務器ip地址,方便其他服務器能夠訪問
listeners=PLAINTEXT://192.168.230.130:9092
# 日誌目錄,這裏這個目錄地址很重要,如果後面需要停止kafka, 但是可能會停止失敗,就是這裏的目錄被佔用或者鎖住了,就需要先刪除這個目錄後進行停止
log.dirs=/tmp/kafka-logs
# ZK的連接地址
zookeeper.connect=192.168.230.130:2181

 

2.3 . 啓動 Kafka 服務器:

bin/kafka-server-start.sh config/server.properties

注意這裏使用啓動之後看日誌是否有異常,有很多坑,啓動成功後看本地服務器能夠連接,如果連接不上,看端口是否被其他進程佔用了,先殺掉進程或者改監聽的端口號再次啓動

客戶端下載kafka tool工具連接,檢驗是否已經啓動成功了。下載地址

https://www.kafkatool.com/download.html

安裝 成功後看Brokers是否有數據,如果有,說明kafka和ZK都沒有問題

 使用Kafka

相關概念:
Broker:消息中間件處理節點,一個Kafka節點就是一個broker,多個broker可以組成一個Kafka集羣。 Topic:一類消息,Kafka集羣能夠同時負責多個topic的分發。也就是我們訂閱和發佈的消息名稱。 Partition:topic物理上的分組,一個topic可以分爲多個partition,每個partition是一個有序的隊列。 Segment:partition物理上由多個segment組成 Offset:每個partition都由一系列有序的、不可變的消息組成,這些消息被連續的追加到partition中。partition中的每個消息都有一個連續的序列號叫做offset,用於partition唯一標識一條消息。 groupid(消費者組ID):是用於標識一組消費者的字符串。消費者組是一組具有相同groupid的消費者,它們共同消費一個或多個 Kafka 主題中的消息。   關於groupid的特點和使用方式:     唯一性:每個消費者組的groupid必須是唯一的。不同的消費者組可以同時消費同一個主題,每個消費者組都會獨立地管理自己的偏移量。     消費者組協調器:Kafka 使用一個特定的消費者作爲消費者組的協調器(coordinator)。協調器負責分配分區給消費者,並跟蹤消費者的偏移量。協調器通過groupid來識別消費者組。     負載均衡:當消費者加入或離開消費者組時,協調器會重新分配分區給消費者,實現負載均衡。這樣,每個消費者都可以處理一部分分區,並且整個消費者組能夠並行地消費消息。     消費者偏移量:協調器會跟蹤每個消費者在每個分區上的偏移量。這樣,即使消費者組中的消費者發生故障或重新加入,它們也能夠從之前的偏移量處繼續消費消息。

如果不想用Cap,可以用kafka的集成.net 客戶端包 Confluent.Kafka 。官方地址:https://github.com/confluentinc/confluent-kafka-dotnet

Cap使用就很簡單,只需要在CapOption中注入使用就行了。

 x.UseKafka(KafkaOptions =>
        {
            KafkaOptions.Servers = "kafka的ip+端口";
        });

消息的發佈和訂閱都和上面RabbitMq一樣,只是配置不一樣最後整理一下kafka和rabbitmq集成在Cap中的配置

Program.cs 配置

builder.Services.AddTransient<CapSubscribeService>();
CapConfig config = Configuration.GetSection("CapConfig").Get<CapConfig>();
builder.Services.AddCap(x =>
{
    x.UseMySql(config.CapConnectString);
    if (config.MQType == "kafka") //根據配置切換使用rabbit 還是 kafka
    {
        x.UseKafka(KafkaOptions =>
        {
            KafkaOptions.Servers = config.KafkaServers;
        });
    }
    else
    {
        x.UseRabbitMQ(rb =>
        {
            rb.HostName = config.RabbitHostName;
            rb.UserName = config.RabbitUserName;
            rb.Password = config.RabbitPassword;
            rb.Port = config.RabbitPort;
            //rb.VirtualHost = "/";
        });
    }


    x.FailedRetryInterval = 60;   //重試間隔時間(秒),默認60秒
    x.FailedRetryCount = 50;    //重試次數,默認50次
    x.FailedThresholdCallback = (failedinfo) =>  //如果重試完還是失敗會進入這裏,這裏就處理進死信隊列裏面,後期可以手動處理
    {
        var _capPublisher = failedinfo.ServiceProvider.GetService<ICapPublisher>();
        var header = new Dictionary<string, string>()
        {
            ["header.error.msgid"] =failedinfo.Message.Headers["cap-msg-id"],
            ["header.error.msgname"] = failedinfo.Message.Headers["cap-msg-name"]
        };
        //發佈消息失敗記錄日誌
        if (failedinfo.MessageType == MessageType.Publish)
        {
            _capPublisher.Publish("publish-dead-letter-queue", failedinfo.Message.Value, header);
        }
        if (failedinfo.MessageType == MessageType.Subscribe)
        {
            _capPublisher.Publish("subscribe-dead-letter-queue", failedinfo.Message.Value, header);
        }
    };
    x.UseDashboard(dashoptions =>
    {
        dashoptions.PathMatch = "/cap";
    });
});

appsettings.json 配置

  "CapConfig": {
    "CapConnectString": "server=192.168.0.208;port=3306;user=root;password=N5_?MCaE$wDDe2PG;database=netcore3_0_mq;SslMode=none;",
    "MQType": "kafka", //kafka    rabbitmq    
    "RabbitHostName": "192.168.0.237",
    "RabbitUserName": "admin",
    "RabbitPassword": "admin",
    "RabbitPort": "5672",
    "KafkaServers": "192.168.230.130:9092"
  }

  

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章