Dotnet微服务:使用cap实现分布式服务的数据一致性

DotNetCore.CAP是一个在分布式系统中(SOA,MicroService)实现事件总线及最终一致性(分布式事务)的一个开源的 C# 库,具有轻量级,高性能,易使用等特点。开源地址

Cap(Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性))是分布式系统中的一个重要理念,根据CAP定理,存在网络分区(微服务即时网络分区架构)时,Web应用不可能同时满足可用性和一致性,DotNetCore.CAP使用“异步确保”方案,利用消息队列和本地消息列表实现最终数据一致性。异步确保模式是补偿模式的一个典型案例,通过异步的方式进行处理,处理后把结果通过通知系统通知给使用方。

一,准备内容:

运输器:过运输将数据从一个地方移动到另一个地方-在采集程序和管道之间,管道与实体数据库之间,甚至在管道与外部系统之间。DotNetCore.CAP 支持以下几种运输方式:RabbitMQ,Kafka,Azure Service bus,Amazon SQS,In-memory queue。我使用的是RabbitMq。

二,DotnetCore webapi项目集成DotNetCore.CAP

 public void ConfigureServices(IServiceCollection services)
        {
            //获取数据库连接字符串
            var connection = Configuration.GetConnectionString("MySql");
            //Cap
            services.AddCap(conf => {
                //配置数据库上下文
                conf.UseEntityFramework<Entity.MicoDatacontext>();
                //配置数据库连接
                conf.UseMySql(connection);
                //使用RabbitMQ运输器
                conf.UseRabbitMQ(rab => {
                    rab.HostName = "192.168.137.2";
                    rab.Password = "xxxxxx";
                    rab.Port = 5672;
                    rab.UserName = "xxxx";
                });
            });
            services.AddDbContext<Entity.MicoDatacontext>(options =>
            {
                options.UseMySql(connection);
            });
            services.AddControllers();
        }

三,在Contoller中使用CAP发送消息

比如下面是一个模拟创建订单的接口,创建完订单后发送一个主题为“order.create”的消息到消息总线。这个接口开启了一个需要手动提交的本地事务,插入本地消息和处理业务逻辑都在这个事务内,确保业务完成的同时消息能发送出去。

 readonly Entity.MicoDatacontext dbcontext;
        readonly ICapPublisher capPublisher;
        public TestController(Entity.MicoDatacontext dbcontext, ICapPublisher capPublisher) {
            this.dbcontext = dbcontext;
            this.capPublisher = capPublisher;
        }
        [HttpGet("CreateOrder")]
        public async Task<bool> CreateOrderAsync(string name,string goodsSid)
        {
            Order order = new Order()
            {
                Sid = Guid.NewGuid().ToString(),
                CreateTime = DateTime.Now,
                GoodsSid = goodsSid,
                Name = name,
                Status = 0,
                UpdateTime = DateTime.Now
            };
            //开启一个需要手动提交的本地事务,插入本地消息和处理业务逻辑都在这个事务内。
            using (var tran = dbcontext.Database.BeginTransaction(capPublisher, false))
            {
                dbcontext.Order.Add(order);
                await capPublisher.PublishAsync("order.create", new OrderCreate() { GoodsSid = goodsSid, OrderName = name });
                dbcontext.SaveChanges();
                tran.Commit();
                return true;
            };
        } 

四,在Contoller中使用CAP接收消息 

 下面是模拟仓库服务在接收到订单创建消息后减库存的操作。如果仓库服务没有成功消费这条消息,DotnetCore.CAP将会启用重发机制。

[Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        readonly Entity.MicoDatacontext dbcontext;
        readonly ICapPublisher capPublisher;
        public TestController(ICapPublisher capPublisher, Entity.MicoDatacontext dbcontext)
        {
            this.dbcontext = dbcontext;
            this.capPublisher = capPublisher;
        }
        [NonAction]
        [CapSubscribe("order.create")]
        public Task OrderCreate(Common.Publish.OrderCreate order)
        {
            var storeage = dbcontext.Storeage.Where(r => r.Sid.Equals(order.GoodsSid)).SingleOrDefault();
            if (storeage != null) throw new Exception("库存不足");
            storeage.Count -= 1;
            storeage.UpdateTime = DateTime.Now;
            dbcontext.Storeage.Update(storeage);
            dbcontext.SaveChanges();
            return Task.CompletedTask;
        }
    }

五,消息重试配置

1、 发送重试

在消息发送过程中,当出现 Broker 宕机或者连接失败的情况亦或者出现异常的情况下,这个时候 CAP 会对发送的重试,第一次重试次数为 3,4分钟后以后每分钟重试一次,进行次数 +1,当总次数达到50次后,CAP将不对其进行重试。

你可以在 CapOptions 中设置FailedRetryCount来调整默认重试的总次数。

