【NServiceBus】什么是Saga,Saga能做什么
前言
Saga单词翻译过来是指尤指古代挪威或冰岛讲述冒险经历和英雄业绩的长篇故事,对,这里强调长篇故事。许多系统都存在长时间运行的业务流程,NServiceBus使用基于事件驱动的体系结构将容错性和可伸缩性融入这些业务处理过程中。
当然一个单一接口调用则算不上一个长时间运行的业务场景,那么如果在给定的用例中有两个或多个调用,则应该考虑数据一致性的问题,这里有可能第一个接口调用成功,第二次调用则可能失败或者超时,Saga的设计以简单而健壮的方式处理这样的业务用例。
认识Saga
先来通过一段代码简单认识一下Saga,在NServiceBus里,使用Saga的话则需要实现抽象类Saga,SqlSaga,这里的T的是Saga业务实体,封装数据,用来在长时间运行过程中封装业务数据。
public class Saga:Saga<State>,
IAmStartedByMessages<StartOrder>,
IHandleMessages<CompleteOrder>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<State> mapper)
{
mapper.ConfigureMapping<StartOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
mapper.ConfigureMapping<CompleteOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
}
public Task Handle(StartOrder message, IMessageHandlerContext context)
{
return Task.CompletedTask;
}
public Task Handle(CompleteOrder message, IMessageHandlerContext context)
{
MarkAsComplete();
return Task.CompletedTask;
}
}
临时状态
长时间运行则意味着有状态,任何涉及多个网络调用的进程都需要一个临时状态,这个临时状态可以存储在内存中,序列化在磁盘中,也可以存储在分布式缓存中。在NServiceBus中我们定义实体,继承抽象类ContainSagaData即可,默认情况下,所有公开访问的属性都会被持久化。
public class State:ContainSagaData
{
public Guid OrderId { get; set; }
}
添加行为
在NServiceBus里,处理消息的有两种接口:IHandlerMessages、IAmStartedByMessages。
开启一个Saga
在前面的代码片段里,我们看到已经实现了接口IAmStartedByMessages,这个接口告诉NServiceBus,如果收到了StartOrder 消息,则创建一个Saga实例(Saga Instance),当然Saga长流程处理的实体至少有一个需要开启Saga流程。
处理无序消息
如果你的业务用例中确实存在无序消息的情况,则还需要业务流程正常轮转,那么则需要多个messaeg都要事先接口IAmStartedByMessages接口,也就是说多个message都可以创建Saga实例。
依赖可恢复性
在处理无序消息和多个消息类型的时候,就存在消息丢失的可能,必须在你的Saga状态完成以后,这个Saga实例又收到一条消息,但这时Saga状态已经是完结状态,这条消息则仍然需要处理,这里则实现NServiceBus的IHandleSagaNotFound接口。
public class SagaNotFoundHandler:IHandleSagaNotFound
{
public Task Handle(object message, IMessageProcessingContext context)
{
return context.Reply(new SagaNotFoundMessage());
}
}
public class SagaNotFoundMessage
{
}
结束Saga
当你的业务用例不再需要Saga实例时,则调用MarkComplete()来结束Saga实例。这个方法在前面的代码片段中也可以看到,其实本质也就是设置Saga.Complete属性,这是个bool值,你在业务用例中也可以用此值来判断Saga流程是否结束。
namespace NServiceBus
{
using System;
using System.Threading.Tasks;
using Extensibility;
public abstract class Saga
{
/// <summary>
/// The saga's typed data.
/// </summary>
public IContainSagaData Entity { get; set; }
public bool Completed { get; private set; }
internal protected abstract void ConfigureHowToFindSaga(IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration);
protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, DateTime at) where TTimeoutMessageType : new()
{
return RequestTimeout(context, at, new TTimeoutMessageType());
}
protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, DateTime at, TTimeoutMessageType timeoutMessage)
{
if (at.Kind == DateTimeKind.Unspecified)
{
throw new InvalidOperationException("Kind property of DateTime 'at' must be specified.");
}
VerifySagaCanHandleTimeout(timeoutMessage);
var options = new SendOptions();
options.DoNotDeliverBefore(at);
options.RouteToThisEndpoint();
SetTimeoutHeaders(options);
return context.Send(timeoutMessage, options);
}
protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within) where TTimeoutMessageType : new()
{
return RequestTimeout(context, within, new TTimeoutMessageType());
}
protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage)
{
VerifySagaCanHandleTimeout(timeoutMessage);
var sendOptions = new SendOptions();
sendOptions.DelayDeliveryWith(within);
sendOptions.RouteToThisEndpoint();
SetTimeoutHeaders(sendOptions);
return context.Send(timeoutMessage, sendOptions);
}
protected Task ReplyToOriginator(IMessageHandlerContext context, object message)
{
if (string.IsNullOrEmpty(Entity.Originator))
{
throw new Exception("Entity.Originator cannot be null. Perhaps the sender is a SendOnly endpoint.");
}
var options = new ReplyOptions();
options.SetDestination(Entity.Originator);
context.Extensions.Set(new AttachCorrelationIdBehavior.State { CustomCorrelationId = Entity.OriginalMessageId });
options.Context.Set(new PopulateAutoCorrelationHeadersForRepliesBehavior.State
{
SagaTypeToUse = null,
SagaIdToUse = null
});
return context.Reply(message, options);
}
//这个方法结束saga流程,标记Completed属性
protected void MarkAsComplete()
{
Completed = true;
}
void VerifySagaCanHandleTimeout<TTimeoutMessageType>(TTimeoutMessageType timeoutMessage)
{
var canHandleTimeoutMessage = this is IHandleTimeouts<TTimeoutMessageType>;
if (!canHandleTimeoutMessage)
{
var message = $"The type '{GetType().Name}' cannot request timeouts for '{timeoutMessage}' because it does not implement 'IHandleTimeouts<{typeof(TTimeoutMessageType).FullName}>'";
throw new Exception(message);
}
}
void SetTimeoutHeaders(ExtendableOptions options)
{
options.SetHeader(Headers.SagaId, Entity.Id.ToString());
options.SetHeader(Headers.IsSagaTimeoutMessage, bool.TrueString);
options.SetHeader(Headers.SagaType, GetType().AssemblyQualifiedName);
}
}
}
Saga持久化
本机开发环境我们使用LearningPersistence,但是投产的话则需要使用数据库持久化,这里我们基于MySQL,SQL持久化需要引入NServiceBus.Persistence.Sql。SQL Persistence会生成几种关系型数据库的sql scripts,然后会根据你的断言配置选择所需数据库,比如SQL Server、MySQL、PostgreSQL、Oracle。
持久化Saga自动创建所需表结构,你只需手动配置即可,配置后编译成功后项目执行目录下会生成sql脚本,文件夹名称是NServiceBus.Persistence.Sql,下面会有Saga子目录。
/* TableNameVariable */
set @tableNameQuoted = concat('`', @tablePrefix, 'Saga`');
set @tableNameNonQuoted = concat(@tablePrefix, 'Saga');
/* Initialize */
drop procedure if exists sqlpersistence_raiseerror;
create procedure sqlpersistence_raiseerror(message varchar(256))
begin
signal sqlstate
'ERROR'
set
message_text = message,
mysql_errno = '45000';
end;
/* CreateTable */
set @createTable = concat('
create table if not exists ', @tableNameQuoted, '(
Id varchar(38) not null,
Metadata json not null,
Data json not null,
PersistenceVersion varchar(23) not null,
SagaTypeVersion varchar(23) not null,
Concurrency int not null,
primary key (Id)
) default charset=ascii;
');
prepare script from @createTable;
execute script;
deallocate prepare script;
/* AddProperty OrderId */
select count(*)
into @exist
from information_schema.columns
where table_schema = database() and
column_name = 'Correlation_OrderId' and
table_name = @tableNameNonQuoted;
set @query = IF(
@exist <= 0,
concat('alter table ', @tableNameQuoted, ' add column Correlation_OrderId varchar(38) character set ascii'), 'select \'Column Exists\' status');
prepare script from @query;
execute script;
deallocate prepare script;
/* VerifyColumnType Guid */
set @column_type_OrderId = (
select concat(column_type,' character set ', character_set_name)
from information_schema.columns
where
table_schema = database() and
table_name = @tableNameNonQuoted and
column_name = 'Correlation_OrderId'
);
set @query = IF(
@column_type_OrderId <> 'varchar(38) character set ascii',
'call sqlpersistence_raiseerror(concat(\'Incorrect data type for Correlation_OrderId. Expected varchar(38) character set ascii got \', @column_type_OrderId, \'.\'));',
'select \'Column Type OK\' status');
prepare script from @query;
execute script;
deallocate prepare script;
/* WriteCreateIndex OrderId */
select count(*)
into @exist
from information_schema.statistics
where
table_schema = database() and
index_name = 'Index_Correlation_OrderId' and
table_name = @tableNameNonQuoted;
set @query = IF(
@exist <= 0,
concat('create unique index Index_Correlation_OrderId on ', @tableNameQuoted, '(Correlation_OrderId)'), 'select \'Index Exists\' status');
prepare script from @query;
execute script;
deallocate prepare script;
/* PurgeObsoleteIndex */
select concat('drop index ', index_name, ' on ', @tableNameQuoted, ';')
from information_schema.statistics
where
table_schema = database() and
table_name = @tableNameNonQuoted and
index_name like 'Index_Correlation_%' and
index_name <> 'Index_Correlation_OrderId' and
table_schema = database()
into @dropIndexQuery;
select if (
@dropIndexQuery is not null,
@dropIndexQuery,
'select ''no index to delete'';')
into @dropIndexQuery;
prepare script from @dropIndexQuery;
execute script;
deallocate prepare script;
/* PurgeObsoleteProperties */
select concat('alter table ', table_name, ' drop column ', column_name, ';')
from information_schema.columns
where
table_schema = database() and
table_name = @tableNameNonQuoted and
column_name like 'Correlation_%' and
column_name <> 'Correlation_OrderId'
into @dropPropertiesQuery;
select if (
@dropPropertiesQuery is not null,
@dropPropertiesQuery,
'select ''no property to delete'';')
into @dropPropertiesQuery;
prepare script from @dropPropertiesQuery;
execute script;
deallocate prepare script;
/* CompleteSagaScript */
生成的表结构:
持久化配置
Saga持久化需要依赖NServiceBus.Persistence.Sql。引入后需要实现SqlSaga抽象类,抽象类需要重写ConfigureMapping,配置Saga工作流程业务主键。
public class Saga:SqlSaga<State>,
IAmStartedByMessages<StartOrder>
{
protected override void ConfigureMapping(IMessagePropertyMapper mapper)
{
mapper.ConfigureMapping<StartOrder>(message=>message.OrderId);
}
protected override string CorrelationPropertyName => nameof(StartOrder.OrderId);
public Task Handle(StartOrder message, IMessageHandlerContext context)
{
Console.WriteLine($"Receive message with OrderId:{message.OrderId}");
MarkAsComplete();
return Task.CompletedTask;
}
}
static async Task MainAsync()
{
Console.Title = "Client-UI";
var configuration = new EndpointConfiguration("Client-UI");
//这个方法开启自动建表、自动创建RabbitMQ队列
configuration.EnableInstallers();
configuration.UseSerialization<NewtonsoftSerializer>();
configuration.UseTransport<LearningTransport>();
string connectionString = "server=127.0.0.1;uid=root;pwd=000000;database=nservicebus;port=3306;AllowUserVariables=True;AutoEnlist=false";
var persistence = configuration.UsePersistence<SqlPersistence>();
persistence.SqlDialect<SqlDialect.MySql>();
//配置mysql连接串
persistence.ConnectionBuilder(()=>new MySqlConnection(connectionString));
var instance = await Endpoint.Start(configuration).ConfigureAwait(false);
var command = new StartOrder()
{
OrderId = Guid.NewGuid()
};
await instance.SendLocal(command).ConfigureAwait(false);
Console.ReadKey();
await instance.Stop().ConfigureAwait(false);
}
Saga Timeouts
在消息驱动类型的环境中,虽然传递的无连接特性可以防止在线等待过程中消耗资源,但是毕竟等待时间需要有一个上线。在NServiceBus里已经提供了Timeout方法,我们只需订阅即可,可以在你的Handle方法中根据需要订阅Timeout,可参考如下代码:
public class Saga:Saga<State>,
IAmStartedByMessages<StartOrder>,
IHandleMessages<CompleteOrder>,
IHandleTimeouts<TimeOutMessage>
{
public Task Handle(StartOrder message, IMessageHandlerContext context)
{
var model=new TimeOutMessage();
//订阅超时消息
return RequestTimeout(context,TimeSpan.FromMinutes(10));
}
public Task Handle(CompleteOrder message, IMessageHandlerContext context)
{
MarkAsComplete();
return Task.CompletedTask;
}
protected override string CorrelationPropertyName => nameof(StartOrder.OrderId);
public Task Timeout(TimeOutMessage state, IMessageHandlerContext context)
{
//处理超时消息
}
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<State> mapper)
{
mapper.ConfigureMapping<StartOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
mapper.ConfigureMapping<CompleteOrder>(message=>message.OrderId).ToSaga(saga=>saga.OrderId);
}
}
//从Timeout的源码看,这个方法是通过设置SendOptions,然后再把当前这个消息发送给自己来实现
protected Task RequestTimeout<TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage)
{
VerifySagaCanHandleTimeout(timeoutMessage);
var sendOptions = new SendOptions();
sendOptions.DelayDeliveryWith(within);
sendOptions.RouteToThisEndpoint();
SetTimeoutHeaders(sendOptions);
return context.Send(timeoutMessage, sendOptions);
}
总结
NServiceBus因为是商业产品,对分布式消息系统所涉及到的东西都做了实现,包括分布式事务(Outbox)、DTC都有,还有心跳检测,监控都有,全而大,目前我们用到的也只是NServiceBus里很小的一部分功能。
【NServiceBus】什么是Saga,Saga能做什么的更多相关文章
- ENode 1.0 - Saga的思想与实现
开源地址:https://github.com/tangxuehua/enode 因为enode框架的思想是,一次修改只能新建或修改一个聚合根:那么,如果一个用户请求要涉及多个聚合根的新建或修改该怎么 ...
- enode框架step by step之saga的思想与实现
enode框架step by step之saga的思想与实现 enode框架系列step by step文章系列索引: 分享一个基于DDD以及事件驱动架构(EDA)的应用开发框架enode enode ...
- Redux:从action到saga
前端应用消失的部分 一个现代的.使用了redux的前端应用架构可以这样描述: 一个存储了应用不可变状态(state)的store 状态(state)可以被绘制在组件里(html或者其他的东西).这个绘 ...
- 分布式事务:Saga模式
1 Saga相关概念 1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇Paper Sagas,讲述的是如何处理long lived transac ...
- 架构模式: Saga
架构模式: Saga 上下文 您已应用每服务数据库模式.每个服务都有自己的数据库.但是,某些业务事务跨越多个服务,因此您需要一种机制来确保服务之间的数据一致性.例如,假设您正在建立一个客户有信用额度的 ...
- 分布式事物SAGA
目录 概述SAGA SAGA的执行方式 存在的问题 重试机制 SAGA VS TCC 实现SAGA的框架 概述SAGA SAGA是1987 Hector & Kenneth 发表的论文,主要是 ...
- 快速搭建react项目骨架(按需加载、redux、axios、项目级目录等等)
一.前言 最近整理了一下项目骨架,顺便自定义了一个脚手架,方便日后使用.我会从头开始,步骤一步步写明白,如果还有不清楚的可以评论区留言.先大致介绍一下这个骨架,我们采用 create-react-ap ...
- 分布式事务解决方案以及 .Net Core 下的实现(上)
数据一致性是构建业务系统需要考虑的重要问题 , 以往我们是依靠数据库来保证数据的一致性.但是在微服务架构以及分布式环境下实现数据一致性是一个很有挑战的的问题.最近在研究分布式事物,分布式的解决方案有很 ...
- RocketMQ 4.3正式发布,支持分布式事务
冯嘉 作者 | 冯嘉 近日,Apache RocketMQ 4.3 版本宣布发布,此次发布不仅包括提升性能,减少内存使用等原有特性增强,还修复了部分社区提出的若干问题,更重要的是该版本开源了社 ...
随机推荐
- i春秋DMZ大型靶场实验(一)内网拓展
更具提示 先下载工具包 ip 172.16.12.226 打开bp 进行代理发现 整个页面 没有请求 没有其页面通过 御剑,dir ,hscan 进行目录爆破未发现有用信息 对当前页面进 ...
- spring boot 中的路径映射
在spring boot中集成thymeleaf后,我们知道thymeleaf的默认的html的路径为classpath:/templates也就是resources/templates,那如何访问这 ...
- 5. Sersync实时同步
rsync+Sersync数据的实时同步 sersync介绍 1.什么是实时同步 监控一个目录的变化, 当该目录触发事件(创建\删除\修改) 就执行动作, 这个动作可以是 rsync同步 ,也可以是其 ...
- 小白学 Python(7):基础流程控制(上)
人生苦短,我选Python 前文传送门 小白学 Python(1):开篇 小白学 Python(2):基础数据类型(上) 小白学 Python(3):基础数据类型(下) 小白学 Python(4):变 ...
- css布局两端固定中间自适应
第一种:采用浮动 1.1首先来看一下网上一个哥们给的代码 <body> <div class="left">左</div> <div cl ...
- 在idea中添加try/catch的快捷键
在idea中添加try/catch的快捷键ctrl+alt+t选中想被try/catch包围的语句,同时按下ctrl+alt+t,出现下图
- LeetCode 1: single-number
Given an array of integers, every element appears twice except for one. Find that single one. soluti ...
- 如何利用C# Roslyn编译器写一个简单的代码提示/错误检查?
OK, 废话不多说,这些天在写C#代码时突然对于IDE提示有了一些想法,之前也有了解过,不过并没有深入. 先看个截图: 一段再简单不过的代码了,大家注意看到 count 字段下面的绿色波浪线了吗,我们 ...
- 关于javascript闭包的最通俗易懂的理解
这两天在研究闭包,网上一通找,有牛人写的帖子,有普通人写的帖子,但是大多没戳中本小白所纠结的点,而且大多插入了立即执行函数,其实根本不需要的,反而让人产生了误解.这里我用我的方式讲解一下闭包. 1.目 ...
- django-Views之request(二)
book/views.py def index(request): http_list = { '<h1>请求协议: <span style="color:red" ...