【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 版本宣布发布,此次发布不仅包括提升性能,减少内存使用等原有特性增强,还修复了部分社区提出的若干问题,更重要的是该版本开源了社 ...
随机推荐
- Pycharm中Python Console与Terminal的区别
1.Python Console是Python交互式模式,可以直接输入代码,然后执行,并立刻得到结果 2.Terminal是命令行模式,与系统的CMD(命令提示符)一样,可以运行各种系统命令
- postman使用实例
以下以一个登录接口为例,介绍一下postman是如何发请求的. 先执行Pre-request Scripts(预处理) - body - tests(进行断言) postman中变量的引用: {{}}
- 5分钟彻底理解Redis持久化
Redis持久化 RDB快照 在默认情况下,Redis将内存数据库快照保存到dump.rdb的二进制文件中. 可以对Redis进行设置,让它在"N秒内数据集至少有N个改动", 这一 ...
- 百万年薪python之路 -- 数据库初始
一. 数据库初始 1. 为什么要有数据库? 先来一个场景: 假设现在你已经是某大型互联网公司的高级程序员,让你写一个火车票购票系统,来hold住十一期间全国的购票需求,你怎么写? 由于在同一时 ...
- 在Mac OSX上运行Windows应用程序
在Mac OSX中,借助wine,不需要安装虚拟机也可以运行Window应用程序. wine是一个在Linux和UNIX之上的,WIndows3.x和Windows APIs的实现.是运用API转换技 ...
- JVM(1) Java内存区域
对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,不容易出现内存泄漏和内存溢出问题.不过,也正是因为Java程序员把内存控制的权 ...
- 文件读取(filestream)
在stream中已经介绍过,文件读取应用filestream,其是以字节为单位读取文件的.在操作中,当应用filestream创建文件流,读取时应先定义一个字节数组,在转化成char类型,最后转化成s ...
- zookeeper+springboot+dubbo简单实现
第一步:在虚拟机中搭建zookeeper. 第二步:本地创建3个maven工程,分别为wxh-dubbo-api(对外暴露的接口),wxh-dubbo-provider(服务提供者,接口的具体实现), ...
- Codeforces Round #595 (Div. 3)B2 简单的dfs
原题 https://codeforces.com/contest/1249/problem/B2 这道题一开始给的数组相当于地图的路标,我们只需对每个没走过的点进行dfs即可 #include &l ...
- Unity1-HellowWord
1.新建一个Unity工程,选择3D类型项目. 2.目录下有: Assets是主要操作的目录. 3.面板 4.做一个简单的方块移动效果: 1.在Hierarchy面板中,点击Create-3D Obj ...