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

浙公网安备 33010602011771号