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);
}

 

posted @ 2022-04-05 20:32  乔安生  阅读(228)  评论(0编辑  收藏  举报