微服務簡單實現最終一致性

星仔007發表於2022-04-05

有花時間去研究masstransit的saga,英文水平不過關,始終無法實現上手他的程式碼編排的業務,遺憾。

本文通過rabbit和sqlserver實現下單,更新庫存,更新產品,模擬資料最終一致性。

 

專案結構如下,reportService可有可無,這裡就相當一個鏈條,只要兩節走通了後面可以接龍,本文有用到不省略。流程:orderservice=>eComm=>reportservice 。

 

下面先看看order的配置,通過控制器新增訂單同時釋出訂單資訊到order_exchange交換機,Key是"order.created,這樣就把訂單推送到了佇列,等到庫存服務獲取訂單去更新庫存。

  // POST api/<OrderController>
        [HttpPost]
        public async Task Post([FromBody] OrderDetail orderDetail)
        {
            var id = await orderCreator.Create(orderDetail);
            publisher.Publish(JsonConvert.SerializeObject(new OrderRequest { 
                OrderId = id,
                ProductId = orderDetail.ProductId,
                Quantity = orderDetail.Quantity


            }), "order.created", null);
        } 

更新庫存的程式碼,然後再傳送訊息告訴order服務,這裡有哪個try包裹,如果這裡有失敗會觸發catch,傳送減庫存失敗的訊息。order服務消費到這條訊息就會執行相應的刪除訂單操作。程式碼如下:

