【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 版本宣布发布,此次发布不仅包括提升性能,减少内存使用等原有特性增强,还修复了部分社区提出的若干问题,更重要的是该版本开源了社 ...
随机推荐
- MySQL常用sql语句-----数据库操作
在数据库操作中,操作基本都是围绕增删改查来操作.简称CRUD C创建创建 R读取/检索查询 U Update修改 D删除删除 在数操作数据库时,所有的数据库语句都要以分号结束 数据库操作不区分大小写 ...
- LeetCode初级算法--设计问题01:Shuffle an Array (打乱数组)
LeetCode初级算法--设计问题01:Shuffle an Array (打乱数组) 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:h ...
- 你必须知道的容器监控 (2) cAdvisor
本篇已加入<.NET Core on K8S学习实践系列文章索引>,可以点击查看更多容器化技术相关系列文章.上一篇我们了解了docker自带的监控子命令以及开源监控工具Weave Scop ...
- Redis 文章一 之持久化机制的介绍
我们已经知道对于一个企业级的redis架构来说,持久化是不可减少的 企业级redis集群架构:海量数据.高并发.高可用 持久化主要是做灾难恢复,数据恢复,也可以归类到高可用的一个环节里面去,比如你re ...
- GridSplitter
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <Colum ...
- 如何在 GitHub 的项目中创建一个分支呢?
如何在 GitHub 的项目中创建一个分支呢? 其实很简单啦,直接点击 Branch,然后在弹出的文本框中添加自己的 Branch Name 然后点击蓝色的Create branch就可以了,这样一来 ...
- Process类调用exe,返回值以及参数空格问题
(方法一)返回值为int fileName为调用的exe路径,入口参数为para,其中多个参数用空格分开,当D:/DD.exe返回值为int类型时. Process p = new Process() ...
- VBoxManage.exe: error: Failed to instantiate CLSID_VirtualBox w/ IVirtualBox, CL SID_VirtualBox w/ IUnknown works.
我先把vagrantbox卸载了 重新装了一个 然后提示这个错误 当时我一脸蒙逼 后来经过百度 1, win+r 快捷键打开 “运行”,输入regedit 打开注册表 2,找到 HKEY_CLASSE ...
- js实现的几种继承方式
他山之石,可以攻玉,本人一直以谦虚的态度学他人之所长,补自己之所短,望各位老师指正! 拜谢 js几种继承方式,学习中的总结: 所谓的继承是为了继承共有的属性,减少不必要代码的书写 第一种:借用构造函数 ...
- 【翻译】Prometheus 2.12.0 新特性
Prometheus 2.12.0 现在(2019.08.17)已经发布,在上个月的 2.11.0 之后又进行了一些修正和改进. 在当前的 6 周发布周期中,每一个 Prometheus 版本都有比较 ...
