建议:小规模分布式就用DTC,大规模分布式就用NetCore.CAP
小规模分布式就用DTC
强一致性模式,大规模分布式就用DotNetCore.CAP
框架
dotnetCore.CAP框架
[CAP]是一个用来解决微服务或者分布式系统中分布式事务问题的一个开源项目解决方案,同样可以用来作为EventBus使用。
1 github地址:https://github.com/dotnetcore/CAP
2 官网地址: https://cap.dotnetcore.xyz/
3 官网文档:https://cap.dotnetcore.xyz/user-guide/zh/cap/idempotence/
分布式的代价:
分布式环境下,服务器之间的通信,可能是不靠谱,这种情况无法避免分区容错,一定存在。CAP
是不能同时满足的!
Consistency
和 Availability
怎么选?一致性和可用性,不能同时满足,要什么?
CP重要,一致性最重要了,数据不能错,银行—交易数据
AP重要,可用性最重要了,系统的可用性,尤其是分布式----微服务,可用性尤为重要,没有可用性是跑不起来
CAP
1、Consistency
(一致性):意思是写操作之后的读操作,必须返回该值,不能出现一个不正确的结果。
a)强一致性:任意时刻数据都是一致性的。
b)弱一致性性:允许某一时刻不一致,承诺在一定时间内变成一致。
c)最终一致性:允许数据不一致,但是最终最终,数据还是的一致的。
2、Availability
(可用性):意思是只要收到用户的请求,服务器就必须给出回应。(分布式下面可用性是最重要的)
3、Partition tolerance
(分区容错):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition
)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
多种一致性:
1、强一致性—任意时刻数据都是一致的
a)、2PC(预备-》就绪-》提交):阻塞导致性能问题、 单点故障(事务管理器挂了)、消息丢失问题。=》DTC是基于数据库层面的事务 .NET Framework 下MSDTC
实现,Distributed Transaction Coordinator
服务必须开启(演示的是单机—局域网需要配置)。 .NET Core不支持—Linux
b)、3PC(预备-》就绪-》预提交-》提交):阻塞导致性能问题、 单点故障(事务管理器挂了)、消息丢失问题。只是增加了数据库自动提交。
2、弱一致性—允许某一时刻不一致,承诺在一定时间内变成一致的–Try-Confirm-Cancel。=》主要用在银行、阿里—钱必须保障-不能阻塞—设计负责,开发工作重ByteTCC、Himly、TCC-transaction
事务管理器—这些都是Java的 .NET没有—所以解决方案也没有—除非自己写
a)、通过TCC
实现弱一致性。
3、最终一致性—允许数据不一致,但是最终最终,数据还是得一致的 业务层面
a)、基于本地消息表实现最终一致性。.net领域首选采用DotNetCore.CAP
框架来实现基于本地消息表的分布式事务。
幂等性----重放攻击
对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的。(网页提交按钮,点1次 跟10次结果一样)。
场景:支付—页面修改信息—订单减库存
1 MVCC多版本并发控制—乐观锁—数据库更新时带上版本号—更新+1 条件必带version-----id + version
2 去重表—请求带个guid—操作前校验下guid—点赞—100赞-不能重复—文章id+用户id+唯一索引
3 Token
机制—每次操作都带个唯一id,请求来了先检测再执行
4 form
表单每次都带一个guid
或雪花算法id,然后将这个id存入redis并设置过期时间,每次提交表单的时候判断redis里面是否存在,如果存在就从redis移除,进行相应业务。如果不存在就不能进行相应业务操作。
本地消息表分布式事务
MQ分布式事务–本地消息表–基于消息的一致性
基于DotNetCore.CAP
框架搭建分布式集群事务过程详解—微服务
项目说明:
a)、此项目NuGet相关依赖包:DotNetCore.CAP、DotNetCore.CAP.RabbitMQ、DotNetCore.CAP.SqlServer、DotNetCore.CAP.MongoDB(需要 MongoDB 4.0+ 集群)、DotNetCore.CAP.Dashboard
b)、此项目分布式事务参与微服务节点:用户服务(Published
)-》订单服务(Received/Published
)-》仓储服务(Received/Published
)-》物流服务(Received/Published
)-》支付服务(Received
)。基于本地消息表模式将任务扭转到下一个服务节点,从而组成一个完整的业务流程。
c)、服务启动命令:
--consul.exe启动
首先cmd到consul.exe所在路径-》consul.exe agent --dev
--用户服务(Published)
dotnet run --urls=http://*:11111
--订单服务(Received/Published)
dotnet run --urls=http://*:11112
--仓储服务(Received/Published)
dotnet run --urls=http://*:11113
--物流服务(Received/Published)
dotnet run --urls=http://*:11114
--支付服务(Received)
dotnet run --urls=http://*:11115
CAP 使用阿里云 AMQP 服务
安装依赖
dotnet add package DotNetCore.CAP
dotnet add package DotNetCore.CAP.RabbitMQ
dotnet add package Tfc.CAP.AliyunAMQP
修改配置
appsettings.json
配置
"Cap": {
"RabbitMQ": {
"HostName": "", // amqp 访问地址
"UserName": "", // 阿里云 AccessKey
"Password": "" // 阿里云访问 AccessSecret
},
"Aliyun": {
"VirtualHost": "" // 阿里云AMQP创建
}
}
Startup
的ConfigureServices
添加如下配置
context.AddCap(capOptions =>
{
......
capOptions.UseRabbitMQ(_ =>
{
_.HostName = configuration["Cap:RabbitMQ:HostName"];
_.UserName = configuration["Cap:RabbitMQ:UserName"];
_.Password = configuration["Cap:RabbitMQ:Password"];
_.ConnectionFactoryOptions = options =>
{
options.VirtualHost = configuration["Cap:Aliyun:VirtualHost"];
options.AuthMechanisms = new List<IAuthMechanismFactory>() {new AliyunMechanismFactory()};
};
});
});
- 支持
Abp.EventBus.CAP
, 配置和上面保持一致
DotNetCore.CAP.Provider
DotNetCore.CAP
为其增加FreeSql
中的统一事务提交
Getting Started
NuGet
你可以运行以下下命令在你的项目中安装 CAP。
PM> Install-Package DotNetCore.CAP.MySql.Provider
安装FreeSql
PM> Install-Package FreeSql
PM> Install-Package FreeSql.Provider.MySqlConnector
Configuration
首先配置CAP到 Startup.cs
文件中,如下:
public IFreeSql Fsql { get; }
public IConfiguration Configuration { get; }
private string connectionString = @"Data Source=localhost;Port=3306;User ID=root;Password=123456;Initial Catalog=captest;Charset=utf8mb4;SslMode=none;Max pool size=10";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Fsql = new FreeSqlBuilder()
.UseConnectionString(DataType.MySql, connectionString)
.UseAutoSyncStructure(true)
.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IFreeSql>(Fsql);
services.AddCap(x =>
{
x.UseMySql(connectionString);
x.UseRabbitMQ("localhost");
x.UseDashboard();
x.FailedRetryCount = 5;
x.FailedThresholdCallback = (type, msg) =>
{
Console.WriteLine(
$@"A message of type {type} failed after executing {x.FailedRetryCount} several times, requiring manual troubleshooting. Message name: {msg.GetName()}");
};
});
services.AddControllers();
}
在控制器中得到
private readonly IFreeSql _freeSql;
private readonly ICapPublisher _capBus;
public TestController(IFreeSql freeSql, ICapPublisher capBus)
{
_freeSql = freeSql;
_capBus = capBus;
}
[HttpGet("~/freesql/transaction")]
public DateTime GetTime3()
{
DateTime now = DateTime.Now;
using (var uow = _freeSql.CreateUnitOfWork())
{
using ICapTransaction trans = uow.BeginTransaction(_capBus, false);
var repo = uow.GetRepository<WeatherForecast>();
repo.Insert(new WeatherForecast()
{
Date = now,
Summary = "summary",
TemperatureC = 100
});
repo.Insert(new WeatherForecast()
{
Date = now,
Summary = "11222",
TemperatureC = 200
});
_capBus.Publish("freesql.time", now);
trans.Commit();
}
return now;
}
[CapSubscribe("freesql.time")]
public void GetTime(DateTime time)
{
Console.WriteLine($"time:{time}");
}
DotNetCore.CAP.Provider
DotNetCore.CAP
为其增加FreeSql
中的统一事务提交,没有减少代码,如对EFCore
的依赖,如果需要去掉EFCore的依赖。
该项目分为二种方式实现CAP
配合FreeSql
的分布式事务一致性。
1.改源码,为其增加一些扩展,并在switch
的地方,加IUnitOfWork
的判断。
Getting Started
NuGet
你可以运行以下下命令在你的项目中安装 CAP。
PM> Install-Package DotNetCore.CAP.MySql.Provider
安装FreeSql
PM> Install-Package FreeSql
PM> Install-Package FreeSql.Provider.MySqlConnector
PM> Install-Package FreeSql.DbContext
Configuration
首先配置CAP到 Startup.cs
文件中,如下:
public IFreeSql Fsql { get; }
public IConfiguration Configuration { get; }
private string connectionString = @"Data Source=localhost;Port=3306;User ID=root;Password=123456;Initial Catalog=captest;Charset=utf8mb4;SslMode=none;Max pool size=10";
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Fsql = new FreeSqlBuilder()
.UseConnectionString(DataType.MySql, connectionString)
.UseAutoSyncStructure(true)
.Build();
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IFreeSql>(Fsql);
services.AddCap(x =>
{
x.UseMySql(connectionString);
x.UseRabbitMQ("localhost");
x.UseDashboard();
x.FailedRetryCount = 5;
x.FailedThresholdCallback = (type, msg) =>
{
Console.WriteLine(
$@"A message of type {type} failed after executing {x.FailedRetryCount} several times, requiring manual troubleshooting. Message name: {msg.GetName()}");
};
});
services.AddControllers();
}
在控制器中得到
private readonly IFreeSql _freeSql;
private readonly ICapPublisher _capBus;
public TestController(IFreeSql freeSql, ICapPublisher capBus)
{
_freeSql = freeSql;
_capBus = capBus;
}
[HttpGet("~/freesql/transaction")]
public DateTime GetTime3()
{
DateTime now = DateTime.Now;
using (var uow = _freeSql.CreateUnitOfWork())
{
using ICapTransaction trans = uow.BeginTransaction(_capBus, false);
var repo = uow.GetRepository<WeatherForecast>();
repo.Insert(new WeatherForecast()
{
Date = now,
Summary = "summary",
TemperatureC = 100
});
repo.Insert(new WeatherForecast()
{
Date = now,
Summary = "11222",
TemperatureC = 200
});
_capBus.Publish("freesql.time", now);
trans.Commit();
}
return now;
}
[CapSubscribe("freesql.time")]
public void GetTime(DateTime time)
{
Console.WriteLine($"time:{time}");
}
2. 不修改CAP源码怎么办呢?
另外,如果你不想修改CAP的源码,FreeSql作者叶老板,为我们指一个思路。
按照大佬的思路,可以不改变CAP的代码基础上,通过写一个扩展方法。这样我们就可以仅安装官方提供的包。
> Install-Package DotNetCore.CAP.Dashboard
> Install-Package DotNetCore.CAP.MySql
> Install-Package DotNetCore.CAP.RabbitMQ
> Install-Package FreeSql
> Install-Package FreeSql.DbContext
> Install-Package FreeSql.Provider.MySqlConnector
public static class CapUnitOfWorkExtensions
{
public static ICapTransaction BeginTransaction(this IUnitOfWork unitOfWork, ICapPublisher publisher, bool autoCommit = false)
{
publisher.Transaction.Value = publisher.ServiceProvider.GetService<ICapTransaction>();
return publisher.Transaction.Value.Begin(unitOfWork.GetOrBeginTransaction(), autoCommit);
}
public static void Flush(this ICapTransaction capTransaction)
{
capTransaction?.GetType().GetMethod("Flush", BindingFlags.Instance | BindingFlags.NonPublic)
?.Invoke(capTransaction, null);
}
public static void Commit(this IUnitOfWork unitOfWork,ICapTransaction capTransaction)
{
unitOfWork.Commit();
capTransaction.Flush();
}
}
使用demo,完整项目可查看 https://github.com/luoyunchong/DotNetCore.CAP.Provider/tree/master/samples/Sample.RabbitMQ.MySql.FreeSql
[HttpGet("~/freesql/flush/{id}")]
public DateTime Flush(int id = 0)
{
DateTime now = DateTime.Now;
using (var uow = _freeSql.CreateUnitOfWork())
{
//这个不能使用using,因为这个using掉,uow.Dispose()时就会导致FreeSql,提示cannot access dispose transaction
ICapTransaction trans = uow.BeginTransaction(_capBus, false);
var repo = uow.GetRepository<WeatherForecast>();
repo.Insert(new WeatherForecast()
{
Date = now,
Summary = "summary" + (id == 1
? "summarysummarysummarysummarysummarysummarysummarysummarysummarysummarysummary"
: ""),
TemperatureC = 100
});
if (id == 0)
{
throw new Exception("异常,事务不正常!!");
}
repo.Insert(new WeatherForecast()
{
Date = now,
Summary = "11222",
TemperatureC = 200
});
_capBus.Publish("FreeSqlController.time", now);
uow.Commit(trans);
}
return now;
}
前言
CAP用来处理分布式事务以及提供EventBus
的功能,具有轻量级,高性能,易使用等特点。
安装包
Install-package DotNetCore.CAP
Install-package DotNetCore.CAP.RabbitMQ
Install-package DotNetCore.CAP.SqlServer
使用方法
配置
Startup
->ConfigureServices
中添加服务:
//add CAP
services.AddCap(x =>
{
x.UseEntityFramework<OrderContext>(); // 使用EF
x.UseSqlServer(connecttext); // 使用SQL Server
x.UseRabbitMQ(cfg =>
{
cfg.HostName = "127.0.0.1";
cfg.Port = 5672;
cfg.UserName = "test1";
cfg.Password = "test1";
}); // 使用RabbitMQ
x.UseDashboard(); // Dashboard
// Below settings is just for demo
x.FailedRetryCount = 2;
x.FailedRetryInterval = 5;
});
案例
模仿订单系统下单成功后,推送后续消息到各个子系统,保证分布式系统间通信的一致性。
代码
订单系统的消息发布者:
public interface IOrderRepository
{
bool CreateOrder(Order order);
}
public class OrderRepository : IOrderRepository
{
public readonly OrderContext _context; //ef 订单上下文
public readonly ICapPublisher _capPublisher;//cap 消息发布者
public OrderRepository(OrderContext context, ICapPublisher capPublisher)
{
this._context = context;
this._capPublisher = capPublisher;
}
public bool CreateOrder(Order order)
{
using (var trans = _context.Database.BeginTransaction())
{
var orderEntity = new Order()
{
OrderTime = order.OrderTime
};
_context.Orders.Add(orderEntity);
_context.SaveChanges();
_capPublisher.Publish("czhsoft.services.order.create", orderEntity);
trans.Commit();
}
return true;
}
}
其它子系统的消息订阅者:
public interface IOrderSubscriberService
{
void ConsumeOrderMessage(Order message);
}
public class OrderSubscriberService : IOrderSubscriberService, ICapSubscribe
{
public OrderSubscriberService()
{
}
[CapSubscribe("czhsoft.services.order.create")]
public void ConsumeOrderMessage(Order message)
{
Console.Out.WriteLine($"[MsgApi] Received message : order id {message.OrderId}");
}
}