using Ecomm.DataAccess;
using Ecomm.Models;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Plain.RabbitMQ;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Ecomm
{
    public class OrderCreatedListener : IHostedService
    {
        private readonly IPublisher publisher;
        private readonly ISubscriber subscriber;
        private readonly IInventoryUpdator inventoryUpdator;

        public OrderCreatedListener(IPublisher publisher, ISubscriber subscriber, IInventoryUpdator inventoryUpdator)
        {
            this.publisher = publisher;
            this.subscriber = subscriber;
            this.inventoryUpdator = inventoryUpdator;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            subscriber.Subscribe(Subscribe);
            return Task.CompletedTask;
        }

        private bool Subscribe(string message, IDictionary<string, object> header)
        {
            var response = JsonConvert.DeserializeObject<OrderRequest>(message);
            try
            {
                inventoryUpdator.Update(response.ProductId, response.Quantity).GetAwaiter().GetResult();
                publisher.Publish(JsonConvert.SerializeObject(
                    new InventoryResponse { OrderId = response.OrderId, IsSuccess = true }
                    ), "inventory.response", null);
            }
            catch (Exception)
            {
                publisher.Publish(JsonConvert.SerializeObject(
                    new InventoryResponse { OrderId = response.OrderId, IsSuccess = false }
                    ), "inventory.response", null);
            }

            return true;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

  

using Ecomm.Models;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Plain.RabbitMQ;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OrderService
{
    public class InventoryResponseListener : IHostedService
    {
        private readonly ISubscriber subscriber;
        private readonly IOrderDeletor orderDeletor;

        public InventoryResponseListener(ISubscriber subscriber, IOrderDeletor orderDeletor)
        {
            this.subscriber = subscriber;
            this.orderDeletor = orderDeletor;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            subscriber.Subscribe(Subscribe);
            return Task.CompletedTask;
        }

        private bool Subscribe(string message, IDictionary<string, object> header)
        {
            var response = JsonConvert.DeserializeObject<InventoryResponse>(message);
            if (!response.IsSuccess)
            {
                orderDeletor.Delete(response.OrderId).GetAwaiter().GetResult();
            }
            return true;
        } 

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

  上面的程式碼是整個服務的核心業務,也很簡單就是佇列相互通訊相互確認操作是否順利,失敗就執行迴歸操作,而這裡我們都會寫好對應補償程式碼:

using Dapper;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;

namespace OrderService
{
    public class OrderDeletor : IOrderDeletor
    {
        private readonly string connectionString;

        public OrderDeletor(string connectionString)
        {
            this.connectionString = connectionString;
        }

        public async Task Delete(int orderId)
        {
            using var connection = new SqlConnection(connectionString);
            connection.Open();
            using var transaction = connection.BeginTransaction();
            try
            {
                await connection.ExecuteAsync("DELETE FROM OrderDetail WHERE OrderId = @orderId", new { orderId }, transaction: transaction);
                await connection.ExecuteAsync("DELETE FROM [Order] WHERE Id = @orderId", new { orderId }, transaction: transaction);
                transaction.Commit();
            }
            catch
            {
                transaction.Rollback();
            }
        }
    }
}

庫存服務裡有釋出產品的介面,這裡沒有做過多的處理,只是把產品新增放到佇列,供後面的ReportService服務獲取,該服務拿到後會執行產品數量扣除:

using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Plain.RabbitMQ;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace ReportService
{
    public class ReportDataCollector : IHostedService
    {
        private const int DEFAULT_QUANTITY = 100;
        private readonly ISubscriber subscriber;
        private readonly IMemoryReportStorage memoryReportStorage;

        public ReportDataCollector(ISubscriber subscriber, IMemoryReportStorage memoryReportStorage)
        {
            this.subscriber = subscriber;
            this.memoryReportStorage = memoryReportStorage;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            subscriber.Subscribe(Subscribe);
            return Task.CompletedTask;
        }
        private bool Subscribe(string message, IDictionary<string, object> header)
        //private bool ProcessMessage(string message, IDictionary<string, object> header)
        {
            if (message.Contains("Product"))
            {
                var product = JsonConvert.DeserializeObject<Product>(message);
                if (memoryReportStorage.Get().Any(r => r.ProductName == product.ProductName))
                {
                    return true;
                }
                else
                {
                    memoryReportStorage.Add(new Report
                    {
                        ProductName = product.ProductName,
                        Count = DEFAULT_QUANTITY
                    });
                }
            }
            else
            {
                var order = JsonConvert.DeserializeObject<Order>(message);
                if(memoryReportStorage.Get().Any(r => r.ProductName == order.Name))
                {
                    memoryReportStorage.Get().First(r => r.ProductName == order.Name).Count -= order.Quantity;
                }
                else
                {
                    memoryReportStorage.Add(new Report
                    {
                        ProductName = order.Name,
                        Count = DEFAULT_QUANTITY - order.Quantity
                    });
                }
            }
            return true;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
    }
}

到這裡整個流程大概如此。只要理清楚了訂單和庫存更新這裡的業務,後面差不多一樣,可以無限遞迴。程式碼文末有連結供下載。

這裡有一個地方的程式碼如下,新增庫存的時候同時釋出訊息。假如新增完訂單後面崩掉了,這裡是個原子操作最佳。

        [HttpPost]
        public async Task Post([FromBody] OrderDetail orderDetail)
        {
            var id = await orderCreator.Create(orderDetail);
            publisher.Publish(JsonConvert.SerializeObject(new OrderRequest { 
                OrderId = id,
                ProductId = orderDetail.ProductId,
                Quantity = orderDetail.Quantity


            }), "order.created", null);
        }

  很遺憾masstransit的saga還沒有整明白,那就上cap,完成業務一致性。加了點cap程式碼因為之前是dapper,所以加了dbcontext和cap相關程式碼有點小亂。核心程式碼如下:

 

using DotNetCore.CAP;
using MediatR;
using OrderService.Command;
using System.Threading;
using Ecomm.Models;
using System.Collections.Generic;

namespace OrderService.Handler
{
    public class InsertOrderDetailHandler : IRequestHandler<InsertOrderDetailCommand, InsertOrderDetailModel>
    {
        private readonly OrderDbContext context;
        private readonly ICapPublisher cap;
        public InsertOrderDetailHandler(OrderDbContext context, ICapPublisher cap)
        {
            this.context = context;
            this.cap = cap;
        }
        public async System.Threading.Tasks.Task<InsertOrderDetailModel> Handle(InsertOrderDetailCommand request, CancellationToken cancellationToken)
        {
            using(var trans =context.Database.BeginTransaction(cap))
            {
                var order =  context.Orders.Add(new Order
                {
                    UpdatedTime = System.DateTime.Today,
                    UserId = request.UserId,
                    UserName = request.UserName
                });
                var orderDetail = context.OrderDetails.Add(new OrderDetail
                {
                    OrderId = order.Entity.Id,
                    ProductId = request.ProductId,
                    Quantity = request.Quantity,
                    ProductName = request.ProductName,
                });
                 context.SaveChanges();
                
                cap.Publish<OrderRequest>("order.created", new OrderRequest
                {
                    OrderId = order.Entity.Id,
                    ProductId = orderDetail.Entity.ProductId,
                    Quantity = orderDetail.Entity.Quantity
                }, new Dictionary<string,string>()) ;
                 trans.Commit();
                return new InsertOrderDetailModel { OrderDetailid = orderDetail.Entity.Id, OrderId = order.Entity.Id, Success = true };
            }
        }
    }
}

 到這裡差不多要結束了,這裡的程式碼都可以除錯執行的。因為加了cap,order服務有兩套rabbitmq的配置,有冗餘,而且有點坑。除錯的時候注意,Plain.RabbitMQ支援的交換機不是持久化的,而cap是持久化的,所以有點不相容。第一次執行可以先確保Plain.RabbitMQ正常,再刪掉交換機,cap跑起來了再建持久化交換機,這樣cap訊息就會被rabbitmq接收,後面就會被庫存服務消費。因為我這裡cap不會自動繫結佇列,Plain.RabbitMQ是可以的。所以需要新建交換機後再繫結佇列。而且這裡佇列以Plain.RabbitMQ生成的名字來繫結。要不然又可能會除錯踩坑無法出坑。 用cap不注意你連訊息佇列都看不到,看到了佇列也看不到消費資料,這點不知道是我不會還是cap有什麼難的配置。結束。。。

上例專案demo:

liuzhixin405/SimpleOrders_Next (github.com)

超簡單微服務demo

liuzhixin405/SimpleOrders (github.com)

相關文章