• 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);
    }
  • 相关阅读:
    js判断是移动端还是PC端
    如何删除mysql注释
    Javascript库的产生和解读
    zeptojs库解读3之ajax模块
    zeptojs库解读2之事件模块
    zeptojs库解读1之整体框架
    发起图片请求的几种可能性(webkit内核)
    让zend studio 支持 redis函数自动提示
    4种常见的MySQL日志类型
    redis 安装
  • 原文地址:https://www.cnblogs.com/ansheng/p/16103755.html
Copyright © 2020-2023  润新知