RabbitMQ .NET Core 分布式事务
使用 .NET 5 + RabbitMQ 实现一个分布式事务,并保证最终一致性
流程为:
减库存 -> 减余额 -> 创建订单
RabbitMQ 中创建六个队列:
减库存队列、减库存死信队列
减余额队列、减余额死信队列
创建订单队列、创建订单死信队列
一个 WebAPI 用来发起流程
四个控制台,三个用来消费各自队列中的消息,一个对各种的错误进行协调
消息的可靠性
生产者确认
// 开启生产者确认 Channel.ConfirmSelect(); // 发送消息 Channel.BasicPublish(exchange, routingKey, props, body); // 生产者确认 bool isSendMsgOk = Channel.WaitForConfirms(); // 进行消息重发 for (int i = 0; i < repeat && !isSendMsgOk; i++) { Channel.BasicPublish(exchange, routingKey, props, body); isSendMsgOk = Channel.WaitForConfirms(); }
消费者确认
client.Channel.BasicAck(ea.DeliveryTag, false);
发送消息的同时把消息持久化到数据库中,并记录当前状态,到下一个环节的时候修改该消息的状态。
消费者异常
这里就涉及到,异常后消息重新投递的问题了。
如果 NACK 那么这个消息会回到队列的最上面,然后消费者在进行消费,这时候就遇见一个问题,不知道这个消息 Retry 了几次,因此需要一个中间介子记录一下这个消息 Retry 的次数。如果超过了一个阈值就XXX处理。
我这里使用的是死信队列进行处理的,消费者异常后直接 Nack ,并把 Request 设置成 false,该消息就会进入到对应的死信队列中。然后又一个调度者,订阅死信队列,把消息重新投递到队列中,并控制重试次数,如果超过阈值就XXX处理。
消息的顺序
因为整个流程都是同步进行的所以不存在顺序问题
重复消费
生产者保证一定能把消息发送出去就行了
消费者需要保证业务代码必须幂等。执行 SQL 的时候使用 if else 判断一下数据是否已经存在,如果不存在就执行相关的 SQL。(减库存、减余额的时候 Where 库存 >= 扣减库存)
生产者
RabbitMQClient _mQClient; public OrderController(RabbitMQClient mQClient) { _mQClient = mQClient; } [HttpPost] public OrderDto CreateOrder(OrderDto dto) { _mQClient.Publish(dto, "DeductStock_Exchange", "", true); return dto; }
Startup
services.AddSingleton(typeof(RabbitMQClient));
RabbitMQClient
public class RabbitMQClient { private readonly IConfiguration _configuration; private bool IsEvent = true; private IModel _channel; public RabbitMQClient(IConfiguration configuration) { _configuration = configuration; ConnectionFactory factory = new ConnectionFactory { UserName = _configuration["RabbitmqConfig:Username"], Password = _configuration["RabbitmqConfig:Password"], HostName = _configuration["RabbitmqConfig:Host"], VirtualHost = _configuration["RabbitmqConfig:VirtualHost"], AutomaticRecoveryEnabled = true, //网络故障自动连接恢复 }; Connection = factory.CreateConnection(); } public IConnection Connection { get; } public IModel Channel { get { if (_channel == null || !_channel.IsOpen) { _channel = Connection.CreateModel(); } return _channel; } } public void Publish<T>(T message, string exchange, string routingKey, bool isConfirm = true, int repeat = 5) { if (isConfirm) { Channel.ConfirmSelect(); } var sendBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message)); PublishMessage(sendBytes, exchange, routingKey, isConfirm); if (isConfirm) { bool isSendMsgOk = Channel.WaitForConfirms(); for (int i = 0; i < repeat && !isSendMsgOk; i++) { // 进行消息重发 PublishMessage(sendBytes, exchange, routingKey, isConfirm); isSendMsgOk = Channel.WaitForConfirms(); } } } private void PublishMessage(ReadOnlyMemory<byte> body, string exchange, string routingKey, bool isConfirm = true) { IBasicProperties props = Channel.CreateBasicProperties(); props.MessageId = Guid.NewGuid().ToString(); Channel.BasicPublish(exchange, routingKey, props, body); } } }
消费者+生产者
订阅减库存的队列,消费成功后向减余额的队列中发送消息
static void Main(string[] args) { Console.Title = "减库存"; var build = new HostBuilder(); build.ConfigureServices((hostContext, services) => { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); RabbitMQClient client = new RabbitMQClient(configuration); string exchangeName = "DeductStock_Exchange"; string queueName = "DeductStock_Queue"; string routingKey = "DeductStock_Routing"; string dead_ExchangeName = "DeductStock_Exchange_dead"; string dead_QueueName = "DeductStock_Queue_dead"; string dead_RoutingKey = "DeductStock_Routing_dead"; client.Channel.ExchangeDeclare(dead_ExchangeName, type: "fanout", durable: true, autoDelete: false); client.Channel.QueueDeclare(dead_QueueName, durable: true, exclusive: false, autoDelete: false); client.Channel.QueueBind(dead_QueueName, dead_ExchangeName, dead_RoutingKey); client.Channel.ExchangeDeclare(exchangeName, type: "fanout", durable: true, autoDelete: false); client.Channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false, arguments: new Dictionary<string, object> { { "x-dead-letter-exchange", dead_ExchangeName }, { "x-dead-letter-routing-key", dead_RoutingKey }, { "x-message-ttl", 10000 } }); client.Channel.QueueBind(queueName, exchangeName, routingKey); //事件基本消费者 EventingBasicConsumer consumer = new EventingBasicConsumer(client.Channel); //接收到消息事件 consumer.Received += (ch, ea) => { try { var message = Encoding.UTF8.GetString(ea.Body.ToArray()); Console.WriteLine($"收到消息: { message }"); OrderDto dto = JsonConvert.DeserializeObject<OrderDto>(message); if (dto.Id % 10 == 1 && !dto.IsBug) { throw new Exception(); } //确认该消息已被消费,确认完成后 RabbitMQ 会删除该消息 client.Channel.BasicAck(ea.DeliveryTag, false); client.Publish(dto, "DeductBalance_Exchange", "", true); } catch (Exception ex) { Console.WriteLine("库存异常"); client.Channel.BasicNack(ea.DeliveryTag, false, false); } }; client.Channel.BasicConsume(queueName, false, consumer); Console.WriteLine("库存消费者"); }).Build().Run(); } }
减余额的和创建订单的基本一样
最后一个调度
订阅死信队列,然后把死信队列中的消息重新投递到队列中,让队列继续处理相关的业务
如果达到阈值,或者出现什么问题把消息持久化到硬盘上面
{ var model = new DispatchModel("", "DeductStock_Exchange", "DeductStock_Queue_dead", Directory.GetCurrentDirectory() + "\\DeductStock.txt"); var channel = Connection.CreateModel(); EventingBasicConsumer consumer = new EventingBasicConsumer(channel); consumer.Received += (ch, ea) => { var message = Encoding.UTF8.GetString(ea.Body.ToArray()); Console.WriteLine($"收到消息: { message }"); OrderDto dto = JsonConvert.DeserializeObject<OrderDto>(message); try { channel.BasicAck(ea.DeliveryTag, false); if (dto.RetryCount >= 5) { File.WriteAllText(model.messageFilePath, message); return; } dto.RetryCount += 1; // 开启发送确认 channel.ConfirmSelect(); var sendBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(dto)); channel.BasicPublish(model.exchangeName, model.routingKey, null, sendBytes); bool isSendMsgOk = channel.WaitForConfirms(); for (int i = 0; i < 5 && !isSendMsgOk; i++) { // 进行消息重发 channel.BasicPublish(model.exchangeName, model.routingKey, null, sendBytes); isSendMsgOk = channel.WaitForConfirms(); } if (!isSendMsgOk) { /// 发送六次都没有成功 /// 消息缓存到本地 File.WriteAllText(model.messageFilePath, message); } } catch (Exception ex) { // 确认消费 channel.BasicAck(ea.DeliveryTag, false); // 消息缓存到本地 File.WriteAllText(model.messageFilePath, message); } }; //启动消费者 设置为手动应答消息 channel.BasicConsume(model.dead_queueName, false, consumer); }