前言

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能做什么的更多相关文章

  1. ENode 1.0 - Saga的思想与实现

    开源地址:https://github.com/tangxuehua/enode 因为enode框架的思想是,一次修改只能新建或修改一个聚合根:那么,如果一个用户请求要涉及多个聚合根的新建或修改该怎么 ...

  2. enode框架step by step之saga的思想与实现

    enode框架step by step之saga的思想与实现 enode框架系列step by step文章系列索引: 分享一个基于DDD以及事件驱动架构(EDA)的应用开发框架enode enode ...

  3. Redux:从action到saga

    前端应用消失的部分 一个现代的.使用了redux的前端应用架构可以这样描述: 一个存储了应用不可变状态(state)的store 状态(state)可以被绘制在组件里(html或者其他的东西).这个绘 ...

  4. 分布式事务:Saga模式

    1 Saga相关概念 1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem发表了一篇Paper Sagas,讲述的是如何处理long lived transac ...

  5. 架构模式: Saga

    架构模式: Saga 上下文 您已应用每服务数据库模式.每个服务都有自己的数据库.但是,某些业务事务跨越多个服务,因此您需要一种机制来确保服务之间的数据一致性.例如,假设您正在建立一个客户有信用额度的 ...

  6. 分布式事物SAGA

    目录 概述SAGA SAGA的执行方式 存在的问题 重试机制 SAGA VS TCC 实现SAGA的框架 概述SAGA SAGA是1987 Hector & Kenneth 发表的论文,主要是 ...

  7. 快速搭建react项目骨架(按需加载、redux、axios、项目级目录等等)

    一.前言 最近整理了一下项目骨架,顺便自定义了一个脚手架,方便日后使用.我会从头开始,步骤一步步写明白,如果还有不清楚的可以评论区留言.先大致介绍一下这个骨架,我们采用 create-react-ap ...

  8. 分布式事务解决方案以及 .Net Core 下的实现(上)

    数据一致性是构建业务系统需要考虑的重要问题 , 以往我们是依靠数据库来保证数据的一致性.但是在微服务架构以及分布式环境下实现数据一致性是一个很有挑战的的问题.最近在研究分布式事物,分布式的解决方案有很 ...

  9. RocketMQ 4.3正式发布,支持分布式事务

      冯嘉 作者 | 冯嘉   近日,Apache RocketMQ 4.3 版本宣布发布,此次发布不仅包括提升性能,减少内存使用等原有特性增强,还修复了部分社区提出的若干问题,更重要的是该版本开源了社 ...

随机推荐

  1. Spring Boot Security And JSON Web Token

    Spring Boot Security And JSON Web Token 说明 流程说明 何时生成和使用jwt,其实我们主要是token更有意义并携带一些信息 https://github.co ...

  2. win7更新,360手机安装谷歌框架

    这两天把11平台被卸载了,不能打竞技场了,很伤心. 成年男子,总要找点有趣的事情去做.我准备洗心革面,好好学习.(巴拉巴拉巴拉一万字.) 首先第一件事情就是重装系统,(由于买了个假显卡,win10以上 ...

  3. sublime设置 reindent 快捷键

    设置快捷键 使用快捷键 cmd + shift + p 打开控制面板 输入 key 关键词 点击进入 Key Bindings -User 添加如下代码 { "keys": [&q ...

  4. Elasticsearch 知识点整理 一

    极力推荐: 官网地址: https://www.elastic.co/guide/en/elasticsearch/reference/6.0 肺腑之言,学ES先学原生的语法,SpringData封装 ...

  5. 初学android小笔记(一)

    一:应用外观基础设置 (1)去掉标题栏:打开Android Manifest文件,将theme如下设置 (2)改应用图标:将icon指定图片改为想要的app图标即可 (3)改应用名字: 在Manife ...

  6. python基础-集合set及内置方法

    数据类型之集合-set 用途:多用于去重,关系运算 定义方式:通过大括号存储,集合中的每个元素通过逗号分隔.集合内存储的元素必须是不可变的,因此,列表-List 和字典dict 不能存储在集合中 注意 ...

  7. 前端技术之:JavaScript Test 断言库

    expect 声称可以写更好的断言. https://github.com/mjackson/expect   chai 可以写BDD样式的断言,也可以写TDD样式的断言,可用于Node.js与浏览器 ...

  8. 关于php注释那些事

    代码注释的作用  --- 为自己,也为别人. 永远不要过于相信自己的理解力!当你思路通畅,思如泉涌,进入编程境界,你可以很流畅的实现某个功能,但这种一泻千里的流畅可能只停留在了当时的状态.当你几个月, ...

  9. 《Effective Java》 读书笔记(二) 在构造参数过多的时候优先考虑使用构造器

    刚开始看见这个标题的时候,我想到了python可以选择初始化参数的语法,C++.C#能有默认参数. 为什么Java什么都没有~~ 好吧,我们是使用构造器来实现它. 1.当一个类的构造函数需要很多构造函 ...

  10. 【XSY2985】【BZOJ1367】【Baltic2004】sequence

    考虑两种情况: 1.\(a_1\)<\(a_2\)<\(a_3\)<\(a_4\)...<\(a_n\) 直接令\(b_i\)=\(a_i\),最小. 2.\(a_1\)> ...