当失败总次数达到默认失败总次数后,就不会进行重试了,你可以在 Dashboard 中查看消息失败的原因,然后进行人工重试处理。

2、 消费重试

当 Consumer 接收到消息时,会执行消费者方法,在执行消费者方法出现异常时,会进行重试。这个重试策略和上面的 发送重试 是相同的

    //获取数据库连接字符串
            var connection = Configuration.GetConnectionString("MySql");
            //Cap
            services.AddCap(conf => {
                //配置数据库上下文
                conf.UseEntityFramework<Entity.MicoDatacontext>();
                //配置数据库连接
                conf.UseMySql(connection);
                //使用RabbitMQ运输器
                conf.UseRabbitMQ(rab => {
                    rab.HostName = "192.168.137.2";
                    rab.Password = "xxxx114";
                    rab.Port = 5672;
                    rab.UserName = "xxxx";
                });
                //消息重试的最大次数
                conf.FailedRetryCount = 50;
                //消息重试间隔时间,4min后该值设置生效(默认快速重试3次)
                conf.FailedRetryInterval = 60;
                //发送成功的消息的过期时间(过期则删除)
                conf.SucceedMessageExpiredAfter = 24 * 3600;
                //发送消息失败后的回调
                conf.FailedThresholdCallback = (context) =>
                {
                    //通知管理人员或其它逻辑
                };

 六,事务补偿

某些情况下,消费者需要返回值以告诉发布者执行结果,以便于发布者实施一些动作,通常情况下这属于补偿范围。可以在消费者执行的代码中通过重新发布一个新消息来通知上游,CAP 提供了一种简单的方式来做到这一点。 你可以在发送的时候指定 callbackName 来得到消费者的执行结果。

比如上面的示例,仓库消费后需要告诉订单服务处理结果。

订单服务:处理创建订单业务后发送一条带有补偿回调的消息并通过CapSubscribe接收该回调消息,处理订单状态

[Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        readonly Entity.MicoDatacontext dbcontext;
        readonly ICapPublisher capPublisher;
        public TestController(Entity.MicoDatacontext dbcontext, ICapPublisher capPublisher) {
            this.dbcontext = dbcontext;
            this.capPublisher = capPublisher;
        }
        [HttpGet("CreateOrder")]
        public async Task<bool> CreateOrderAsync(string name,string goodsSid)
        {
            Order order = new Order()
            {
                Sid = Guid.NewGuid().ToString(),
                CreateTime = DateTime.Now,
                GoodsSid = goodsSid,
                Name = name,
                Status = 0,
                UpdateTime = DateTime.Now
            };
            //开启一个需要手动提交的本地事务,插入本地消息和处理业务逻辑都在这个事务内。
            using (var tran = dbcontext.Database.BeginTransaction(capPublisher, false))
            {
                dbcontext.Order.Add(order);
                //发送一个带有补偿回调的消息
                await capPublisher.PublishAsync("order.create", new OrderCreate() { GoodsSid = goodsSid, OrderName = name },"Soreage.reduced");
                dbcontext.SaveChanges();
                tran.Commit();
                return true;
            };
        }
        /// <summary>
        /// 补偿消息处理
        /// </summary>
        /// <param name="order"></param>
        /// <returns></returns>
        [NonAction]
        [CapSubscribe("Soreage.reduced")]
        public Task OrderCreate(Common.Publish.StoreageReduced msg)
        {
            var order = this.dbcontext.Order.Find(msg.OrderSid);
            if (order != null)
            {
                order.Status = 1;
                dbcontext.SaveChanges();
            }
            return Task.CompletedTask;
        }
    }

  仓库服务:接收order.create主题消息并返回正确的返回值

 [NonAction]
        [CapSubscribe("order.create")]
        public Common.Publish.StoreageReduced OrderCreate(Common.Publish.OrderCreate order)
        {
            var storeage = dbcontext.Storeage.Where(r => r.Sid.Equals(order.GoodsSid)).SingleOrDefault();
            if (storeage != null) throw new Exception("库存不足");
            storeage.Count -= 1;
            storeage.UpdateTime = DateTime.Now;
            dbcontext.Storeage.Update(storeage);
            dbcontext.SaveChanges();
            return new Common.Publish.StoreageReduced() {OrderSid=order.OrderSid,IsSuccess=true };
        }

,并发冲突处理

使用EF的RowVersion做乐观锁解决并发冲突的问题。

Do实体添加Timestamp列

        [Timestamp]
        public byte[] Timespan { get; set; }

DbContext类可以做如下配置

protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Model.Order>().Property(r => r.Timespan).IsRowVersion();
            base.OnModelCreating(modelBuilder);
        }

  

posted @ 2020-10-20 15:13  坚持坚持  阅读(1577)  评论(0编辑  收藏  举报