.NET Core微服务之基于MassTransit实现数据最终一致性(Part 1)
Tip: 此篇已加入.NET Core微服务基础系列文章索引
一、预备知识:数据一致性
关于数据一致性的文章,园子里已经有很多了,如果你还不了解,那么可以通过以下的几篇文章去快速地了解了解,有个感性认识即可。
(1)左正,《保证分布式系统数据一致性的6种方案》
(2)成金之路,《分布式系统的数据一致性解决方案》
(3)E_Star,《分布式环境下数据一致性的设计总结》
(4)Itegel,《分布式事务?No,最终一致性》
必须要了解的点:ACID、CAP、BASE、强一致性、弱一致性、最终一致性。
CAP理论由加州大学伯克利分校的计算机教授Eric Brewer在2000年提出,其核心思想是任何基于网络的数据共享系统最多只能满足数据一致性(Consistency)、可用性(Availability)和网络分区容忍(Partition Tolerance)三个特性中的两个(由此我们知道在分布式系统中,同时满足CAP三个特性是不可能的),三个特性的定义如下:
C:数据一致性(Consistency):如果系统对一个写操作返回成功,那么之后的读请求都必须读到这个新数据;如果返回失败,那么所有读操作都不能读到这个数据,对调用者而言数据具有强一致性(Strong Consistency)(又叫原子性Atomic或线性一致性Linerizable Consistency)
A:服务可用性(Availability):所有读写请求在一定时间内得到响应,可终止、不会一直等待
P:分区容错性(Partition-Tolerance):在网络分区的情况下,被分隔的节点仍能正常对外服务
- 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。=> 在传统单体式应用中,大部分都是强一致性的应用,想想我们写过多少工作单元模式的Code?
- 弱一致性:系统并不保证续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。
- 最终一致性:弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。
为保证可用性,互联网分布式架构中经常将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。
在微服务架构中,各个微服务之间通常会使用事件驱动通信和发布订阅系统实现最终一致性。
更多背景知识,还是得看上面列出的参考文章,这里不再赘述。
二、MassTransit极简介绍
MassTransit 是一个自由、开源、轻量级的消息总线, 用于使用. NET 框架创建分布式应用程序。MassTransit 在现有消息传输上提供了一组广泛的功能, 从而使开发人员能够友好地使用基于消息的会话模式异步连接服务。基于消息的通信是实现面向服务的体系结构的可靠和可扩展的方式。
官网地址:http://masstransit-project.com/,GitHub地址:https://github.com/MassTransit/MassTransit (目前:1590Star,719Fork)
类似的国外开源组件还有NServiceBus,没有用过,据说MassTransit比NServiceBus更加轻量级,并且在开发之初就选用了RabbitMQ作为消息传输组件,当然MassTransit还支持Azure Service Bus。类似的国内开源组件则有园友savorboard(杨晓东)的CAP,这个我会在MassTransit学习结束后去使用使用,CAP在GitHub上已经有了超过1000个Star,是NCC的几个千星项目之一。另外,张善友大队长在他的NanoFabric项目中推荐我们使用Rebus和Ray,如下图所示:
由于时间和精力,以及文档资料的可见性,我在我的POC和这个系列博文的准备中,只会使用到MassTransit和CAP这两个开源项目。
三、MassTransit Quick Start
这里以MassTransit + RabbitMQ为例子,首先请确保安装了RabbitMQ,如果没有安装,可以阅读我的《基于EasyNetQ使用RabbitMQ消息队列》去把RabbitMQ先安装到你的电脑上。另外,RabbitMQ的背景知识也有一堆,有机会也还是要了解下Exchange,Channel、Queue等内容。
3.1 最简单的发送/接收实例
(1)准备两个控制台程序,一个为Sender(发送者),一个为Receiver(接收者),并分别通过NuGet安装MassTransit以及MassTransit.RabbitMQ
NuGet>Install-Package MassTransit
NuGet>Install-Package MassTransit.RabbitMQ
(2)编写Sender
public class Program { public static void Main(string[] args) { Console.Title = "MassTransit Client"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); }); var uri = new Uri("rabbitmq://192.168.80.71/EDCVHOST/Qka.MassTransitTest"); var message = Console.ReadLine(); while (message != null) { Task.Run(() => SendCommand(bus, uri, message)).Wait(); message = Console.ReadLine(); } Console.ReadKey(); } private static async void SendCommand(IBusControl bus, Uri sendToUri, string message) { var endPoint = await bus.GetSendEndpoint(sendToUri); var command = new Client() { Id = 100001, Name = "Edison Zhou", Birthdate = DateTime.Now.AddYears(-18), Message = message }; await endPoint.Send(command); Console.WriteLine($"You Sended : Id = {command.Id}, Name = {command.Name}, Message = {command.Message}"); } }
这里首先连接到我的RabbitMQ服务,然后向指定的Queue发送消息(这里是一个Client类型的实例对象)。
(3)编写Receiver
public class Program { public static void Main(string[] args) { Console.Title = "MassTransit Server"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); cfg.ReceiveEndpoint(host, "Qka.MassTransitTest", e=> { e.Consumer<TestConsumerClient>(); e.Consumer<TestConsumerAgent>(); }); }); bus.Start(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); bus.Stop(); } }
对于Receiver,要做的事就只有两件:一是连接到RabbitMQ,二是告诉RabbitMQ我要接收哪个消息队列的什么类型的消息。下面是TestConsumerClient和TestConsumerAgent的定义:
public class TestConsumerClient : IConsumer<Client> { public async Task Consume(ConsumeContext<Client> context) { Console.ForegroundColor = ConsoleColor.Red; await Console.Out.WriteLineAsync($"Receive message: {context.Message.Id}, {context.Message.Name}, {context.Message.Birthdate.ToString()}"); Console.ResetColor(); } } public class Client { public int Id { get; set; } public string Name { get; set; } public DateTime Birthdate { get; set; } public string Message { get; set; } } public class TestConsumerAgent : IConsumer<Agent> { public async Task Consume(ConsumeContext<Agent> context) { Console.ForegroundColor = ConsoleColor.Red; await Console.Out.WriteLineAsync($"Receive message: {context.Message.AgentCode}, {context.Message.AgentName}, {context.Message.AgentRole}"); Console.ResetColor(); } } public class Agent { public int AgentCode { get; set; } public string AgentName { get; set; } public string AgentRole { get; set; } public string Message { get; set; } }
(4)测试一下:
3.2 最简单的发布/订阅实例
除了简单的发送/接收模式外,我们用的更多的是发布/订阅这种模式。
(1)准备下图所示的类库和控制台项目,并对除Messages类库之外的其他项目安装MassTransit以及MassTransit.RabbitMQ。
(2)Messages类库:准备需要传输的Message
public class TestBaseMessage { public string Name { get; set; } public DateTime Time { get; set; } public string Message { get; set; } } public class TestCustomMessage { public string Name { get; set; } public DateTime Time { get; set; } public int Age { get; set; } public string Message { get; set; } }
(3)Publisher:接收我的消息吧骚年们
public class Program { public static void Main(string[] args) { Console.Title = "MassTransit Publisher"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST/"), hst => { hst.Username("admin"); hst.Password("edison"); }); }); do { Console.WriteLine("Please enter your message, if want to exit please press q."); string message = Console.ReadLine(); if (message.ToLower().Equals("q")) { break; } bus.Publish(new TestBaseMessage() { Name = "Edison Zhou", Time = DateTime.Now, Message = message }); bus.Publish(new TestCustomMessage() { Name = "Leo Dai", Age = 27, Time = DateTime.Now, Message = message }); } while (true); bus.Stop(); }
这里向RabbitMQ发布了两个不同类型的消息(TestBaseMessage和TestCustomMessage)
(4)SubscriberA:我只接收TestBaseMessage类型的消息,其他的我不要
public class Program { public static void Main(string[] args) { Console.Title = "MassTransit SubscriberA"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); cfg.ReceiveEndpoint(host, "Qka.MassTransitTestv2.CA", e => { e.Consumer<ConsumerA>(); }); }); bus.Start(); Console.ReadKey(); // press Enter to Stop bus.Stop(); } }
这里ConsumerA的定义如下:可以看出,它实现了一个泛型接口IConsumer,然后指定了TestBaseMessage为消费的消息类型。
public class ConsumerA : IConsumer<TestBaseMessage> { public async Task Consume(ConsumeContext<TestBaseMessage> context) { Console.ForegroundColor = ConsoleColor.Red; await Console.Out.WriteLineAsync($"SubscriberA => ConsumerA received message : {context.Message.Name}, {context.Message.Time}, {context.Message.Message}, Type:{context.Message.GetType()}"); Console.ResetColor(); } }
(5)SubscriberA:我只接收TestCustomMessage类型的消息,其他的我不要
public class Program { public static void Main(string[] args) { Console.Title = "MassTransit SubscriberB"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); cfg.ReceiveEndpoint(host, "Qka.MassTransitTestv2.CB", e => { e.Consumer<ConsumerA>(); }); }); bus.Start(); Console.ReadKey(); // press Enter to Stop bus.Stop(); } }
这里的ConsumerA的定义如下;它实现的接口是IConsumer<TestCustomMessage>
public class ConsumerA : IConsumer<TestCustomMessage> { public async Task Consume(ConsumeContext<TestCustomMessage> context) { Console.ForegroundColor = ConsoleColor.Red; await Console.Out.WriteLineAsync($"SubscriberB => ConsumerA received message : {context.Message.Name}, {context.Message.Time}, {context.Message.Message}, Age: {context.Message.Age}, Type:{context.Message.GetType()}"); Console.ResetColor(); } }
(6)测试一下:由于Publisher发送了两个不同类型的消息,两个Subscriber均只接受其中的一个类型的消息。
3.3 带返回状态消息的示例
之前的例子都是发布之后,不管订阅者有没有收到以及收到后有没有处理成功(即有没有返回消息,类似于HTTP请求和响应),在MassTransit中提供了这样的一种模式,并且还可以结合GreenPipes的一些扩展方法实现重试、限流以及熔断机制。这一部分详见官方文档:http://masstransit-project.com/MassTransit/usage/request-response.html
(1)准备下图所示的三个项目:通过NuGet安装MassTransit以及MassTransit.RabbitMQ
(2)Messages:准备请求和响应的消息传输类型
public interface IRequestMessage { int MessageId { get; set; } string Content { get; set; } } public class RequestMessage : IRequestMessage { public int MessageId { get; set; } public string Content { get; set; } public int RequestId { get; set; } } public interface IResponseMessage { int MessageCode { get; set; } string Content { get; set; } } public class ResponseMessage : IResponseMessage { public int MessageCode { get; set; } public string Content { get; set; } public int RequestId { get; set; } }
(3)Sender 请求发送端
public class Program { public static void Main(string[] args) { Console.Title = "Masstransit Request Side"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); // Retry : 重试 cfg.UseRetry(ret => { ret.Interval(3, 10); // 消费失败后重试3次,每次间隔10s }); // RateLimit : 限流 cfg.UseRateLimit(1000, TimeSpan.FromMinutes(1)); // 1分钟以内最多1000次调用访问 // CircuitBreaker : 熔断 cfg.UseCircuitBreaker(cb => { cb.TrackingPeriod = TimeSpan.FromMinutes(1); cb.TripThreshold = 15; // 当失败的比例至少达到15%才会启动熔断 cb.ActiveThreshold = 10; // 当失败次数至少达到10次才会启动熔断 cb.ResetInterval = TimeSpan.FromMinutes(5); }); // 当在1分钟内消费失败率达到15%或调用了10次还是失败时,暂停5分钟的服务访问 }); bus.Start(); SendMessage(bus); bus.Stop(); } private static void SendMessage(IBusControl bus) { var mqAddress = new Uri($"rabbitmq://192.168.80.71/EDCVHOST/Qka.MassTransitTestv3"); var client = bus.CreateRequestClient<IRequestMessage, IResponseMessage>(mqAddress, TimeSpan.FromHours(10)); // 创建请求客户端,10s之内木有回馈则认为是超时(Timeout) do { Console.WriteLine("Press q to exit if you want."); string value = Console.ReadLine(); if (value.ToLower().Equals("q")) { break; } Task.Run(async () => { var request = new RequestMessage() { MessageId = 10001, Content = value, RequestId = 10001 }; var response = await client.Request(request); Console.WriteLine($"Request => MessageId={request.MessageId}, Content={request.Content}"); Console.WriteLine($"Response => MessageCode={response.MessageCode}, Content={response.Content}"); }).Wait(); } while (true); } }
这里不再解释,请看注释。
(4)Receiver 接收端
public class Program { public static void Main(string[] args) { Console.Title = "MassTransit Response Side"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); cfg.ReceiveEndpoint(host, "Qka.MassTransitTestv3", e => { e.Consumer<RequestConsumer>(); }); }); bus.Start(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); bus.Stop(); } }
其中,RequestConsumer的定义如下:
public class RequestConsumer : IConsumer<IRequestMessage> { public async Task Consume(ConsumeContext<IRequestMessage> context) { Console.ForegroundColor = ConsoleColor.Red; await Console.Out.WriteLineAsync($"Received message: Id={context.Message.MessageId}, Content={context.Message.Content}"); Console.ResetColor(); var response = new ResponseMessage { MessageCode = 200, Content = $"Success", RequestId = context.Message.MessageId }; Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"Response message: Code={response.MessageCode}, Content={response.Content}, RequestId={response.RequestId}"); Console.ResetColor(); await context.RespondAsync(response); } }
(5)测试一下:
可以看出,请求调用方收到了来自接收方返回的状态消息,我们可以借助返回值去check一些状态。这里不再演示发生异常从而启用重试、熔断等的示例,有兴趣的园友可以自行测试。
3.4 Observer模式的发布/订阅示例
在某些场景中,我们需要针对一个消息进行类似于AoP(面向切面编程)或者监控的操作,比如在发送消息之前和结束后记日志等操作,我们可以借助MassTransit中的Observer模式来实现。(在MassTransit的消息接收中,可以通过两种模式来实现:一种是基于实现IConsumer接口,另一种就是基于实现IObserver接口)关于这一部分,详见官方文档:http://masstransit-project.com/MassTransit/usage/observers.html
(1)准备以下图所示的项目:
(2)Messages:定义要使用的Consumer和Observer
Consumer:
public class TestMessageConsumer : IConsumer<TestMessage> { public async Task Consume(ConsumeContext<TestMessage> context) { Console.ForegroundColor = ConsoleColor.Red; await Console.Out.WriteLineAsync($"TestMessageConsumer => Type:{context.Message.GetType()}, ID:{context.Message.MessageId}, Content:{context.Message.Content}"); Console.ResetColor(); } }
Observer:
public class PublishObserver : IPublishObserver { public Task PrePublish<T>(PublishContext<T> context) where T : class { Console.WriteLine("------------------PrePublish--------------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("----------------------------------------------------"); return Task.CompletedTask; } public Task PostPublish<T>(PublishContext<T> context) where T : class { Console.WriteLine("------------------PostPublish--------------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("----------------------------------------------------"); return Task.CompletedTask; } public Task PublishFault<T>(PublishContext<T> context, Exception exception) where T : class { Console.WriteLine("------------------PublishFault--------------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("------------------------------------------------------"); return Task.CompletedTask; } } public class SendObserver : ISendObserver { public Task PreSend<T>(SendContext<T> context) where T : class { Console.WriteLine("------------------PreSend--------------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("-------------------------------------------------"); return Task.CompletedTask; } public Task PostSend<T>(SendContext<T> context) where T : class { Console.WriteLine("------------------PostSend-------------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("-------------------------------------------------"); return Task.CompletedTask; } public Task SendFault<T>(SendContext<T> context, Exception exception) where T : class { Console.WriteLine("------------------SendFault-----------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("-------------------------------------------------"); return Task.CompletedTask; } } public class ReceiveObserver : IReceiveObserver { public Task PreReceive(ReceiveContext context) { Console.WriteLine("------------------PreReceive--------------------"); Console.WriteLine(Encoding.Default.GetString(context.GetBody())); Console.WriteLine("--------------------------------------"); return Task.CompletedTask; } public Task PostReceive(ReceiveContext context) { Console.WriteLine("------------------PostReceive--------------------"); Console.WriteLine(Encoding.Default.GetString(context.GetBody())); Console.WriteLine("------------------------------------------------------"); return Task.CompletedTask; } public Task ReceiveFault(ReceiveContext context, Exception exception) { Console.WriteLine("------------------ReceiveFault--------------------"); Console.WriteLine(Encoding.Default.GetString(context.GetBody())); Console.WriteLine("-------------------------------------------------------"); return Task.CompletedTask; } public Task PostConsume<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType) where T : class { Console.WriteLine("------------------PostConsume--------------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("--------------------------------------------------------"); return Task.CompletedTask; } public Task ConsumeFault<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType, Exception exception) where T : class { Console.WriteLine("------------------ConsumeFault-------------------"); var message = context.Message as TestMessage; Console.WriteLine($"ID={message.MessageId}, Content={message.Content},Time={message.Time}"); Console.WriteLine("--------------------------------------------------------"); return Task.CompletedTask; } }
(3)ObserverPublish
public class Program { public static void Main(string[] args) { Console.Title = "Masstransit Observer Publisher"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); }); var observer1 = new SendObserver(); var handle1 = bus.ConnectSendObserver(observer1); var observer2 = new PublishObserver(); var handle2 = bus.ConnectPublishObserver(observer2); bus.Start(); do { Console.WriteLine("Presss q if you want to exit this program."); string message = Console.ReadLine(); if (message.ToLower().Equals("q")) { break; } bus.Publish(new TestMessage { MessageId = 10001, Content = message, Time = DateTime.Now }); } while (true); bus.Stop(); } }
可以看到,这里我们使用了两个用于发送消息的Observer:SendObserver和PublishObserver
(3)ObserverReceive
public class Program { public static void Main(string[] args) { Console.Title = "Masstransit Observer Receiver"; var bus = Bus.Factory.CreateUsingRabbitMq(cfg => { var host = cfg.Host(new Uri("rabbitmq://192.168.80.71/EDCVHOST"), hst => { hst.Username("admin"); hst.Password("edison"); }); cfg.ReceiveEndpoint(host, "Qka.MassTransitTestv4", e => { e.Consumer<TestMessageConsumer>(); }); }); var observer = new ReceiveObserver(); var handle = bus.ConnectReceiveObserver(observer); bus.Start(); Console.ReadKey(); bus.Stop(); } }
(4)测试一下:
由此看出,我们可以借助Observer这种方式来截取到消息的一个lifecycle,用以进行不同阶段的业务逻辑操作。
四、小结
本篇极简的介绍了一下数据一致性和MassTransit这个开源的组件,通过几个例子介绍了在.NET环境下如何使用MassTransit操作RabbitMQ实现消息的接收/发送以及发布/订阅。后续我会继续使用MassTransit结合Quartz.Net和Polly在ASP.NET Core环境下实现一个简单的基于补偿机制的数据一致性的小案例,希望到时也可以和各位园友分享。
示例代码
Click Here => 点我下载
参考资料
(1)桂素伟,《基于.NET Core的微服务》
(2)richieyangs(张阳),《如何优雅的使用RabbitMQ》,《使用Masstransit开发基于消息传递的分布式应用》
(3)青客宝团队,《MassTransit&Sagas分布式服务开发ppt分享》
(4)成天,《MassTransit实现应用程序间的交互》
(5)娃娃都会打酱油了,《MassTransit学习记录》
(6)MassTransit 官方文档,http://masstransit-project.com/MassTransit/