0%

C#调用C++方法,C#使用c++方法返回类或结构体。

1. 在c++中定义结构体,类,处理方法;要给c#调用的方法要加extern “C” __declspec(dllexport) 修饰

#include "stdafx.h"

#define EXPORT_DLL extern "C" __declspec(dllexport)

EXPORT_DLL int Add(``int a,``int b)

{

return a + b;

}

struct Bar

{

public :

int id;

char``* name;

};

EXPORT_DLL void GetBar(Bar& bar)

{

bar.id = 10;

bar.name = "hi bar 中文了"``;

}

class Foo

{

public :

int id;

char``* name;

};

EXPORT_DLL void GetFoo(Foo& foo)

{

foo.id = 100;

foo.name = "I'm 傻瓜"``;

}

2. 在C#中使用C++方法,首先需要定义C++中的Foo(在c++中定义为类class),Bar(在C++中定义为struct)对应的数据结构struct;然后定义extern的方法,如下代码所示:

using System;

using System.Collections.Generic;

using System.Text;

using System.Runtime.InteropServices;

namespace UseCppDll

{

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]

public struct Bar

{

/// int

public int id;

/// char*

[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.LPStr)]

public string name;

}

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]

public struct Foo

{

/// int

public int id;

/// char*

[System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.LPStr)]

public string name;

}

class Program

{

[DllImport(``"CppDll.dll"``,EntryPoint=``"Add"``)]

extern static int Add(``int a, int b);

[DllImport(``"CppDll.dll"``, EntryPoint = "GetBar"``,CharSet=CharSet.Ansi)]

extern static void GetBar(``ref Bar bar);

[DllImport(``"CppDll.dll"``, EntryPoint = "GetFoo"``, CharSet = CharSet.Ansi)]

extern static void GetFoo(``ref Foo foo);

static void Main(``string``[] args)

{

Console.WriteLine(Add(5,4));

Bar b = new Bar();

GetBar(``ref b);

Console.WriteLine(``"b's id is " + b.id);

Console.WriteLine(``"b's name is " + b.name);

Foo f = new Foo();

GetFoo(``ref f);

Console.WriteLine(``"f's id is " + f.id);

Console.WriteLine(``"f's name is " + f.name);

Console.Read();

}

}

}

C++中的类或者结构在C#中的定义代码可以使用微软提供的工具(P/Invoke Interop Assistant)生成。

在运行C#程序前要把C++dll复制到C#程序运行目录下。

C#WPF的多屏显示问题(完善版) - CodeBuug

Excerpt

之前就做过这个多屏显示的问题,但是这几天在现场的时候还是会出现问题。  出现的问题主要是: 一、窗口不用对应的显示在所希望的显示器上。比如希望1,2…


之前就做过这个多屏显示的问题,但是这几天在现场的时候还是会出现问题。 

出现的问题主要是:

一、窗口不用对应的显示在所希望的显示器上。比如希望1,2,3显示器分别显示窗口1,2,3.但实际上1,2窗口显示在1显示器上,2窗口显示在2显示上,而三显示器上并没有显示。

二、并不能按照顺序对应显示,会造成显示器序号和窗口序号不对应。

三、窗口的区域不对。有时候一个窗口会跨2个显示器显示,一个显示器中并不能完全显示整个窗口

之后再进行了一些资料的查阅和修改。 

产生这些问题的原因。

1.WindowState的最大化不对,如果在窗口加载之前就把窗口最大化之后,则窗口也不能正确的显示到所希望的显示器。只能显示到主窗口

2.显示区域的范围选择不对。

对整个程序进行了一下的修改。并且进行一些显示器设置操作

首先如果需要用到Screen,这个类型就必须包含:

using System.Windows.Forms;这个语句,如果不成功请查阅相关的资料。

1.显示器设置操作:

这里用displayfusion对四个显示进行了设置,设置主显示器为第一个显示器,坐标从0,0开始,其他显示器一次紧贴紧跟在第一个显示器之后,并且把所有的显示Y坐标都定义为0;

读取显示列表后要根据X坐标的大小进行排序,使第一个显示器为主显示器。

1
2
<span>            </span><span>List</span><span>&lt;</span><span>System</span><span>.</span><span>Windows</span><span>.</span><span>Forms</span><span>.</span><span>Screen</span><span>&gt;</span><span> screens </span><span>=</span><span> </span><span>System</span><span>.</span><span>Windows</span><span>.</span><span>Forms</span><span>.</span><span>Screen</span><span>.</span><span>AllScreens</span><span>.</span><span>ToList</span><span>();</span><span>
screens</span><span>.</span><span>Sort</span><span>((</span><span>x</span><span>,</span><span> y</span><span>)</span><span> </span><span>=&gt;</span><span> x</span><span>.</span><span>Bounds</span><span>.</span><span>X</span><span>.</span><span>CompareTo</span><span>(</span><span>y</span><span>.</span><span>Bounds</span><span>.</span><span>X</span><span>));</span>

2.窗口最大化语句放入到窗口的加载函数中

再xaml界面中选中窗口,然后在属性窗口中添加loaded行为:

1
2
3
4
5
<span>    </span><span>private</span><span> </span><span>void</span><span> </span><span>Window_Loaded</span><span>(</span><span>object</span><span> sender</span><span>,</span><span> </span><span>RoutedEventArgs</span><span> e</span><span>)</span><span>
</span><span>{</span><span>
</span><span>}</span><span>

</span>

在此函数中添加语句

1
<span>            </span><span>WindowState</span><span> </span><span>=</span><span> </span><span>WindowState</span><span>.</span><span>Maximized</span><span>;</span>

每个窗口都需要进行这样操作,添加loaded函数,然后添加语句设置窗口最大化。

3.编写对应窗口对应显示器的display函数,屏幕的显示范围用Bounds来取值,而不是用之前的WorkingArea来取值。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<span>            </span><span>Window</span><span> window </span><span>=</span><span> _window </span><span>as</span><span> </span><span>Window</span><span>;</span><span>
</span><span>if</span><span> </span><span>(</span><span>window </span><span>!=</span><span> </span><span>null</span><span>)</span><span>
</span><span>{</span><span>
</span><span>//用workingarea显示区域会出错,按照显示的Bounds来显示需要的内容。</span><span>
</span><span>//另外需要用屏幕管理软件比如displayfusion来管理各个显示器的位置。</span><span>
</span><span>//设置各个显示的坐标位置,把所有显示的高度初始都标志位0;</span><span>
window</span><span>.</span><span>Top</span><span> </span><span>=</span><span> screen</span><span>.</span><span>Bounds</span><span>.</span><span>Top</span><span>;</span><span>
window</span><span>.</span><span>Left</span><span> </span><span>=</span><span> screen</span><span>.</span><span>Bounds</span><span>.</span><span>Left</span><span>;</span><span>
window</span><span>.</span><span>Width</span><span> </span><span>=</span><span> screen</span><span>.</span><span>Bounds</span><span>.</span><span>Width</span><span>;</span><span>
window</span><span>.</span><span>Height</span><span> </span><span>=</span><span> screen</span><span>.</span><span>Bounds</span><span>.</span><span>Height</span><span>;</span><span>
</span><span>//每个窗口都需要加载window_load函数,然后再window_load函数中定义窗口的最大化,不能在显示的时候进行</span><span>
</span><span>//最大化设置,不然窗口会现实回主显示器中。</span><span>

window</span><span>.</span><span>Show</span><span>();</span><span>
</span><span>}</span>

4。显示功能的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<span>    </span><span>//获取显示器的数量</span><span>
</span><span>List</span><span>&lt;</span><span>System</span><span>.</span><span>Windows</span><span>.</span><span>Forms</span><span>.</span><span>Screen</span><span>&gt;</span><span> screens </span><span>=</span><span> </span><span>System</span><span>.</span><span>Windows</span><span>.</span><span>Forms</span><span>.</span><span>Screen</span><span>.</span><span>AllScreens</span><span>.</span><span>ToList</span><span>();</span><span>
screens</span><span>.</span><span>Sort</span><span>((</span><span>x</span><span>,</span><span> y</span><span>)</span><span> </span><span>=&gt;</span><span> x</span><span>.</span><span>Bounds</span><span>.</span><span>X</span><span>.</span><span>CompareTo</span><span>(</span><span>y</span><span>.</span><span>Bounds</span><span>.</span><span>X</span><span>));</span><span>
</span><span>//对显示器的边界坐标进行排序,0最小的为主显示器,其他的显示按照中间,左边,右边挨个显示,</span><span>
</span><span>//如果获取到显示器数量为4个,则表示4个显示器打开了。主显示器也已经打开,则程序启动后直接显示主界面以外的3个显示器</span><span>
</span><span>//用workingarea显示区域会出错,按照显示的Bounds来显示需要的内容。</span><span>
</span><span>//另外需要用屏幕管理软件比如displayfusion来管理各个显示器的位置。</span><span>
</span><span>//设置各个显示的坐标位置,把所有显示的高度初始都标志位0;</span><span>
</span><span>Window1</span><span> window1 </span><span>=</span><span> </span><span>new</span><span> </span><span>Window1</span><span>();</span><span>
</span><span>Window2</span><span> window2 </span><span>=</span><span> </span><span>new</span><span> </span><span>Window2</span><span>();</span><span>


</span><span>if</span><span> </span><span>(</span><span>screens</span><span>.</span><span>Count</span><span> </span><span>==</span><span> </span><span>4</span><span>)</span><span>
</span><span>{</span><span>
</span><span>Display</span><span>(</span><span>this</span><span>,</span><span> screens</span><span>[</span><span>1</span><span>]);</span><span>
</span><span>Display</span><span>(</span><span>window1</span><span>,</span><span> screens</span><span>[</span><span>2</span><span>]);</span><span>
</span><span>Display</span><span>(</span><span>window2</span><span>,</span><span> screens</span><span>[</span><span>3</span><span>]);</span><span>
</span><span>}</span><span>
</span><span>else</span><span>
</span><span>{</span><span>
</span><span>if</span><span> </span><span>(</span><span>screens</span><span>.</span><span>Count</span><span> </span><span>&gt;</span><span> </span><span>1</span><span>)</span><span>
</span><span>Display</span><span>(</span><span>window1</span><span>,</span><span> screens</span><span>[</span><span>1</span><span>]);</span><span>
</span><span>else</span><span>
</span><span>Display</span><span>(</span><span>window1</span><span>,</span><span> screens</span><span>[</span><span>0</span><span>]);</span><span>
</span><span>if</span><span> </span><span>(</span><span>screens</span><span>.</span><span>Count</span><span> </span><span>&gt;</span><span> </span><span>2</span><span>)</span><span>
</span><span>Display</span><span>(</span><span>window2</span><span>,</span><span> screens</span><span>[</span><span>2</span><span>]);</span><span>
</span><span>else</span><span>
</span><span>Display</span><span>(</span><span>window2</span><span>,</span><span> screens</span><span>[</span><span>0</span><span>]);</span><span>
</span><span>}</span>

上一篇:《IDDD 实现领域驱动设计-SOA、REST 和六边形架构

阅读目录:

  1. CQRS-命令查询职责分离

  2. EDA-事件驱动架构

    1. Domin Event-领域事件

    2. Long-Running Process(Saga)-长时处理过程

    3. Event Sourcing-事件溯源

  3. CQRS Journey-微软示例项目

  4. ENode-netfocus 实践项目

存在即是理由,每一种架构的产生都会有一种特定的场景,或者解决某一种实际应用问题,经验的累积促成了某一种架构的产生。

1. CQRS-命令查询职责分离

说明:本图摘自 MSDN

CQRS(Command & Query Responsibility Segregation)命令查询职责分离,和 REST 同属于架构风格,如果单纯理解 CQRS,是比较容易的,另一种方式解释就是,一个方法要么是执行某种动作的命令,要么是返回数据的查询,命令的体现是对系统状态的修改,而查询则不会,职责的分离更加有利于领域模型的提炼,系统的灵活性和可扩展性也得到进一步加强。

为什么要进行命令和查询职责分离?

如果你有时间,可以先阅读下上面几篇博文及相关评论。

我们都知道 Repository 的职责就是管理聚合根(Aggregate)对象,一般是一一对应关系,领域层中的业务逻辑要对某种聚合根对象进行操作,必须要通过 Repository,而应用层接受用户请求获取数据对象显示,也必须要通过 Repository 进行聚合根对象转换,这个一般没有涉及到领域业务操作,仅仅只是获取聚合根对象数据。领域层中的业务逻辑要求 Repository 实现对聚合根状态的管理,所以我们一般会在领域层 IRepository 接口中定义 Add、Update、GetById 等方法,然后在基础设施层中的 Repository 进行实现,而来自应用层的要求,需要获取聚合根对象数据,所以在 Repository 中还需要添加一些 GetList 等操作,而根据 IRepository 的接口契约,返回的类型必须是聚合根,而在这种场景中,是不需要获取聚合根对象的,只需要获取数据(DTO)就可以了。。。

我大致列一下上面描述中,所出现的一系列问题:

  1. Repository 职责变得飘忽不定。
  2. IRepository 会被污染,导致的结果是领域层也会被污染。
  3. Repository 会出现本不应该出现的 DTO 概念。
  4. Repository 会被大量 GetList 操作所吞没。
  5. Repository 最后会变得“人不像人,鬼不像鬼”。

如果你带着这些问题去理解 CQRS,就会有这样的感慨:“天哪,这简直就是老天派下的一个救星啊!”。

回到一开始的那张图上,看起来感觉很简单的样子,来自用户 UI 的请求分为 Query(查询)和 Command(命令),这些请求操作都会被 Service Interfaces(服务接口,只是一个统称)接收,然后再进行分发处理,对于命令操作会更新 Update Data store,因为读与写分离,为了保持数据的一致性,我们还需要把数据更新应用到 Read Data store。对于一般的应用系统来说,查询会占很大的比重,因为读与写分离了,所以我们可以针对查询进行进一步性能优化,而且还可以保持查询的灵活性和独立性,这种方式在应对大型业务系统来说是非常重要的,从这种层面上来说,CQRS 不用于 DDD 架构好像也是可以的,因为它是一种风格,并不局限于一种架构实现,所以你可以把它有价值的东西进行提炼,应用到合适的一个架构系统中也是可以的。

如果 CQRS 中包含有 Domain(领域)的概念,会是怎样的一种情形呢?

说明:本图摘自 AxonFramework

上面图中包含有很多的概念,但本质是和第一张图是一样的,只不过在其基础上进行了扩展和延伸,先列举一下所涉及的概念:

  • Command Bus(命令总线):图中没有,应该放在 Command Handler 之前,可以看作是 Command 发布者。
  • Command Handler(命令处理器):处理来自 Command Bus 分发的请求,可以看作是 Command 订阅者、处理者。
  • Event Bus(事件总线):一般在 Command Handler 完成之后,可以看作是 Event 发布者。
  • Event Handler(事件处理器):处理来自 Event Bus 分发的请求,可以看作是 Event 订阅者、处理者。
  • Event Store(事件存储):对应概念 Event Sourcing(事件溯源),可以用于事件回放处理,还原指定对象状态。

上面有些是 EDA(事件驱动架构)中的概念,这个在后面会有详细说明,我简单描述一下处理流程,首先抽离两个重要概念:Command(命令)和 Event(事件),Command 是一种命令的语气(本身就是命令的意思,呵呵),它的效果就是对某种对象状态的修改,Command Bus 收集来自 UI 的 Command 命令,并根据具体命令分发给具体的 Command Handler 进行处理,这时候就会产生一些领域操作,并对相应的领域对象进行修改,Command Handler 只是修改操作,并不会涉及到修改之后的操作(比如保存、事件发布等),Command Handler 完成之后并不表示这个 Command 命令就此结束,它需要把接下来的操作交给 Event Bus(完成之后的操作),并分发给相应的 Event Handler 订阅者进行处理,一般是数据保存、事件存储等。

我们来看 IDDD 中的一段代码(P126):

1
2
3
4
5
6
7
8
9
10
11
12
13
public void commitBacklogItemToSprint(
String aTenantId, String aBacklogItemId, String aSprintId) {

TenantId tenantId = new TenantId(aTenantId);

BacklogItem backlogItem = backlogItemRepository().backlogItemOfId(
tenantId, new BacklogItemId(aBacklogItemId));

Sprint sprint = sprintRepository().backlogItemOfId(
tenantId, new SprintId(aSprintId));

backlogItem.commitTo(sprint);
}

commitBacklogItemToSprint 就可以看作是一个 Command Handler,注意其命名(commitXXXXToXXXX),一眼看过去就是命令的意思,commitTo 之后的操作是提交给 Event Bus,然后分发给相应 Event Handler 订阅者,来完成状态修改后确定的操作,这样一个领域对象状态的变更才算完成。

关于 Event Handler 保存领域状态操作,其实说简单也简单,说复杂会很复杂,对于它的实现一般会采用异步的方式,也就是说领域状态的保存操作不会延时领域中的业务操作,数据的一致性使用 Unit of Work,具体的领域状态保存用 Repository 实现。

梳理 Command 整个流程,你会发现一个关键词:状态(Status),在上一篇博文讲 REST 概念时,也有一个相似的概念:应用状态(Application State),REST 其中的一个含义就是状态转换,从客气端的发起请求开始,到服务端响应请求结束,应用状态在其过程中会进行不断的转换,请求响应的整个过程也就是应用状态转换的过程,对于 Command 处理流程来说,领域对象的状态和应用状态其实是相类似。我举一个例子,在 REST 架构风格中,应用状态是不会保存到服务端的,客户端发起请求(包含应用状态信息),服务端做出相应处理,此时的状态会转换成资源状态呈现给客户端,这就是表现层状态转换的意思,回到 Command 处理流程上,Command Bus 接收来自 UI 的请求,分发给相应的 Command Handler 进行处理,在处理过程中,就会对领域对象进行修改操作,但它不会保存修改之后的状态信息,而是交给 Event Handler 进行保存状态信息。

和 Command 相比,Query 的处理流程就简单很多了,Query Service 接收来自 UI 的查询请求,这个查询处理可以用各种方式实现,你可以使用 ORM,也可以直接写 SQL 代码,反正是:怎么能提高性能,就怎么来!返回的结果类型一般是 DTO(数据传输对象),根据 UI 进行设计,可以减少不必要的数据传输。

2. EDA-事件驱动架构

Event-Driven Architecture(事件驱动架构),来自解道的定义:

事件代表过去发生的事件,事件既是技术架构概念,也是业务概念,以事件为驱动的编程模型称为事件驱动架构 EDA。

EDA 架构的三个特性:

  1. 异步
  2. 实时
  3. 彻底解耦

EDA 架构的核心是基于消息的发布订阅模式,通过发布订阅模式实现事件的一对多灵活分发。消息消费方对发送方而言完全透明,消息发送方只管把消息发送到消息中间件,其它事情全部不用关心,由于消息中间件中的 MQ 等技术,即使发送消息时候,消息接收方不可用,但仍然可以正常发送,这才叫彻底解耦。其次一对多的发布订阅模式也是一个核心重点,对于消息的订阅方和订阅机制,可以在消息中间件灵活的进行配置和管理,而对于消息发送方和发送逻辑基本没有任何影响。

EDA 要求我们的是通过业务流程,首先要识别出有价值的业务事件,这些事件符合异步、实时和发布订阅等基本的事件特征;其次是对事件进行详细的分析和定义,对事件对应的消息格式进行定义,对事件的发布订阅机制进行定义等,最后才是基于消息事件模式的开发和测试等工作。

在上一篇博文中有讲到 SOA,我们知道分为客户端和服务端,客户端发起请求给服务端,服务端做出相应的响应,也就是说客户端是主动的,服务端是被动的,这种情况就会造成服务的分散,也就是说,我们一般在设计服务的时候,会根据客户端的响应而被迫的切分业务逻辑,最后导致的情况是各个业务模块所属的服务,被分散在各个业务系统中,这种设计就会导致很多问题的发生。而对于 EDA 架构来说,订阅者向 Event Bus 订阅事件,告诉事件总线我要这个,而 Event Bus 接收订阅后,并不会立即进行处理,而是想什么时候处理就什么时候处理,主动权在 Event Bus 手中,当 Event Bus 想进行处理的时候,一般是接受来自 Command Handler 的请求,然后就分别向指定订阅者发布通知,告诉它们我已经处理了,你们可以接着做下面的事了。

从上面的描述中,我们可以看到 SOA 和 EDA 的明显区别,相对于 SOA 来说,EDA 更加有利于领域的聚合,主动权在领域手中,我们就可以从容面对很多的情形,简单画了一张图:

另外,需要注意的一点,CQRS 可以结合 EDA,也可以不结合,但反过来对于 EDA 来说,则必须结合 CQRS 使用。

2.1 Domin Event-领域事件

领域事件和 Domain Service(领域服务)一样,同属于 DDD 战术模式,这部分内容在 IDDD 第八章有详细介绍,因为我还没学习到那部分,这边就简单说明一下。在 EDA 的定义中说到:事件代表过去发生的事件,换句话说它是代表已完成的事件,准备来说,还应该包含正在完成的事件,既然是属于 DDD 战术模式的一种,那在领域设计中必然有所用武之地。

我用大白话来描述下领域事件在领域中的作用:我们知道行军打仗需要做出抉择,也就是说,需要指挥部商量后下达作战命令,然后把命令交给各个负责的作战中心,有陆军、海军、空军、导弹部队等,它们是命令的实施者,而指挥部是命令的决策者,这个和领域事件是一样的,领域中处理一些业务逻辑后,就会对领域对象的状态做出一些改变,这个就相当于作战命令,然后根据作战命令分配的作战中心进行完成,也就是领域事件的订阅者去完成领域对象状态改变之后的操作,简单而言,领域事件就是领域中的“跑腿者”。

在上面 EDA 的介绍中,有这样的一段代码:backlogItem.commitTo(sprint);,用通用语言表述就是:待定项提交到冲刺,这是领域中完成的一个操作,由 Command Handler 进行委派完成,backlogItem 是一个聚合根对象,commitTo 是聚合根中的一个操作,这个操作完成后,backlogItem 聚合根对象的状态就会被修改了,那在 commitTo 中具体有怎么的操作呢?看下示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void commitTo(Sprint aSprint)
{
this.assertArgumentNotNull(aSprint, "Sprint must not be null.");
this.assertArgumentEquals(aSprint.tenantId(), this.tenantId(), "Sprint must be of same tenant.");
this.assertArgumentEquals(aSprint.productId(), this.productId(), "Sprint must be of same product.");

if (!this.isScheduledForRelease())
{
throw new IllegalStateException("Must be scheduled for release to commit to sprint.");
}

if (this.isCommittedToSprint())
{
if (!aSprint.sprintId().equals(this.sprintId()))
{
this.uncommitFromSprint();
}
}

this.elevateStatusWith(BacklogItemStatus.COMMITTED);

this.setSprintId(aSprint.sprintId());

DomainEventPublisher
.instance()
.publish(new BacklogItemCommitted(
this.tenantId(),
this.backlogItemId(),
this.sprintId()));
}

注意 commitTo 所处在 BacklogItem 聚合根内,前面都是对聚合根对象的一些状态操作,在后面我们会看到 DomainEventPublisher(领域事件发布者),BacklogItemCommitted 继承自 DomainEvent,BacklogItemCommitted 对应的领域事件,在 BacklogItemApplicationService 中进行订阅,一般是聚合根对象在初始化的时候。

根据上面这个代码示例,然后结合 EDA 的三个特性就可以很好理解了,首先对于领域事件的处理操作一般是异步完成,这样就不会影响聚合根中的其他业务操作,当领域事件发布的时候,会实时的告知订阅者进行处理,因为它不管订阅者的具体处理情况,订阅者和发布者的规范在 DomainEvent 中,而不是像接口定义和实现那么强制,所以,当领域事件发布的时候,就说明订阅者已经被告知并进行了处理,所以他们直接的关系可以彻底的解耦。

在之前的短消息项目中,我没用到领域事件,对它也不是很深入的了解,在后面的博文中,再进行详细说明。

2.2 Long-Running Process(Saga)-长时处理过程

来自 IDDD 中的定义:

事件驱动的、分布式的并行处理模式。

关于 Saga 的相关博文,国内几乎没有(netfocus 有一篇相关说明),长时处理过程,说明它是一个需要耗时、多任务并行的处理过程,那在领域中,什么时候会有它的用武之地呢?比如一个看似简单的业务逻辑,可能会涉及到领域中很复杂的业务操作,而且对于这些处理需要耗费很长的时间。

在电子商城提交一个订单操作,用户看来可能会非常简单,但在领域进行处理的时候,会涉及到订单操作、客户操作、商品操作、库存操作、消息通知操作等等,有些会进行及时的处理,但有些则不会,比如消息通知操作等等,我们可以把这个业务操作分离一下,对于一些耗时比较长的操作精拣一下,商品的减少对应库存的减少,减少之后会进行警戒线判断,如果低于警戒下,则会给库存管理人员发送消息,商品减少了对应的商品统计也要进行更新,客户购买之后也要进行发送消息通知,我们可以把这些用一个 Saga 进行处理,因为是基于事件驱动,所以一个 Saga 中会订阅多个事件,Saga 会对这些事件进行跟踪,对于一些事件处理失败,也要进行做出相应的弥补措施,当所有的操作完成后,Saga 会返回一个状态给领域,也许这个返回操作已经在开始的几天以后了。

说明:本图摘自 MSDN

上图描述的是:一个会议购买座位的业务过程,中间的 Order Process Manager 就是一个 Saga,在 CQRS 架构中的表现就是 Process Manager(过程管理),我们一般会用它处理多个聚合根交互的业务逻辑,比如 netfocus 博文中列举的 TransferProcessCommandHandlers 操作,还有上图中的购买座位业务操作,那我们应该怎么设计 Saga 呢?或者说在设计的时候,应该需要注意哪些地方呢?我大致列一下:

  • 将 Saga 尽量设计成组合任务的形式,你可以把它看作是一个任务的结合体,并对内部每个任务进行跟踪操作。
  • Saga 也可以用一组聚合的形式体现,也就是上面的图中示例。
  • 无状态处理,因为是基于事件驱动,状态信息会包裹在事件中,对于 Sage 整个处理过程来说,可以看作是无状态处理。
  • 可以适用于分布式设计。

2.3 Event Sourcing-事件溯源

字面上的理解:

事件即 Event,溯是一个动词,可以理解为追溯的意思,源代表原始、源头的意思,合起来表示一个事件追踪过程。

我们都知道在源代码管理的时候,可以使用 SVN、Git、CVS 进行对代码修改的跟踪操作,从一个代码文件的创建开始,我们可以查看各个版本对代码的修改情况,甚至可以指定版本进行还原操作,这就是对代码跟踪所带来的好处。而对于领域对象来说,我们也应该知晓其整个生命周期的演变过程,这样有利于查看并还原某一“时刻”的领域对象,在 EDA 架构中,对于领域对象状态的保存是通过领域事件进行完成,所以我们要想记录领域对象的状态记录,就需要把领域对象所经历的所有事件进行保存下来,这就是 Event Store(事件存储),这个东西就相当于 Git 代码服务器,存储所有领域对象所经历的事件,对于某一事件来说,可以看作是对应领域对象的“快照”。

总的来说,ES 事件溯源可以概括为两点:

  1. 记录
  2. 还原

最后,贴一张 CQRS、EDA、Saga、ES 结合图:

说明:本图来自 netfocus

CQRS 参考资料:

EDA 参考资料:


未完成的两点:

  • 3. CQRS Journey-微软示例项目
  • 4. ENode-netfocus 实践项目

本来还想把这两个项目分析一下,至少可以看懂一个业务流程,比如 Conference 项目中的 AssignSeats、ConferencePublish 等,ENode 项目中的 BankTransferSample 示例,但分析起来,真的有些吃力,有时候概念是一方面,实践又是另一方面,后面有时间理解了,再把这两点内容进行补充下。

这篇博文内容有点虚,大家借鉴有用的地方就行,也欢迎拍砖斧正。

上一篇:《IDDD 实现领域驱动设计-SOA、REST 和六边形架构

阅读目录:

  1. CQRS-命令查询职责分离

  2. EDA-事件驱动架构

    1. Domin Event-领域事件

    2. Long-Running Process(Saga)-长时处理过程

    3. Event Sourcing-事件溯源

  3. CQRS Journey-微软示例项目

  4. ENode-netfocus 实践项目

存在即是理由,每一种架构的产生都会有一种特定的场景,或者解决某一种实际应用问题,经验的累积促成了某一种架构的产生。

1. CQRS-命令查询职责分离

说明:本图摘自 MSDN

CQRS(Command & Query Responsibility Segregation)命令查询职责分离,和 REST 同属于架构风格,如果单纯理解 CQRS,是比较容易的,另一种方式解释就是,一个方法要么是执行某种动作的命令,要么是返回数据的查询,命令的体现是对系统状态的修改,而查询则不会,职责的分离更加有利于领域模型的提炼,系统的灵活性和可扩展性也得到进一步加强。

为什么要进行命令和查询职责分离?

如果你有时间,可以先阅读下上面几篇博文及相关评论。

我们都知道 Repository 的职责就是管理聚合根(Aggregate)对象,一般是一一对应关系,领域层中的业务逻辑要对某种聚合根对象进行操作,必须要通过 Repository,而应用层接受用户请求获取数据对象显示,也必须要通过 Repository 进行聚合根对象转换,这个一般没有涉及到领域业务操作,仅仅只是获取聚合根对象数据。领域层中的业务逻辑要求 Repository 实现对聚合根状态的管理,所以我们一般会在领域层 IRepository 接口中定义 Add、Update、GetById 等方法,然后在基础设施层中的 Repository 进行实现,而来自应用层的要求,需要获取聚合根对象数据,所以在 Repository 中还需要添加一些 GetList 等操作,而根据 IRepository 的接口契约,返回的类型必须是聚合根,而在这种场景中,是不需要获取聚合根对象的,只需要获取数据(DTO)就可以了。。。

我大致列一下上面描述中,所出现的一系列问题:

  1. Repository 职责变得飘忽不定。
  2. IRepository 会被污染,导致的结果是领域层也会被污染。
  3. Repository 会出现本不应该出现的 DTO 概念。
  4. Repository 会被大量 GetList 操作所吞没。
  5. Repository 最后会变得“人不像人,鬼不像鬼”。

如果你带着这些问题去理解 CQRS,就会有这样的感慨:“天哪,这简直就是老天派下的一个救星啊!”。

回到一开始的那张图上,看起来感觉很简单的样子,来自用户 UI 的请求分为 Query(查询)和 Command(命令),这些请求操作都会被 Service Interfaces(服务接口,只是一个统称)接收,然后再进行分发处理,对于命令操作会更新 Update Data store,因为读与写分离,为了保持数据的一致性,我们还需要把数据更新应用到 Read Data store。对于一般的应用系统来说,查询会占很大的比重,因为读与写分离了,所以我们可以针对查询进行进一步性能优化,而且还可以保持查询的灵活性和独立性,这种方式在应对大型业务系统来说是非常重要的,从这种层面上来说,CQRS 不用于 DDD 架构好像也是可以的,因为它是一种风格,并不局限于一种架构实现,所以你可以把它有价值的东西进行提炼,应用到合适的一个架构系统中也是可以的。

如果 CQRS 中包含有 Domain(领域)的概念,会是怎样的一种情形呢?

说明:本图摘自 AxonFramework

上面图中包含有很多的概念,但本质是和第一张图是一样的,只不过在其基础上进行了扩展和延伸,先列举一下所涉及的概念:

  • Command Bus(命令总线):图中没有,应该放在 Command Handler 之前,可以看作是 Command 发布者。
  • Command Handler(命令处理器):处理来自 Command Bus 分发的请求,可以看作是 Command 订阅者、处理者。
  • Event Bus(事件总线):一般在 Command Handler 完成之后,可以看作是 Event 发布者。
  • Event Handler(事件处理器):处理来自 Event Bus 分发的请求,可以看作是 Event 订阅者、处理者。
  • Event Store(事件存储):对应概念 Event Sourcing(事件溯源),可以用于事件回放处理,还原指定对象状态。

上面有些是 EDA(事件驱动架构)中的概念,这个在后面会有详细说明,我简单描述一下处理流程,首先抽离两个重要概念:Command(命令)和 Event(事件),Command 是一种命令的语气(本身就是命令的意思,呵呵),它的效果就是对某种对象状态的修改,Command Bus 收集来自 UI 的 Command 命令,并根据具体命令分发给具体的 Command Handler 进行处理,这时候就会产生一些领域操作,并对相应的领域对象进行修改,Command Handler 只是修改操作,并不会涉及到修改之后的操作(比如保存、事件发布等),Command Handler 完成之后并不表示这个 Command 命令就此结束,它需要把接下来的操作交给 Event Bus(完成之后的操作),并分发给相应的 Event Handler 订阅者进行处理,一般是数据保存、事件存储等。

我们来看 IDDD 中的一段代码(P126):

1
2
3
4
5
6
7
8
9
10
11
12
13
public void commitBacklogItemToSprint(
String aTenantId, String aBacklogItemId, String aSprintId) {

TenantId tenantId = new TenantId(aTenantId);

BacklogItem backlogItem = backlogItemRepository().backlogItemOfId(
tenantId, new BacklogItemId(aBacklogItemId));

Sprint sprint = sprintRepository().backlogItemOfId(
tenantId, new SprintId(aSprintId));

backlogItem.commitTo(sprint);
}

commitBacklogItemToSprint 就可以看作是一个 Command Handler,注意其命名(commitXXXXToXXXX),一眼看过去就是命令的意思,commitTo 之后的操作是提交给 Event Bus,然后分发给相应 Event Handler 订阅者,来完成状态修改后确定的操作,这样一个领域对象状态的变更才算完成。

关于 Event Handler 保存领域状态操作,其实说简单也简单,说复杂会很复杂,对于它的实现一般会采用异步的方式,也就是说领域状态的保存操作不会延时领域中的业务操作,数据的一致性使用 Unit of Work,具体的领域状态保存用 Repository 实现。

梳理 Command 整个流程,你会发现一个关键词:状态(Status),在上一篇博文讲 REST 概念时,也有一个相似的概念:应用状态(Application State),REST 其中的一个含义就是状态转换,从客气端的发起请求开始,到服务端响应请求结束,应用状态在其过程中会进行不断的转换,请求响应的整个过程也就是应用状态转换的过程,对于 Command 处理流程来说,领域对象的状态和应用状态其实是相类似。我举一个例子,在 REST 架构风格中,应用状态是不会保存到服务端的,客户端发起请求(包含应用状态信息),服务端做出相应处理,此时的状态会转换成资源状态呈现给客户端,这就是表现层状态转换的意思,回到 Command 处理流程上,Command Bus 接收来自 UI 的请求,分发给相应的 Command Handler 进行处理,在处理过程中,就会对领域对象进行修改操作,但它不会保存修改之后的状态信息,而是交给 Event Handler 进行保存状态信息。

和 Command 相比,Query 的处理流程就简单很多了,Query Service 接收来自 UI 的查询请求,这个查询处理可以用各种方式实现,你可以使用 ORM,也可以直接写 SQL 代码,反正是:怎么能提高性能,就怎么来!返回的结果类型一般是 DTO(数据传输对象),根据 UI 进行设计,可以减少不必要的数据传输。

2. EDA-事件驱动架构

Event-Driven Architecture(事件驱动架构),来自解道的定义:

事件代表过去发生的事件,事件既是技术架构概念,也是业务概念,以事件为驱动的编程模型称为事件驱动架构 EDA。

EDA 架构的三个特性:

  1. 异步
  2. 实时
  3. 彻底解耦

EDA 架构的核心是基于消息的发布订阅模式,通过发布订阅模式实现事件的一对多灵活分发。消息消费方对发送方而言完全透明,消息发送方只管把消息发送到消息中间件,其它事情全部不用关心,由于消息中间件中的 MQ 等技术,即使发送消息时候,消息接收方不可用,但仍然可以正常发送,这才叫彻底解耦。其次一对多的发布订阅模式也是一个核心重点,对于消息的订阅方和订阅机制,可以在消息中间件灵活的进行配置和管理,而对于消息发送方和发送逻辑基本没有任何影响。

EDA 要求我们的是通过业务流程,首先要识别出有价值的业务事件,这些事件符合异步、实时和发布订阅等基本的事件特征;其次是对事件进行详细的分析和定义,对事件对应的消息格式进行定义,对事件的发布订阅机制进行定义等,最后才是基于消息事件模式的开发和测试等工作。

在上一篇博文中有讲到 SOA,我们知道分为客户端和服务端,客户端发起请求给服务端,服务端做出相应的响应,也就是说客户端是主动的,服务端是被动的,这种情况就会造成服务的分散,也就是说,我们一般在设计服务的时候,会根据客户端的响应而被迫的切分业务逻辑,最后导致的情况是各个业务模块所属的服务,被分散在各个业务系统中,这种设计就会导致很多问题的发生。而对于 EDA 架构来说,订阅者向 Event Bus 订阅事件,告诉事件总线我要这个,而 Event Bus 接收订阅后,并不会立即进行处理,而是想什么时候处理就什么时候处理,主动权在 Event Bus 手中,当 Event Bus 想进行处理的时候,一般是接受来自 Command Handler 的请求,然后就分别向指定订阅者发布通知,告诉它们我已经处理了,你们可以接着做下面的事了。

从上面的描述中,我们可以看到 SOA 和 EDA 的明显区别,相对于 SOA 来说,EDA 更加有利于领域的聚合,主动权在领域手中,我们就可以从容面对很多的情形,简单画了一张图:

另外,需要注意的一点,CQRS 可以结合 EDA,也可以不结合,但反过来对于 EDA 来说,则必须结合 CQRS 使用。

2.1 Domin Event-领域事件

领域事件和 Domain Service(领域服务)一样,同属于 DDD 战术模式,这部分内容在 IDDD 第八章有详细介绍,因为我还没学习到那部分,这边就简单说明一下。在 EDA 的定义中说到:事件代表过去发生的事件,换句话说它是代表已完成的事件,准备来说,还应该包含正在完成的事件,既然是属于 DDD 战术模式的一种,那在领域设计中必然有所用武之地。

我用大白话来描述下领域事件在领域中的作用:我们知道行军打仗需要做出抉择,也就是说,需要指挥部商量后下达作战命令,然后把命令交给各个负责的作战中心,有陆军、海军、空军、导弹部队等,它们是命令的实施者,而指挥部是命令的决策者,这个和领域事件是一样的,领域中处理一些业务逻辑后,就会对领域对象的状态做出一些改变,这个就相当于作战命令,然后根据作战命令分配的作战中心进行完成,也就是领域事件的订阅者去完成领域对象状态改变之后的操作,简单而言,领域事件就是领域中的“跑腿者”。

在上面 EDA 的介绍中,有这样的一段代码:backlogItem.commitTo(sprint);,用通用语言表述就是:待定项提交到冲刺,这是领域中完成的一个操作,由 Command Handler 进行委派完成,backlogItem 是一个聚合根对象,commitTo 是聚合根中的一个操作,这个操作完成后,backlogItem 聚合根对象的状态就会被修改了,那在 commitTo 中具体有怎么的操作呢?看下示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void commitTo(Sprint aSprint)
{
this.assertArgumentNotNull(aSprint, "Sprint must not be null.");
this.assertArgumentEquals(aSprint.tenantId(), this.tenantId(), "Sprint must be of same tenant.");
this.assertArgumentEquals(aSprint.productId(), this.productId(), "Sprint must be of same product.");

if (!this.isScheduledForRelease())
{
throw new IllegalStateException("Must be scheduled for release to commit to sprint.");
}

if (this.isCommittedToSprint())
{
if (!aSprint.sprintId().equals(this.sprintId()))
{
this.uncommitFromSprint();
}
}

this.elevateStatusWith(BacklogItemStatus.COMMITTED);

this.setSprintId(aSprint.sprintId());

DomainEventPublisher
.instance()
.publish(new BacklogItemCommitted(
this.tenantId(),
this.backlogItemId(),
this.sprintId()));
}

注意 commitTo 所处在 BacklogItem 聚合根内,前面都是对聚合根对象的一些状态操作,在后面我们会看到 DomainEventPublisher(领域事件发布者),BacklogItemCommitted 继承自 DomainEvent,BacklogItemCommitted 对应的领域事件,在 BacklogItemApplicationService 中进行订阅,一般是聚合根对象在初始化的时候。

根据上面这个代码示例,然后结合 EDA 的三个特性就可以很好理解了,首先对于领域事件的处理操作一般是异步完成,这样就不会影响聚合根中的其他业务操作,当领域事件发布的时候,会实时的告知订阅者进行处理,因为它不管订阅者的具体处理情况,订阅者和发布者的规范在 DomainEvent 中,而不是像接口定义和实现那么强制,所以,当领域事件发布的时候,就说明订阅者已经被告知并进行了处理,所以他们直接的关系可以彻底的解耦。

在之前的短消息项目中,我没用到领域事件,对它也不是很深入的了解,在后面的博文中,再进行详细说明。

2.2 Long-Running Process(Saga)-长时处理过程

来自 IDDD 中的定义:

事件驱动的、分布式的并行处理模式。

关于 Saga 的相关博文,国内几乎没有(netfocus 有一篇相关说明),长时处理过程,说明它是一个需要耗时、多任务并行的处理过程,那在领域中,什么时候会有它的用武之地呢?比如一个看似简单的业务逻辑,可能会涉及到领域中很复杂的业务操作,而且对于这些处理需要耗费很长的时间。

在电子商城提交一个订单操作,用户看来可能会非常简单,但在领域进行处理的时候,会涉及到订单操作、客户操作、商品操作、库存操作、消息通知操作等等,有些会进行及时的处理,但有些则不会,比如消息通知操作等等,我们可以把这个业务操作分离一下,对于一些耗时比较长的操作精拣一下,商品的减少对应库存的减少,减少之后会进行警戒线判断,如果低于警戒下,则会给库存管理人员发送消息,商品减少了对应的商品统计也要进行更新,客户购买之后也要进行发送消息通知,我们可以把这些用一个 Saga 进行处理,因为是基于事件驱动,所以一个 Saga 中会订阅多个事件,Saga 会对这些事件进行跟踪,对于一些事件处理失败,也要进行做出相应的弥补措施,当所有的操作完成后,Saga 会返回一个状态给领域,也许这个返回操作已经在开始的几天以后了。

说明:本图摘自 MSDN

上图描述的是:一个会议购买座位的业务过程,中间的 Order Process Manager 就是一个 Saga,在 CQRS 架构中的表现就是 Process Manager(过程管理),我们一般会用它处理多个聚合根交互的业务逻辑,比如 netfocus 博文中列举的 TransferProcessCommandHandlers 操作,还有上图中的购买座位业务操作,那我们应该怎么设计 Saga 呢?或者说在设计的时候,应该需要注意哪些地方呢?我大致列一下:

  • 将 Saga 尽量设计成组合任务的形式,你可以把它看作是一个任务的结合体,并对内部每个任务进行跟踪操作。
  • Saga 也可以用一组聚合的形式体现,也就是上面的图中示例。
  • 无状态处理,因为是基于事件驱动,状态信息会包裹在事件中,对于 Sage 整个处理过程来说,可以看作是无状态处理。
  • 可以适用于分布式设计。

2.3 Event Sourcing-事件溯源

字面上的理解:

事件即 Event,溯是一个动词,可以理解为追溯的意思,源代表原始、源头的意思,合起来表示一个事件追踪过程。

我们都知道在源代码管理的时候,可以使用 SVN、Git、CVS 进行对代码修改的跟踪操作,从一个代码文件的创建开始,我们可以查看各个版本对代码的修改情况,甚至可以指定版本进行还原操作,这就是对代码跟踪所带来的好处。而对于领域对象来说,我们也应该知晓其整个生命周期的演变过程,这样有利于查看并还原某一“时刻”的领域对象,在 EDA 架构中,对于领域对象状态的保存是通过领域事件进行完成,所以我们要想记录领域对象的状态记录,就需要把领域对象所经历的所有事件进行保存下来,这就是 Event Store(事件存储),这个东西就相当于 Git 代码服务器,存储所有领域对象所经历的事件,对于某一事件来说,可以看作是对应领域对象的“快照”。

总的来说,ES 事件溯源可以概括为两点:

  1. 记录
  2. 还原

最后,贴一张 CQRS、EDA、Saga、ES 结合图:

说明:本图来自 netfocus

CQRS 参考资料:

EDA 参考资料:


未完成的两点:

  • 3. CQRS Journey-微软示例项目
  • 4. ENode-netfocus 实践项目

本来还想把这两个项目分析一下,至少可以看懂一个业务流程,比如 Conference 项目中的 AssignSeats、ConferencePublish 等,ENode 项目中的 BankTransferSample 示例,但分析起来,真的有些吃力,有时候概念是一方面,实践又是另一方面,后面有时间理解了,再把这两点内容进行补充下。

这篇博文内容有点虚,大家借鉴有用的地方就行,也欢迎拍砖斧正。

CRC32/CRC16算法C#中的实现 - uusystem - 博客园

Excerpt

CRC32算法 CRC16算法 更多内容请访问 www.uusystem.com


CRC32算法

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<span> 1</span> <span>using</span><span> System;
</span><span> 2</span> <span>using</span><span> System.Collections.Generic;
</span><span> 3</span> <span>using</span><span> System.Text;
</span><span> 4</span> <span>using</span><span> System.IO;
</span><span> 5</span>
<span> 6</span> <span>namespace</span><span> GetCRC32
</span><span> 7</span> <span>{
</span><span> 8</span> <span>class</span><span> CRC32Cls
</span><span> 9</span> <span> {
</span><span>10</span> <span>protected</span> <span>ulong</span><span>[] Crc32Table;
</span><span>11</span> <span>//</span><span>生成CRC32码表</span>
<span>12</span> <span>public</span> <span>void</span><span> GetCRC32Table()
</span><span>13</span> <span> {
</span><span>14</span> <span>ulong</span><span> Crc;
</span><span>15</span> Crc32Table = <span>new</span> <span>ulong</span>[<span>256</span><span>];
</span><span>16</span> <span>int</span><span> i,j;
</span><span>17</span> <span>for</span>(i = <span>0</span>;i &lt; <span>256</span>; i++<span>)
</span><span>18</span> <span> {
</span><span>19</span> Crc = (<span>ulong</span><span>)i;
</span><span>20</span> <span>for</span> (j = <span>8</span>; j &gt; <span>0</span>; j--<span>)
</span><span>21</span> <span> {
</span><span>22</span> <span>if</span> ((Crc &amp; <span>1</span>) == <span>1</span><span>)
</span><span>23</span> Crc = (Crc &gt;&gt; <span>1</span>) ^ <span>0xEDB88320</span><span>;
</span><span>24</span> <span>else</span>
<span>25</span> Crc &gt;&gt;= <span>1</span><span>;
</span><span>26</span> <span> }
</span><span>27</span> Crc32Table[i] =<span> Crc;
</span><span>28</span> <span> }
</span><span>29</span> <span> }
</span><span>30</span>
<span>31</span> <span>//</span><span>获取字符串的CRC32校验值</span>
<span>32</span> <span>public</span> <span>ulong</span> GetCRC32Str(<span>string</span><span> sInputString)
</span><span>33</span> <span> {
</span><span>34</span> <span>//</span><span>生成码表</span>
<span>35</span> <span> GetCRC32Table();
</span><span>36</span> <span>byte</span>[] buffer =<span> System.Text.ASCIIEncoding.ASCII.GetBytes(sInputString);
</span><span>37</span> <span>ulong</span> value = <span>0xffffffff</span><span>;
</span><span>38</span> <span>int</span> len =<span> buffer.Length;
</span><span>39</span> <span>for</span> (<span>int</span> i = <span>0</span>; i &lt; len; i++<span>)
</span><span>40</span> <span> {
</span><span>41</span> value = (value &gt;&gt; <span>8</span>) ^ Crc32Table[(value &amp; <span>0xFF</span>)^<span> buffer[i]];
</span><span>42</span> <span> }
</span><span>43</span> <span>return</span> value ^ <span>0xffffffff</span><span>;
</span><span>44</span> <span> }
</span><span>45</span> <span> }
</span><span>46</span> }

复制代码

CRC16算法

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<span> 1</span>          <span>public</span> <span>static</span> <span>byte</span>[] CRC16(<span>string</span><span> sInputString)
</span><span> 2</span> <span> {
</span><span> 3</span> <span>byte</span>[] data =<span> System.Text.ASCIIEncoding.ASCII.GetBytes(sInputString);
</span><span> 4</span> <span>int</span> len =<span> data.Length;
</span><span> 5</span> <span>if</span> (len &gt; <span>0</span><span>)
</span><span> 6</span> <span> {
</span><span> 7</span> <span>ushort</span> crc = <span>0xFFFF</span><span>;
</span><span> 8</span>
<span> 9</span> <span>for</span> (<span>int</span> i = <span>0</span>; i &lt; len; i++<span>)
</span><span>10</span> <span> {
</span><span>11</span> crc = (<span>ushort</span>)(crc ^<span> (data[i]));
</span><span>12</span> <span>for</span> (<span>int</span> j = <span>0</span>; j &lt; <span>8</span>; j++<span>)
</span><span>13</span> <span> {
</span><span>14</span> crc = (crc &amp; <span>1</span>) != <span>0</span> ? (<span>ushort</span>)((crc &gt;&gt; <span>1</span>) ^ <span>0xA001</span>) : (<span>ushort</span>)(crc &gt;&gt; <span>1</span><span>);
</span><span>15</span> <span> }
</span><span>16</span> <span> }
</span><span>17</span> <span>byte</span> hi = (<span>byte</span>)((crc &amp; <span>0xFF00</span>) &gt;&gt; <span>8</span>); <span>//</span><span>高位置</span>
<span>18</span> <span>byte</span> lo = (<span>byte</span>)(crc &amp; <span>0x00FF</span>); <span>//</span><span>低位置</span>
<span>19</span>
<span>20</span> <span>return</span> <span>new</span> <span>byte</span><span>[] { hi, lo };
</span><span>21</span> <span> }
</span><span>22</span> <span>return</span> <span>new</span> <span>byte</span>[] { <span>0</span>, <span>0</span><span> };
</span><span>23</span> <span> }
</span><span>24</span>
<span>25</span> <span>//</span><span> ASCII码转为字符串</span>
<span>26</span> <span>public</span> <span>static</span> <span>string</span> ByteToString(<span>byte</span>[] arr, <span>bool</span><span> isReverse)
</span><span>27</span> <span> {
</span><span>28</span> <span>try</span>
<span>29</span> <span> {
</span><span>30</span> <span>byte</span> hi = arr[<span>0</span>], lo = arr[<span>1</span><span>];
</span><span>31</span> <span>return</span> Convert.ToString(isReverse ? hi + lo * <span>0x100</span> : hi * <span>0x100</span> + lo, <span>16</span>).ToUpper().PadLeft(<span>4</span>, <span>'</span><span>0</span><span>'</span><span>);
</span><span>32</span> <span> }
</span><span>33</span> <span>catch</span> (Exception ex) { <span>throw</span><span> (ex); }
</span><span>34</span> }

复制代码

更多内容请访问 www.uusystem.com

posted @ 2018-06-23 13:12  uusystem  阅读(6727)  评论()  编辑  收藏  举报

搜集整理的代码会是很不错的文章,花了一天时间,搜索到最后居然出来一篇叫做”C# 与 C++ 数据类型对照表”的文章.几乎囊括掉和大部分的数据了,太打击我了. 本文中有部分的数据没有测试.也有一些不错的是看了上百篇网文对比整理得来的.希望有帮助.

//C++中的DLL函数原型为
        //extern “C” __declspec(dllexport) bool 方法名一(const char* 变量名1, unsigned char* 变量名2)
        //extern “C” __declspec(dllexport) bool 方法名二(const unsigned char* 变量名1, char* 变量名2)

//C#调用C++的DLL搜集整理的所有数据类型转换方式,可能会有重复或者多种方案,自己多测试
        //c++:HANDLE(void   *)          —-    c#:System.IntPtr 
        //c++:Byte(unsigned   char)     —-    c#:System.Byte 
        //c++:SHORT(short)              —-    c#:System.Int16 
        //c++:WORD(unsigned   short)    —-    c#:System.UInt16 
        //c++:INT(int)                  —-    c#:System.Int16
        //c++:INT(int)                  —-    c#:System.Int32 
        //c++:UINT(unsigned   int)      —-    c#:System.UInt16
        //c++:UINT(unsigned   int)      —-    c#:System.UInt32
        //c++:LONG(long)                —-    c#:System.Int32 
        //c++:ULONG(unsigned   long)    —-    c#:System.UInt32 
        //c++:DWORD(unsigned   long)    —-    c#:System.UInt32 
        //c++:DECIMAL                   —-    c#:System.Decimal 
        //c++:BOOL(long)                —-    c#:System.Boolean 
        //c++:CHAR(char)                —-    c#:System.Char 
        //c++:LPSTR(char   *)           —-    c#:System.String 
        //c++:LPWSTR(wchar_t   *)       —-    c#:System.String 
        //c++:LPCSTR(const   char   *)  —-    c#:System.String 
        //c++:LPCWSTR(const   wchar_t   *)      —-    c#:System.String 
        //c++:PCAHR(char   *)   —-    c#:System.String 
        //c++:BSTR              —-    c#:System.String 
        //c++:FLOAT(float)      —-    c#:System.Single 
        //c++:DOUBLE(double)    —-    c#:System.Double 
        //c++:VARIANT           —-    c#:System.Object 
        //c++:PBYTE(byte   *)   —-    c#:System.Byte[]

//c++:BSTR      —-    c#:StringBuilder
        //c++:LPCTSTR   —-    c#:StringBuilder
        //c++:LPCTSTR   —-    c#:string
        //c++:LPTSTR    —-    c#:[MarshalAs(UnmanagedType.LPTStr)] string 
        //c++:LPTSTR 输出变量名    —-    c#:StringBuilder 输出变量名
        //c++:LPCWSTR   —-    c#:IntPtr
        //c++:BOOL      —-    c#:bool   
        //c++:HMODULE   —-    c#:IntPtr    
        //c++:HINSTANCE —-    c#:IntPtr 
        //c++:结构体    —-    c#:public struct 结构体{}; 
        //c++:结构体 **变量名   —-    c#:out 变量名   //C#中提前申明一个结构体实例化后的变量名
        //c++:结构体 &变量名    —-    c#:ref 结构体 变量名 //c++:WORD      —-    c#:ushort
        //c++:DWORD     —-    c#:uint
        //c++:DWORD     —-    c#:int

//c++:UCHAR     —-    c#:int
        //c++:UCHAR     —-    c#:byte
        //c++:UCHAR*    —-    c#:string
        //c++:UCHAR*    —-    c#:IntPtr

//c++:GUID      —-    c#:Guid
        //c++:Handle    —-    c#:IntPtr
        //c++:HWND      —-    c#:IntPtr
        //c++:DWORD     —-    c#:int
        //c++:COLORREF  —-    c#:uint

//c++:unsigned char     —-    c#:byte
        //c++:unsigned char *   —-    c#:ref byte
        //c++:unsigned char *   —-    c#:[MarshalAs(UnmanagedType.LPArray)] byte[]
        //c++:unsigned char *   —-    c#:[MarshalAs(UnmanagedType.LPArray)] Intptr

//c++:unsigned char &   —-    c#:ref byte
        //c++:unsigned char 变量名      —-    c#:byte 变量名
        //c++:unsigned short 变量名     —-    c#:ushort 变量名
        //c++:unsigned int 变量名       —-    c#:uint 变量名
        //c++:unsigned long 变量名      —-    c#:ulong 变量名

//c++:char 变量名       —-    c#:byte 变量名   //C++中一个字符用一个字节表示,C#中一个字符用两个字节表示
        //c++:char 数组名[数组大小]     —-    c#:MarshalAs(UnmanagedType.ByValTStr, SizeConst = 数组大小)]        public string 数组名; ushort

//c++:char *            —-    c#:string       //传入参数
        //c++:char *            —-    c#:StringBuilder//传出参数
        //c++:char *变量名      —-    c#:ref string 变量名
        //c++:char *输入变量名  —-    c#:string 输入变量名
        //c++:char *输出变量名  —-    c#:[MarshalAs(UnmanagedType.LPStr)] StringBuilder 输出变量名

//c++:char **           —-    c#:string
        //c++:char **变量名     —-    c#:ref string 变量名
        //c++:const char *      —-    c#:string
        //c++:char[]            —-    c#:string
        //c++:char 变量名[数组大小]     —-    c#:[MarshalAs(UnmanagedType.ByValTStr,SizeConst=数组大小)] public string 变量名;

//c++:struct 结构体名 *变量名   —-    c#:ref 结构体名 变量名
        //c++:委托 变量名   —-    c#:委托 变量名

//c++:int       —-    c#:int
        //c++:int       —-    c#:ref int
        //c++:int &     —-    c#:ref int
        //c++:int *     —-    c#:ref int      //C#中调用前需定义int 变量名 = 0;

//c++:*int      —-    c#:IntPtr
        //c++:int32 PIPTR *     —-    c#:int32[]
        //c++:float PIPTR *     —-    c#:float[]

//c++:double** 数组名          —-    c#:ref double 数组名
        //c++:double*[] 数组名          —-    c#:ref double 数组名
        //c++:long          —-    c#:int
        //c++:ulong         —-    c#:int

                //c++:UINT8 *       —-    c#:ref byte       //C#中调用前需定义byte 变量名 = new byte();       

//c++:handle    —-    c#:IntPtr
        //c++:hwnd      —-    c#:IntPtr

                        //c++:void *    —-    c#:IntPtr        
        //c++:void * user_obj_param    —-    c#:IntPtr user_obj_param
        //c++:void * 对象名称    —-    c#:([MarshalAs(UnmanagedType.AsAny)]Object 对象名称

                //c++:char, INT8, SBYTE, CHAR                               —-    c#:System.SByte  
        //c++:short, short int, INT16, SHORT                        —-    c#:System.Int16  
        //c++:int, long, long int, INT32, LONG32, BOOL , INT        —-    c#:System.Int32  
        //c++:__int64, INT64, LONGLONG                              —-    c#:System.Int64  
        //c++:unsigned char, UINT8, UCHAR , BYTE                    —-    c#:System.Byte  
        //c++:unsigned short, UINT16, USHORT, WORD, ATOM, WCHAR , __wchar_t             —-    c#:System.UInt16  
        //c++:unsigned, unsigned int, UINT32, ULONG32, DWORD32, ULONG, DWORD, UINT      —-    c#:System.UInt32  
        //c++:unsigned __int64, UINT64, DWORDLONG, ULONGLONG                            —-    c#:System.UInt64  
        //c++:float, FLOAT                                                              —-    c#:System.Single  
        //c++:double, long double, DOUBLE                                               —-    c#:System.Double 

//Win32 Types        —-  CLR Type //Struct需要在C#里重新定义一个Struct
        //CallBack回调函数需要封装在一个委托里,delegate static extern int FunCallBack(string str);

//unsigned char** ppImage替换成IntPtr ppImage
        //int& nWidth替换成ref int nWidth
        //int*, int&, 则都可用 ref int 对应
        //双针指类型参数,可以用 ref IntPtr
        //函数指针使用c++: typedef double (*fun_type1)(double); 对应 c#:public delegate double  fun_type1(double);
        //char* 的操作c++: char*; 对应 c#:StringBuilder;
        //c#中使用指针:在需要使用指针的地方 加 unsafe

//unsigned   char对应public   byte
        /*
         * typedef void (*CALLBACKFUN1W)(wchar_t*, void* pArg);
         * typedef void (*CALLBACKFUN1A)(char*, void* pArg);
         * bool BIOPRINT_SENSOR_API dllFun1(CALLBACKFUN1 pCallbackFun1, void* pArg);
         * 调用方式为
         * [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
         * public delegate void CallbackFunc1([MarshalAs(UnmanagedType.LPWStr)] StringBuilder strName, IntPtr pArg);
         * 
         * 
         */

C#调用C/C++DLL收取中文字符处理 - ccqin - 博客园

Excerpt

C#可以通过P/Invoke调用C/C++写的DLL,一般在从DLL接收字符串时比较麻烦,本人在某个项目中就遇到这个问题, 从DLL收读取字符串时遇到中文乱码,这里总结一下C#收取字符串时的处理。 C/C++字符串一般通过char* 或wchar_t来表示,char表示的是ANSCII字符串,


C#可以通过P/Invoke调用C/C++写的DLL,一般在从DLL接收字符串时比较麻烦,本人在某个项目中就遇到这个问题,

从DLL收读取字符串时遇到中文乱码,这里总结一下C#收取字符串时的处理。

C/C++字符串一般通过char* 或wchar_t*来表示,char*表示的是ANSCII字符串, wchar_t*表示Unicode字符串,Unicode字符串在C/C++中一个字符占用两个字节,ANSII字符串一个字符占用一个字节(中文占用两个字节), 如果C++的接口声明为char*的入口参数时,C#需要使用byte[] 数组来表示字节缓冲, 注意C#中char 是占用两个字节的。

比如C++中接口原型

void testStr(char *buf, int size);

在C#中作如下的原型声明

[Dllimport(“yourdll.dll”)]

extern int testStr(IntPtr buf, int size);

作如下调用

byte[] buf = new byte[LEN];

int len = testStr(Marshal.UnsafeAddrOfPinnedArrayElement(buf, 0), buf.length);

byte[] cvtBuf = new byte[LEN];

//重点在于这里的转换 这里是将默认的编码ANSII转换为unicode编码。适用于char* ->unicode

//如果是_TCHAR或wchar_t表示的字符串,不妨用char[]作为缓冲传入

cvtBuf = Encoding.Convert(Encoding.Default, Encoding.Unicode, buf , 0, len-1);

string recvStr = Marshal.PtrToStringAuto(Marshal.UnsafeAddrOfPinnedArrayElement(cvtBuf , 0));

至此中文字符传入到C#就能正常地显示了。

C++中有个编码转换比较有用的函数WideCharToMultiByte和MultiByteToWideChar,可以在MSDN中找到它.

它也是从C/C++动态库通过JNI向Java传字符串解决中文乱码的解决之道。

   前面介绍的所有专题都是基于经典的领域驱动实现的,然而,领域驱动除了经典的实现外,还可以基于CQRS模式来进行实现。本专题将全面剖析如何基于CQRS模式(Command Query Responsibility Segregation,命令查询职责分离)来实现领域驱动设计。

   在介绍具体的实现之前,对于之前不了解CQRS的朋友来说,首先第一个问题应该是:什么是CQRS啊?你倒是详细介绍完CQRS后再介绍具体实现啊?既然大家会有这样的问题,所以本专题首先全面介绍下什么是CQRS。

2.1 CQRS发展历程

  在介绍CQRS之前,我觉得有必要先了解一下CQS(即Command Query Separation,命令查询分离)模式。我们可以理解CQRS是在DDD的实践中基于CQS理论而出现的一种体系结构模式。CQS模式最早由软件大师Bertrand Meyer(Eiffel语言之父,面向对象开-闭原则OCP提出者)提出,他认为,对象的行为仅有两种:命令和查询,不存在第三种情况。根据CQS的思想,任何方法都可以拆分为命令和查询两部分。例如下面的方法:

复制代码

    private int \_number = 0;
    public int Add(int factor)
    {
        \_number += factor;
        return \_number;
    }

复制代码

  在上面的方法中,执行了一个命令,即对变量_number加上一个因子factor,同时又执行了一个查询,即查询返回_number的值。根据CQS的思想,该方法可以拆成Command和Query两个方法:

复制代码

复制代码

private int _number = 0;
private void AddCommand(int factor)
{
_number += factor;
}

private int QueryValue()
{
return _number;
}

复制代码

复制代码

  命令和查询分离使得我们可以更好地把握对象的细节,更好地理解哪些操作会改变系统的状态。从而使的系统具有更好的扩展性,并获得更好的性能。

  CQRS根据CQS思想,并结合领域驱动设计思想,由Grey Young在CQRS, Task Based UIs, Event Sourcing agh! 这篇文章中提出。CQRS将之前只需要定义一个对象拆分成两个对象,分离的原则按照对象中方法是执行命令还是执行查询来进行拆分的。

2.2 CQRS结构

由前面的介绍可知,采用CQRS模式实现的系统结构可以分为两个部分:命令部分和查询部分。其系统结构如下图所示:

  从上面系统结构图可以发现,采用CQRS实现的领域驱动设计与经典DDD有很大的不同。采用CQRS实现的DDD结构大体分为两部分,查询部分和命令部分,并且维护着两个数据库实例,一个专门用来进行查询,另一个用来响应命令操作。然后通过EventHandler操作将命令改变的状态同步到用来查询的数据库实例中。从这个描述中,我们可能会联想到数据库级别主从读写分离。然而数据读写分离是在数据库层面来实现读写分离的机制,而CQRS是在业务逻辑层面来实现读写分离机制。两者是站在两个不同的层面对读写分离进行实现的。

   前面我们已经详细介绍了CQRS模式,相信经过前面的介绍,大家对CQRS模式一定有一些了解了,但为什么要引入CQRS模式呢?

  在传统的实现中,对DB执行增、删、改、查所有操作都会放在对应的仓储中,并且这些操作都公用一份领域实体对象。对于一些简单的系统,使用传统的设计方式并没有什么不妥,但在一些大型复杂的系统中,传统的实现方式也会存在一些问题:

  • 使用同一个领域实体来进行数据读写可能会遇到资源竞争的情况。所以经常要处理锁的问题,在写入数据的时候,需要加锁,读取数据的时候需要判断是否允许脏读。这样使得系统的逻辑性和复杂性增加,并会影响系统的吞吐量。
  • 在大数据量同时进行读写的情况下,可能出现性能的瓶颈。
  • 使用同一个领域实体来进行数据库读写可能会太粗糙。在大多是情况下,比如编辑操作,可能只需要更新个别字段,这时却需要将整个对象都穿进去。还有在查询的时候,表现层可能只需要个别字段,但需要查询和返回整个领域实体,再把领域实体对象转换从对应的DTO对象。
  • 读写操作都耦合在一起,不利于对问题的跟踪和分析,如果读写操作分离的话,如果是由于状态改变的问题就只需要去分析写操作相关的逻辑就可以了,如果是关于数据的不正确,则只需要关心查询操作的相关逻辑即可。

  针对上面的这些问题,采用CQRS模式的系统都可以解决。由于CQRS模式中将查询和命令进行分析,所以使得两者分工明确,各自负责不同的部分,并且在业务上将命令和查询分离能够提高系统的性能和可扩展性。既然CQRS这么好,那是不是所有系统都应该基于CQRS模式去实现呢?显然不是的,CQRS也有其使用场景:

  1. 系统的业务逻辑比较复杂的情况下。因为本来业务逻辑就比较复杂了,如果再把命令操作和查询操作绑定同一个业务实体的话,这样会导致后期的需求变更难于进行扩展下去。
  2. 需要对系统中查询性能和写入性能分开进行优化的情况下,尤其读/写比例非常高的情况下。例如,在很多系统中读操作的请求数远大于写操作,此时,就可以考虑将写操作抽离出来进行单独扩展。
  3. 系统在将来随着时间不断变化的情况下。

  然而,CQRS也有其不适用的场景:

  • 业务逻辑比较简单的情况下,此时采用CQRS反而会把系统搞的复杂。
  • 系统用户访问量都比较小的情况下,并且需求以后不怎么会变更的情况下。针对这样的系统,完全可以用传统的实现方式快速将系统实现出来,没必要引入CQRS来增加系统的复杂度。

  在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回,这个方面的操作相对比较简单。而命令方面,是通过发送具体Command,接着由CommandBus来分发到具体的CommandHandle来进行处理,CommandHandle在进行处理时,并没有直接将对象的状态保存到外部持久化结构中,而仅仅是从领域对象中获得产生的一系列领域事件,并将这些事件保存到Event Store中,同时将事件发布到事件总线Event Bus进行下一步处理;接着Event Bus同样进行协调,将具体的事件交给具体的Event Handle进行处理,最后Event Handler再把对象的状态保存到对应Query数据库中。

上面过程正是CQRS系统中的调用顺序。从中可以发现,采用CQRS实现的系统存在两个数据库实例,一个是Event Store,该数据库实例用来保存领域对象中发生的一系列的领域事件,简单来说就是保存领域事件的数据库。另一个是Query Database,该数据库就是存储具体的领域对象数据的,查询操作可以直接对该数据库进行查询。由于,我们在Event Store中记录领域对象发生的所有事件,这样我们就可以通过查询该数据库实例来获得领域对象之前的所有状态了。所谓Event Sourcing,就是指的的是:通过事件追溯对象的起源,它允许通过记录下来的事件,将领域模型恢复到之前的任意一个时间点。

  通过Event来记录领域对象所发生的所有状态,这样利用系统的跟踪并能够方便地回滚到某一历史状态。经过上面的描述,感觉事件溯源一般用于系统的维护。例如,我们可以设计一个同步服务,该服务程序从Event Store数据库查询出领域对象的历史数据,从而打印生成一个历史报表,如历史价格报表等。但正是的CQRS系统中如何使用Event Sourcing的呢?

  在前面介绍CQRS系统的调用顺序中,我们讲到,由Event Handler将对象的状态保存到对应的Query数据库中,这里有一个问题,对象的状态怎么获得呢?对象状态的获得正是由Event sourcing机制来获得,因为用户发送的仅仅是Command,Command中并不包含对象的状态数据,所以此时需要通过Event Sourcing机制来查询Event Store来还原对象的状态,还原根据就是对应的Id,该Id是通过命令传入的。Event Sourcing的调用需要放在CommandHandle中,因为CommandHandle需要先获得领域对象,这样才能把领域对象与命令对象来进行对比,从而获得领域对象中产生的一系列领域事件。

   然而,当随着时间的推移,领域事件变得越来越多时,通过Event Sourcing机制来还原对象状态的过程会非常耗时,因为每一次都需要从最早发生的事件开始。那有没有好的一个方式来解决这个问题呢?答案是肯定的,即在Event Sourcing中引入快照(Snapshots)实现。实现原理就是——没产生N个领域事件,则对对象做一次快照。这样,领域对象溯源的时候,可以先从快照中获得最近一次的快照,然后再逐个应用快照之后所有产生的领域事件,而不需要每次溯源都从最开始的事件开始对对象重建,这样就大大加快了对象重建的过程。

  前面介绍了那么多CQRS的内容,下面就具体通过一个例子来演示下CQRS系统的实现。

  命令部分的实现

复制代码

复制代码

// 应用程序初始化操作,将依赖的对象通过依赖注入框架StructureMap进行注入
public sealed class ServiceLocator
{
    private static readonly ICommandBus \_commandBus;
    private static readonly IStorage \_queryStorage;
    private static readonly bool IsInitialized;
    private static readonly object LockThis = new object();
    
    static ServiceLocator()
    {
        if (!IsInitialized)
        {
            lock (LockThis)
            {
                // 依赖注入
                ContainerBootstrapper.BootstrapStructureMap();

                \_commandBus = ContainerBootstrapper.Container.GetInstance<ICommandBus>();
                \_queryStorage = ContainerBootstrapper.Container.GetInstance<IStorage>();
                IsInitialized = true;
            }
        }
    }

    public static ICommandBus CommandBus
    {
        get { return \_commandBus; }
    }

    public static IStorage QueryStorage
    {
        get { return \_queryStorage; }
    }
}

class ContainerBootstrapper
{
    private static Container \_container;
    public static void BootstrapStructureMap()
    {
        \_container = new Container(x =>
        {
            x.For(typeof (IDomainRepository<>)).Singleton().Use(typeof (DomainRepository<>));
            x.For<IEventStorage>().Singleton().Use<InMemoryEventStorage>();
            x.For<IEventBus>().Use<EventBus>();
            x.For<ICommandBus>().Use<CommandBus>();
            x.For<IStorage>().Use<InMemoryStorage>();
            x.For<IEventHandlerFactory>().Use<StructureMapEventHandlerFactory>();
            x.For<ICommandHandlerFactory>().Use<StructureMapCommandHandlerFactory>();
        });
    }

    public static Container Container 
    {
        get { return \_container;}
    }
}

public class HomeController : Controller
{
[HttpPost]
public ActionResult Add(DiaryItemDto item)
{
// 发布CreateItemCommand到CommandBus中
ServiceLocator.CommandBus.Send(new CreateItemCommand(Guid.NewGuid(), item.Title, item.Description, -1, item.From, item.To));

        return RedirectToAction("Index");
    }    
}

// CommandBus 的实现
public class CommandBus : ICommandBus
{
private readonly ICommandHandlerFactory _commandHandlerFactory;

    public CommandBus(ICommandHandlerFactory commandHandlerFactory)
    {
        \_commandHandlerFactory = commandHandlerFactory;
    }

    public void Send<T>(T command) where T : Command
    {
        // 获得对应的CommandHandle来对命令进行处理
        var handlers = \_commandHandlerFactory.GetHandlers<T>();

        foreach (var handler in handlers)
        {
            // 处理命令
            handler.Execute(command);
        }
    }       
}

// 对CreateItemCommand处理类
public class CreateItemCommandHandler : ICommandHandler
{
private readonly IDomainRepository _domainRepository;

    public CreateItemCommandHandler(IDomainRepository<DiaryItem> domainRepository)
    {
        \_domainRepository = domainRepository;
    }

    // 具体处理逻辑
    public void Execute(CreateItemCommand command)
    {
        if (command == null)
        {
            throw new ArgumentNullException("command");
        }
        if (\_domainRepository == null)
        {
            throw new InvalidOperationException("domainRepository is not initialized.");
        }

        var aggregate = new DiaryItem(command.ID, command.Title, command.Description, command.From, command.To)
        {
            Version = -1
        };

        // 将对应的领域实体进行保存
        \_domainRepository.Save(aggregate, aggregate.Version);
    }
}

// IDomainRepository的实现类
public class DomainRepository : IDomainRepository where T : AggregateRoot, new()
{
// 并没有直接对领域实体进行保存,而是先保存领域事件进EventStore,然后在Publish事件到EventBus进行处理
// 然后EventBus把事件分配给对应的事件处理器进行处理,由事件处理器来把领域对象保存到QueryDatabase中
public void Save(AggregateRoot aggregate, int expectedVersion)
{
if (aggregate.GetUncommittedChanges().Any())
{
_storage.Save(aggregate);
}
}
}

// Event Store的实现,这里保存在内存中,通常是保存到具体的数据库中,如SQL Server、Mongodb等
public class InMemoryEventStorage : IEventStorage
{
// 领域事件的保存
public void Save(AggregateRoot aggregate)
{
// 获得对应领域实体未提交的事件
var uncommittedChanges = aggregate.GetUncommittedChanges();
var version = aggregate.Version;

        foreach (var @event in uncommittedChanges)
        {
            version++;
            // 没3个事件创建一次快照
            if (version > 2)
            {
                if (version % 3 == 0)
                {
                    var originator = (ISnapshotOrignator)aggregate;
                    var snapshot = originator.CreateSnapshot();
                    snapshot.Version = version;
                    SaveSnapshot(snapshot);
                }
            }

            @event.Version = version;
            // 保存事件到EventStore中
            \_events.Add(@event);
        }

        // 保存事件完成之后,再将该事件发布到EventBus 做进一步处理
        foreach (var @event in uncommittedChanges)
        {
            var desEvent = TypeConverter.ChangeTo(@event, @event.GetType());
            \_eventBus.Publish(desEvent);
        }
    }
}

// EventBus的实现
public class EventBus : IEventBus
{
private readonly IEventHandlerFactory _eventHandlerFactory;

    public EventBus(IEventHandlerFactory eventHandlerFactory)
    {
        \_eventHandlerFactory = eventHandlerFactory;
    }

    public void Publish<T>(T @event) where T : DomainEvent
    {
        // 获得对应的EventHandle来处理事件
        var handlers = \_eventHandlerFactory.GetHandlers<T>();
        foreach (var eventHandler in handlers)
        {
            // 对事件进行处理
            eventHandler.Handle(@event);
        }
    }
}

// DiaryItemCreatedEvent的事件处理类
public class DiaryIteamCreatedEventHandler : IEventHandler
{
private readonly IStorage _storage;

    public DiaryIteamCreatedEventHandler(IStorage storage)
    {
        \_storage = storage;
    }

    public void Handle(DiaryItemCreatedEvent @event)
    {
        var item = new DiaryItemDto()
        {
            Id = @event.SourceId,
            Description = @event.Description,
            From = @event.From,
            Title = @event.Title,
            To = @event.To,
            Version = @event.Version
        };

        // 将领域对象持久化到QueryDatabase中
        \_storage.Add(item);
    }
}

复制代码

复制代码

  上面代码主要演示了Command部分的实现,从代码可以看出,首先我们需要通过ServiceLocator类来对依赖注入对象进行注入,然后UI层通过CommandBus把对应的命令发布到CommandBus中进行处理,命令总线再查找对应的CommandHandler来对命令进行处理,接着CommandHandler调用仓储类来保存领域对象对应的事件,保存事件成功后再将事件发布到事件总线中进行处理,然后由对应的事件处理程序将领域对象保存到QueryDatabase中。这样就完成了命令部分的操作,从中可以发现,命令部分的实现和CQRS系统中的系统结构图的处理过程是一样的。然而创建日志命令并没有涉及事件溯源操作,因为创建命令并需要重建领域对象,此时的领域对象是通过创建日志命令来获得的,但在修改和删除命令中涉及了事件溯源,因为此时需要根据命令对象的ID来重建领域对象。具体的实现可以参考源码。

  下面让我们再看看查询部分的实现。

  查询部分的实现代码:

复制代码

复制代码

public class HomeController : Controller
{
// 查询部分
public ActionResult Index()
{
// 直接获得QueryDatabase对象来查询所有日志
var model = ServiceLocator.QueryStorage.GetItems();
return View(model);
}
}

public class InMemoryStorage : IStorage
{
private static readonly List Items = new List();

    public DiaryItemDto GetById(Guid id)
    {
        return Items.FirstOrDefault(a => a.Id == id);
    }

    public void Add(DiaryItemDto item)
    {
        Items.Add(item);
    }

    public void Delete(Guid id)
    {
        Items.RemoveAll(i => i.Id == id);
    }

    public List<DiaryItemDto> GetItems()
    {
        return Items;
    }
}

复制代码

复制代码

  从上面代码可以看出,查询部分的代码实现相对比较简单,UI层直接通过QueryDatabase来查询领域对象,然后由UI层进行渲染出来显示。

  到此,一个简单的CQRS系统就完成了,然而在项目中,UI层并不会直接CommandBus和QueryDatabase进行引用,而是通过对应的CommandService和QueryService来进行协调,具体的系统结构如下图所示(只是在CommandBus和Query Database前加入了一个SOA的服务层来进行协调,这样有利于系统扩展,可以通过SOA服务来进行请求路由,将不同请求路由不同的系统中,这样会可以实现多个系统进行一个整合):

  关于该CQRS系统的演示效果,大家可以自行去Github或MSDN中进行下载,具体的下载地址将会本专题最后给出。

   到这里,本专题关于CQRS的介绍就结束了,并且本专题也是领域驱动设计系列的最后一篇了。本系列专题的内容主要是参考daxnet的ByteartRetail案例,由于daxnet在写这个案例的时候并没有一步一步介绍其创建过程,对于一些领域驱动的初学者来说,直接去学习这个案例未免会有点困难,导致学习兴趣降低,从而放弃领域驱动的学习。为了解决这些问题,所以,本人对ByteartRetail案例进行剖析,并参考该案例一步步实现自己的领域驱动案例OnlineStore。希望本系列可以帮助大家打开领域驱动的大门。

  由于现在NO-SQL在互联网行业的应用已经非常流行,以至于面试的时候经常会被问到你用过的非关系数据库有哪些?所以本人也不想Out,所以在最近2个月的时候学习了一些No-SQL的内容,所以,接下来,我将会开启一个NO-SQL系列,记录自己这段时间来学习NO-SQL的一些心得和体会。

  本专题所有源码下载:

  Github地址:https://github.com/lizhi5753186/CQRSDemo

   MSDN地址:https://code.msdn.microsoft.com/CQRS-1f05ebe5

     本文参考链接:

     http://www.codeproject.com/Articles/555855/Introduction-to-CQRS

     http://www.cnblogs.com/daxnet/archive/2010/08/02/1790299.html

      Python模块和C/C++的动态库间相互调用在实际的应用中会有所涉及,在此作一总结。

1、Python调用C动态链接库

        Python调用C库比较简单,不经过任何封装打包成so,再使用python的ctypes调用即可。
(1)C语言文件:pycall.c

复制代码

/***gcc -o libpycall.so -shared -fPIC pycall.c*/ #include <stdio.h> #include <stdlib.h>
int foo(int a, int b)
{
printf(“you input %d and %d\n”, a, b); return a+b;
}

复制代码

(2)gcc编译生成动态库libpycall.so:gcc -o libpycall.so -shared -fPIC pycall.c。使用g++编译生成C动态库的代码中的函数或者方法时,需要使用extern “C”来进行编译。

(3)Python调用动态库的文件:pycall.py

import ctypes
ll = ctypes.cdll.LoadLibrary
lib = ll(“./libpycall.so”)
lib.foo(1, 3) print ‘***finish***‘

(4)运行结果:

2、Python调用C++(类)动态链接库

       需要extern “C”来辅助,也就是说还是只能调用C函数,不能直接调用方法,但是能解析C++方法。不是用extern “C”,构建后的动态链接库没有这些函数的符号表。
(1)C++类文件:pycallclass.cpp

(3)Python调用动态库的文件:pycallclass.py

3、Python调用C/C++可执行程序

(1)C/C++程序:main.cpp

(3)Python调用程序:main.py

复制代码

import commands import os
main = “./testmain”
if os.path.exists(main):
rc, out = commands.getstatusoutput(main) print ‘rc = %d, \nout = %s’ % (rc, out) print ‘*‘*10 f = os.popen(main)
data = f.readlines()
f.close() print data print ‘*‘*10 os.system(main)

复制代码

(4)运行结果:

4、扩展Python(C++为Python编写扩展模块)

       所有能被整合或导入到其它python脚本的代码,都可以被称为扩展。可以用Python来写扩展,也可以用C和C++之类的编译型的语言来写扩展。Python在设计之初就考虑到要让模块的导入机制足够抽象。抽象到让使用模块的代码无法了解到模块的具体实现细节。Python的可扩展性具有的优点:方便为语言增加新功能、具有可定制性、代码可以实现复用等。
       为 Python 创建扩展需要三个主要的步骤:创建应用程序代码、利用样板来包装代码和编译与测试。
(1)创建应用程序代码

复制代码

#include <stdio.h> #include <stdlib.h> #include <string.h>

int fac(int n)
{ if (n < 2) return(1); /* 0! == 1! == 1 */
return (n)*fac(n-1); /* n! == n*(n-1)! */ } char *reverse(char *s)
{
register char t, /* tmp */
*p = s, /* fwd */
*q = (s + (strlen(s) - 1)); /* bwd */

while (p < q)               /\* if p < q \*/ {  
    t \= \*p;         /\* swap & move ptrs \*/  
    \*p++ = \*q; \*q-- = t;  
} return(s);  

} int main()
{ char s[BUFSIZ];
printf(“4! == %d\n”, fac(4));
printf(“8! == %d\n”, fac(8));
printf(“12! == %d\n”, fac(12));
strcpy(s, “abcdef”);
printf(“reversing ‘abcdef’, we get ‘%s’\n”, \
reverse(s));
strcpy(s, “madam”);
printf(“reversing ‘madam’, we get ‘%s’\n”, \
reverse(s)); return 0;
}

复制代码

上述代码中有两个函数,一个是递归求阶乘的函数fac();另一个reverse()函数实现了一个简单的字符串反转算法,其主要目的是修改传入的字符串,使其内容完全反转,但不需要申请内存后反着复制的方法。

(2)用样板来包装代码
        接口的代码被称为“样板”代码,它是应用程序代码与Python解释器之间进行交互所必不可少的一部分。样板主要分为4步:a、包含Python的头文件;b、为每个模块的每一个函数增加一个型如PyObject* Module_func()的包装函数;c、为每个模块增加一个型如PyMethodDef ModuleMethods[]的数组;d、增加模块初始化函数void initModule()。

        增加包装函数,所在模块名为Extest,那么创建一个包装函数叫Extest_fac(),在Python脚本中使用是先import Extest,然后调用Extest.fac(),当Extest.fac()被调用时,包装函数Extest_fac()会被调用,包装函数接受一个 Python的整数参数,把它转为C的整数,然后调用C的fac()函数,得到一个整型的返回值,最后把这个返回值转为Python的整型数做为整个函数调用的结果返回回去。其他两个包装函数Extest_doppel()和Extest_test()类似。
         从Python到C的转换用PyArg_Parse*系列函数,int PyArg_ParseTuple():把Python传过来的参数转为C;int PyArg_ParseTupleAndKeywords()与PyArg_ParseTuple()作用相同,但是同时解析关键字参数;它们的用法跟C的sscanf函数很像,都接受一个字符串流,并根据一个指定的格式字符串进行解析,把结果放入到相应的指针所指的变量中去,它们的返回值为1表示解析成功,返回值为0表示失败。从C到Python的转换函数是PyObject* Py_BuildValue():把C的数据转为Python的一个对象或一组对象,然后返回之;Py_BuildValue的用法跟sprintf很像,把所有的参数按格式字符串所指定的格式转换成一个Python的对象。
        C与Python之间数据转换的转换代码:

        为每个模块增加一个型如PyMethodDef ModuleMethods[]的数组,以便于Python解释器能够导入并调用它们,每一个数组都包含了函数在Python中的名字,相应的包装函数的名字以及一个METH_VARARGS常量,METH_VARARGS表示参数以tuple形式传入。 若需要使用PyArg_ParseTupleAndKeywords()函数来分析命名参数的话,还需要让这个标志常量与METH_KEYWORDS常量进行逻辑与运算常量 。数组最后用两个NULL来表示函数信息列表的结束。
         所有工作的最后一部分就是模块的初始化函数,调用Py_InitModule()函数,并把模块名和ModuleMethods[]数组的名字传递进去,以便于解释器能正确的调用模块中的函数。
(3)编译
        为了让新Python的扩展能被创建,需要把它们与Python库放在一起编译,distutils包被用来编译、安装和分发这些模块、扩展和包。
        创建一个setup.py 文件,编译最主要的工作由setup()函数来完成:

        运行setup.py build命令就可以开始编译我们的扩展了,提示部分信息:

         你的扩展会被创建在运行setup.py脚本所在目录下的build/lib.*目录中,可以切换到那个目录中来测试模块,或者也可以用命令把它安装到Python中:python setup.py install,会提示相应信息。
         测试模块:

(5)引用计数和线程安全
     Python对象引用计数的宏:Py_INCREF(obj)增加对象obj的引用计数,Py_DECREF(obj)减少对象obj的引用计数。Py_INCREF()和Py_DECREF()两个函数也有一个先检查对象是否为空的版本,分别为Py_XINCREF()和Py_XDECREF()。
      编译扩展的程序员必须要注意,代码有可能会被运行在一个多线程的Python环境中。这些线程使用了两个C宏Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS,通过将代码和线程隔离,保证了运行和非运行时的安全性,由这些宏包裹的代码将会允许其他线程的运行。

       C++可以调用Python脚本,那么就可以写一些Python的脚本接口供C++调用了,至少可以把Python当成文本形式的动态链接库, 
需要的时候还可以改一改,只要不改变接口。缺点是C++的程序一旦编译好了,再改就没那么方便了。
(1)Python脚本:pytest.py

复制代码

#test function
def add(a,b): print “in python function add”
print “a = “ + str(a) print “b = “ + str(b) print “ret = “ + str(a+b) return

def foo(a): print “in python function foo”
print “a = “ + str(a) print “ret = “ + str(a * a) return

class guestlist: def __init__(self): print “aaaa”
def p(): print “bbbbb”
def __getitem__(self, id): return “ccccc”
def update():
guest = guestlist() print guest[‘aa’] #update()

复制代码

(2)C++代码:

复制代码

/**g++ -o callpy callpy.cpp -I/usr/include/python2.6 -L/usr/lib64/python2.6/config -lpython2.6**/ #include <Python.h>
int main(int argc, char** argv)
{ // 初始化Python //在使用Python系统前,必须使用Py_Initialize对其 //进行初始化。它会载入Python的内建模块并添加系统路 //径到模块搜索路径中。这个函数没有返回值,检查系统 //是否初始化成功需要使用Py_IsInitialized。
Py_Initialize(); // 检查初始化是否成功
if ( !Py_IsInitialized() ) { return -1;
} // 添加当前路径 //把输入的字符串作为Python代码直接运行,返回0 //表示成功,-1表示有错。大多时候错误都是因为字符串 //中有语法错误。
PyRun_SimpleString(“import sys”);
PyRun_SimpleString(“print ‘—import sys—‘“);
PyRun_SimpleString(“sys.path.append(‘./‘)”);
PyObject *pName,*pModule,*pDict,*pFunc,*pArgs; // 载入名为pytest的脚本
pName = PyString_FromString(“pytest”);
pModule = PyImport_Import(pName); if ( !pModule ) {
printf(“can’t find pytest.py”);
getchar(); return -1;
}
pDict = PyModule_GetDict(pModule); if ( !pDict ) { return -1;
} // 找出函数名为add的函数
printf(“-———————\n”);
pFunc = PyDict_GetItemString(pDict, “add”); if ( !pFunc || !PyCallable_Check(pFunc) ) {
printf(“can’t find function [add]“);
getchar(); return -1;
} // 参数进栈
*pArgs;
pArgs = PyTuple_New(2); // PyObject* Py_BuildValue(char *format, …) // 把C++的变量转换成一个Python对象。当需要从 // C++传递变量到Python时,就会使用这个函数。此函数 // 有点类似C的printf,但格式不同。常用的格式有 // s 表示字符串, // i 表示整型变量, // f 表示浮点数, // O 表示一个Python对象。
PyTuple_SetItem(pArgs, 0, Py_BuildValue(“l”,3));
PyTuple_SetItem(pArgs, 1, Py_BuildValue(“l”,4)); // 调用Python函数
PyObject_CallObject(pFunc, pArgs); //下面这段是查找函数foo 并执行foo
printf(“-———————\n”);
pFunc = PyDict_GetItemString(pDict, “foo”); if ( !pFunc || !PyCallable_Check(pFunc) ) {
printf(“can’t find function [foo]“);
getchar(); return -1;
}

pArgs \= PyTuple\_New(1);  
PyTuple\_SetItem(pArgs, 0, Py\_BuildValue("l",2));   

PyObject\_CallObject(pFunc, pArgs);  
   
printf("\----------------------\\n");  
pFunc \= PyDict\_GetItemString(pDict, "update"); if ( !pFunc || !PyCallable\_Check(pFunc) ) {  
    printf("can't find function \[update\]");  
    getchar(); return -1;  
 }  
pArgs \= PyTuple\_New(0);  
PyTuple\_SetItem(pArgs, 0, Py\_BuildValue(""));  
PyObject\_CallObject(pFunc, pArgs);       

Py\_DECREF(pName);  
Py\_DECREF(pArgs);  
Py\_DECREF(pModule); // 关闭Python 

Py_Finalize(); return 0;
}

复制代码

(3)C++编译成二进制可执行文件:g++ -o callpy callpy.cpp -I/usr/include/python2.6 -L/usr/lib64/python2.6/config -lpython2.6,编译选项需要手动指定Python的include路径和链接接路径(Python版本号根据具体情况而定)。

(4)运行结果:

(1)Python和C/C++的相互调用仅是测试代码,具体的项目开发还得参考Python的API文档。
(2)两者交互,C++可为Python编写扩展模块,Python也可为C++提供脚本接口,更加方便于实际应用。
(3)若有不足,请留言,在此先感谢!

大家好,我叫汤雪华。我平时工作使用Java,业余时间喜欢用C#做点开源项目,如ENode, EQueue。我个人对DDD领域驱动设计、CQRS架构、事件溯源(Event Sourcing,简称ES)、事件驱动架构(EDA)这些领域比较感兴趣。我希望把自己所学的知识能否分享给大家,所以,把这个领域里的一些知识串联了起来,整理了一个PPT,并为每张PPT配备注释,分享给大家。希望能对这个领域有兴趣的朋友有所帮助。

上面的提纲是今天主要分享的内容概要。开始之前想先说一下微服务架构和CQRS架构的区别和联系。

微服务架构现在很热,到处可以看到各大互联网公司的微服务道路的分享总结。但是,我今天的分享和微服务没有关系,希望可以带给大家一些新的东西。如果一定要说微服务和CQRS架构的关系,那我觉得微服务是一种边界思维,微服务的目的是为了从业务角度拆分(职责分离)当前业务领域的不同业务模块到不同的服务,每个微服务之间的数据完全独立,它们之间的交互可以通过SOA RPC调用(耦合比较高),也可以通过EDA 消息驱动(耦合比较低);

微服务架构和CQRS架构的关系:每个微服务内部,我们可以用CQRS/ES架构来实现,也可以用传统三次架构来实现;

首先,我们需要先理解DDD中的聚合、聚合根这两个概念。

聚合,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。聚合定义了一组具有内聚关系的相关对象的集合,我们把聚合看作是一个修改数据的最小原子单元。聚合根,每个聚合都有一个根对象,根对象管理聚合内的其他子对象(实体、值对象);聚合之间的交互都是通过聚合根来交互,不能绕过聚合根去直接和聚合下的子实体进行交互。上面的例子中,Car、Wheel、Position、Tire四个对象构成一个聚合,其中Car是聚合根;Customer也是聚合根,Customer不能直接访问Car下的Tire(子实体),而是只能通过聚合根Car来访问。

上面表达了一个关于聚合的一致性设计原则:聚合内的数据修改,是ACID强一致性的;跨聚合的数据修改,是最终一致性的。遵守这个原则,可以让我们最大化的降低并发冲突,从而最大化的提高整个系统的吞吐。

In-Memory的意思是指整个系统中的所有的聚合根对象都活在内存。而不是像我们平时那样,用到的时候才从DB获取对象,然后再做修改,再保存回去。

在In-Memory的架构下,当要修改某个聚合根的状态时,它已经在内存,我们可以直接拿到该对象的引用,且框架会尽量保证聚合根对象的状态就是最新的。聚合根是在内存中的最小计算单元,每个聚合内部都封装了业务规则,并保证数据的强一致性。

上图我是挪用了之前比较或的LMAX架构中的一个图,表达的思想就是in-memory架构。其中Business Logic Processor就是中央业务逻辑处理器,内部承载了大量在机器内存中活着的聚合根对象。

接下来,我们再来看一下什么是事件溯源。

一个对象从创建开始到消亡会经历很多事件,以前我们是在每次对象参与完一个业务动作后把对象的最新状态持久化保存到数据库中,也就是说我们的数据库中的数据是反映了对象的当前最新的状态。而事件溯源则相反,不是保存对象的最新状态,而是保存这个对象所经历的每个事件,所有的由对象产生的事件会按照时间先后顺序有序的存放在数据库中。可以看出,事件溯源的这种做法是更符合事实观的,因为它完整的描述了对象的整个生命周期过程中所经历的所有事件。

那么,事件到底如何影响一个领域对象的状态的呢?很简单,当我们在触发某个领域对象的某个行为时,该领域对象会先产生一个事件,然后该对象自己响应该事件并更新其自己的状态,同时我们还会持久化在该对象上所发生的每一个事件;这样当我们要重新得到该对象的最新状态时,只要先创建一个空的对象,然后将和该对象相关的所有事件按照事件发生先后顺序从先到后再全部应用一遍即可还原得到该对象的最新状态,这个过程就是所谓的事件溯源。

另一方面,因为是用事件来表示对象的状态,而事件是只会增加不会修改。这就能让数据库里的表示对象的数据非常稳定,不可能存在DELETE或UPDATE等操作。因为一个事件就是表示一个事实,事实是不能被磨灭或修改的。这种特性可以让领域模型非常稳定,在数据库级别不会产生并发更新同一条数据的问题。

通过上面这个图,大家应该可以更直观的理解事件溯源和传统CRUD思想的区别。

Actor模型,这个概念大家应该都了解。Actor模型的核心思想是,对象直接不会直接调用来通信,而是通过发消息来通信。每个Actor都有一个Mailbox,它收到的所有的消息都会先放入Mailbox中,然后Actor内部单线程处理Mailbox中的消息。从而保证对同一个Actor的任何消息的处理,都是线性的,无并发冲突。从全局上来看,就是整个系统中,有很多的Actor,每个Actor都在处理自己Mailbox中的消息,Actor之间通过发消息来通信。

Akka框架就是实现Actor模型的并行开发框架,并且Akka框架融入了聚合、In-Memory、Event Sourcing这些概念。Actor非常适合作为DDD聚合根。Actor的状态修改是由事件驱动的,事件被持久化起来,然后通过Event Sourcing的技术,还原特定Actor的最新状态到内存。

上图表达的是事件驱动的架构的思想。Node表示节点,每个节点负责处理逻辑;Event表示消息,节点之间通过消息进行通信。消息通过分布式消息队列如RocketMQ,Equeue进行通信。

事件驱动架构的核心思想是:

  1. 不同于SOA架构,EDA架构是pub-sub模式;Node1处理完逻辑后产生消息,Node2订阅消息并进行处理,Node1不知道Node2的存在;
  2. 最终一致性原则,Node1,Node2之间的数据一致性通过MQ最终保证一致;
  3. 如何保证最终一致性(消息链不会断开):1)MQ保证消息不丢;2)任何一个Node要保证自己完全处理完后才发送ACK给MQ;3)每个Node做到对任何消息处理的幂等性;
  4. 整个架构具有所有分布式MQ所带来的优点:如异步解耦、削峰、降低整个系统的整体部署成本;

上图是一个面向Topic的分布式MQ的逻辑架构图,采用这种架构的MQ有:Kafka,RocketMQ,EQueue

  1. Producer发送消息到某个Topic的某个Queue;
  2. 消息都存储在Broker上;
  3. Consumer从Broker拉取消息进行消费,并支持消费者负载均衡;

好了,上面是基本概念的介绍。接下来我们来看一下CQRS/ES架构。

上图是CQRS架构的典型架构图。

什么是CQRS架构?

CQRS本身只是一个读写分离的架构思想,全称是:Command Query Responsibility Segregation,即命令查询职责分离,表示在架构层面,将一个系统分为写入(命令)和查询两部分。一个命令表示一种意图,表示命令系统做什么修改,命令的执行结果通常不需要返回;一个查询表示向系统查询数据并返回。

CQRS架构中,另外一个重要的概念就是事件,事件表示命令操作领域中的聚合根,然后聚合根的状态发生变化后产生的事件。

采用CQRS架构的一个前提

由于CQRS架构的一致性模型为最终一致性,所以,你的系统要接受查询到的数据可能不是最新的,而是有几个毫秒的延迟。之所以会有这个前提,是因为CQRS架构考虑到,作为一个多用户同时访问的互联网应用,当在高并发修改数据的情况下,比如秒杀、12306购票等场景,用户UI上看到的数据总是旧的。比如你秒杀时提交订单前看到库存还大于0,但是当你提交订单时,系统提示你宝贝卖完了。这个就说明,在这种高并发修改同一资源的情况下,任何人看到的数据总是Stale的,即旧的。

CQRS作为一种架构思想,可以有多种实现方式

  • 最常见的CQRS架构是数据库的读写分离;
  • 系统底层存储不分离,但是上层逻辑代码分离;
  • 系统底层存储分离,C端采用Event Sourcing的技术,在EventStore中存储事件;Q端存储对象的最新状态,用于提供查询支持;

CQRS架构的适用场景

  • 当我们的应用的写模型和读模型差别比较大时;
  • 当我们希望实践DDD时;因为CQRS架构可以让我们实现领域模型不受任何ORM框架带来的对象和数据库的阻抗失衡的影响;
  • 当我们希望对系统的查询性能和写入性能分开进行优化时,尤其是读/写比非常高的系统,CQ分离是必须的;
  • 当我们希望我们的系统同时满足高并发的写、高并发的读的时候;因为CQRS架构可以做到C端最大化的写,Q端非常方便的提供可扩展的读模型;

这里我主要分享的CQRS架构是上面第3种实现方式,也就是上图所画的架构。在我心目中,只有第三种才是真正意义上的CQRS架构。

下面简单描述一下上面的CQRS架构的数据流

C端的命令的执行流程

客户端如(MVC Controller)发送命令通知系统做修改:

  1. 发送命令到分布式MQ;
  2. 然后命令的订阅者处理命令;
  3. 订阅者内部根据不同的命令调用不同的Command Handler进行处理;
  4. Command Handler内部根据命令所指定的聚合根ID从In-Memory内存中直接获取聚合根对象的引用,然后操作聚合根对象;
  5. 聚合根对象状态发生变化并产生事件;
  6. 框架负责自动持久化事件到Event Storage(简称EventStore);
  7. 框架负责将事件发布到Event MQ;
  8. Event订阅者订阅事件,然后调用对应的Event Handler进行处理,如更新Data Storage(保存了聚合根的最新状态,通常叫读库,ReadDB);

Q端的查询的执行流程

客户端如(MVC Controller)发出查询请求系统返回数据:

  1. 调用轻薄的Query Service,传如Query DTO;
  2. Query Service从读库进行查询并返回结果;

读库可以有很多种,依据我们的业务场景来选择:比如关系型DB、分布式缓存等NoSQL、搜索引擎,etc.

前面的CQRS架构图我介绍了CQRS架构的基本概念、设计初衷、一致性模型、实现方式、适用场景、架构的基本数据流这些方面。但这不是CQRS架构的全部,我们还可以挖掘出更多有用的特性出来。比如假设我们为这个架构引入以下一些特性,就可以达到更多意想不到的好处:

  1. 遵守一个原则:一个命令只允许修改一个聚合根;
  2. 命令或事件在分布式MQ的路由根据聚合根ID来路由,也就是同一个聚合根的命令和事件都在一个队列里;
  3. 引入Command Mailbox,Event Mailbox这两个概念,将聚合根需要处理的命令和产生的事件都队列化,去并发;做到架构上最大的并行,将并发降低到最低;
  4. 引入Group Commit技术,做到整个C端的架构层面支持批量提交聚合根产生的事件,从而极大的提高C端的整体吞吐量;比如可以实现对同一个聚合根的每秒修改TPS达到5W?这个在传统的架构下是很难做到的。而在这个架构下,框架就可以提供支持。
  5. 通过引入Saga(不了解的同学可以网上搜一下什么是CQRS Saga)的概念,做到基于事件驱动的最终一致性,大家可以回想一下前面介绍的Node通过Event连接的架构;整个系统的所有节点的交互通过消息来驱动;

通过引入上面这些架构设计原则,我们可以让CQRS架构的C端更强大,性能更高;当然,复杂性也大大增加。所以,要完成这样一套架构,没有成熟框架的支撑,是几乎不可能的,ENode框架就是在为做这样的一个框架而努力。

我们可以从上面几个非功能性特性去考察这个架构。大部分大家应该都可以体会到,关于消息的幂等处理这块,CQRS\ES这个架构可以做的非常彻底。

平时传统我们的消息驱动的架构,或者是RPC调用的SOA风格的应用,消息处理者或者服务被调用方,必须自己做到数据修改的幂等性。而幂等性的实现思路也很多,比如用kv来判重,用DB的唯一索引,等等。

而CQRS\ES架构,由于使用了Event Sourcing的技术,所以可以直接在EventStore中自动做到聚合根并发修改的冲突的检测、以及同一个命令的重复处理的检测。并能通知框架自动做并发处理或做重新发布该命令所产生的事件;

大家可能会疑问,为何已经将命令通过聚合根ID进行路由了,且同一台机器内页已经通过Actor Mailbox技术解决并发问题了,还是有并发冲突的可能呢?原因是当我们的服务器在出现扩容或缩容时,会出现由于集群中服务器变动导致的同一个聚合根的不同命令可能会在不同的机器上同时被处理,从而导致并发冲突。

最后,关于这个架构的瓶颈,相信大家已经可以发现,是在EventStore。所以,这就要求我们设计一个超高性能的EventStore数据库。具体见后面的介绍吧。

上面这个图演示了,当C端产生的事件,在Q端的处理顺序如果不一致时,导致Q端的结果和C端不一致了。所以,事件的处理顺序必须和产生的顺序一致,这点必须保证,但可以由框架来保证,开发者无需关注。需要强调的是,这个顺序处理事件不需要交给分布式消息中间件来保证,而是应该交给Consumer来自己保重。当Consumer收到一个版本为N+2的时间,而当前Q端的版本为N,则N+2的消息需要先hold一下,不要立即处理。然后等待N+1的事件过来,N+1的事件过来并处理后,再处理N+2的事件。如果N+1的事件一直不过来,则需要永远等待。总之,这里的顺序必须保证。如果这个顺序交给分布式消息中间件去保证,那性能上会非常差,而要让分布式消息中间件实现绝对意义上的顺序消费,又要实现高可用,高性能,难度很大。我个人不太赞成,除非是Consumer自己无法处理消息顺序的场景才迫不得已让分布式消息中间件来保证,比如mysql binlog的同步。

上图演示了假设一个命令修改两个或多个聚合根时,会导致阻塞大大增加,从而整个系统的吞吐会降低。而好处是,我们可以得到聚合根之间的数据的强一致性。

上图演示了,当一个命令只修改一个聚合根时,先通过一级路由,将聚合根路由到分布式MQ的同一个队列里,然后同一个队列总是被一台固定的机器消费,从而保证同一个聚合根的命令总是在一台机器上处理。

上图掩演示了,当命令进入一台机器后,再通过Command Mailbox的二次路由,同样是根据聚合根ID,从而保证单个机器内,同一个聚合根的命令的处理是顺序线性的,从而避免了并发冲突。

EventStore处理并发和命令幂等的根本设计就是上图的两个唯一索引。
1. 聚合根ID + 事件版本号唯一;
2. 聚合根ID + 命令ID唯一;

当万一出现了并发冲突,则框架需要取出重新加载该聚合根的最新状态,然后重试当前命令;当出现了命令的重复处理,则框架需要把该命令之前产生的事件再重新取出来,发布到分布式消息中间件。因为有可能之前虽然这个事件被持久化了,但理论山有可能这个事件没有成功发布到分布式消息中间件(因为那个时候断电了,够倒霉的,呵呵)。所以,事件的消费者可能会再次收到这个事件,并处理。但这么做都是为了保证整个业务流的最终一致性。想想之前的EDA的架构图的说明吧。

下面我们来看看CQRS架构下,开发者需要写的代码有哪些?

首先是需要定义Command和Event。其中Command相当于DDD经典四层架构中的应用层的一个方法的参数。

Command表示命令系统做什么,表达一种意图,在架构上设计为一个DTO即可。Event表示一个事件,表示领域内发生了什么状态变化,用过去式命名事件。事件是只读的。

Command Handler是无状态的,用于处理一个或多个命令,不同的命令有不同的Handle方法。一个Command Handler做的典型的事情就两个:

  1. 根据命令的信息创建一个聚合根;
  2. 根据命令的信息修改一个聚合根;

框架可以做到开发人员无需关注底层的技术问题,比如如何存储聚合根产生的事件,如何发布事件到MQ;彻底做到技术架构和业务逻辑分离。这点在传统架构下是很难做到的。

Note表示一个DDD聚合根,这里最核心的概念是:Note内部的状态的修改都是通过事件来驱动的,也就是Note要做任何修改前,总是应该先产生事件,然后框架根据事件调用到对应的Handle方法,然后我们在Handle方法中修改Note的内部状态。

为何要独立拆分出Handle方法呢?因为是在Event Souring事件溯源还原聚合根状态时,框架需要调用这些Handle方法。根据Event Sourcing的思想,会根据Note聚合根的ID获取该聚合根的所有的事件,然后按照事件发生的顺序,分别调用每个事件的Handle方法,就可以还原出聚合根的最新状态了。

最后一个需要开发者写的代码就是Event Handler,根据CQRS架构的定义,Event Handler负责根据C端产生的事件来更新读库。上面的例子只是记录日志,实际我们需要在Handle方法中更新读库,如数据库,分布式缓存等。

这是ENode中今年打算实现的文件版本的EventStore的设计思路,目前是使用的DB来实现的。我现在在做EQueue的高可用,等这个做完,就开始做EventStore的文件版本。上面PPT中的设计思路,还希望能和大家多多交流,一起完善它。因为它是整个CQRS/ES架构的核心所在。

前面介绍了很多CQRS\ES架构方面的东西,最后我们再看两个实际应用的场景:秒杀、12036购票。

要实现高并发的订单处理(生成订单、预扣库存两个核心步骤)。淘宝做的很牛逼,可以在这两个步骤都完成后直接告诉用户下单结果,当然,我认为CQRS架构也完全可以在保证这两点处理后再返回买家的前提下,实现淘宝一样的吞吐。

这里我列举这些订单状态的目的,主要是想表达第一个状态用意:订单处理中。通过引入这个状态,我们处理订单的的代价就轻很多了,不需要在完成生成订单、预扣库存这两个核心步骤就可以返回客户端浏览器了。买家订单提交成功后,服务端首先在分布式缓存中检查商品的库存是否足够,如果不够,则立即返回并通知买家宝贝卖完了;如果足够,则发送下单的命令到MQ(异步处理订单)。然后通知买家“您好,您的订单已收到,正在处理中。请稍后到我的订单中心查看订单处理结果。祝您购物愉快!”之类的提示。

然后当买家进入“我的订单中心”查看订单时,可能的情况有:

  1. 订单未生成,则买家看不到订单,没关系,TA过一会儿刷新页面继续查看;
  2. 订单已生成,但是预扣库存还未有结果,则提示订单处理中,用户同样会等待;
  3. 订单已生成,预扣库存也已经有结果,不管库存是否足够,都显示相应状态给用户;

通过这样的订单状态的设计和交互体验,相当于把轮训查看订单处理结果的职责交给了买家。而这个小小的设计,带来的好处是极大的方便我们实现非常高的订单处理吞吐了。当然,如果我们能做到像淘宝这样的体验,就是下单时直接告诉结果,那自然最好了。只是这样代价更大而已。我提出这个例子的原因是CQRS架构是一种C端异步处理命令的架构,所以在这种架构上,我们需要一切尽量以异步为出发点去思考和设计业务流程,设计用户交互体验。实际上这个体验在亚马逊上买东西,你可能会遇到,甚至亚马逊直接让你去你的邮箱看订单处理结果。所以,我觉得这里只是一个购物习惯的差别,但对技术的要求却差别很大。

上图描述了一个DDD CQRS架构的典型的Saga的设计,对应前面的秒杀场景的订单处理流程。

上图中,Order、Conference、Payment为三个聚合根,分别表示订单、库存、支付;Order Process Manager是无状态的,表示一个流程管理器,CQRS架构中一般叫Saga。流程管理器的设计理念是:订阅事件,根据不同的事件,发送不同的命令。也就是说,流程管理器的职责是对流程进行建模,负责封装流程控制逻辑,而聚合根负责业务逻辑。整个订单处理的流程大概为业务层面的2PC。即下单时,要先预扣库存;然后,买家付款后要真正扣库存。

上图中,棕色的线条表示命令,蓝色的线条表示事件。

Saga是CQRS架构中处理复杂业务流程的典型做法,通过事件驱动的方式去替代传统的分布式事务。牺牲强一致性的方式来提高系统的吞吐。实际上,在高并发的情况下,有时我们不得不选择最终一致性,因为分布式事务的成本太高。

这个案例是关于12306购票的例子,上面说了核心的业务场景和领域概念。我举这个例子的用意是为了说明,12306购票的场景,C端的领域模型是比传统的电商网站要复杂很多的,因为库存是一个动态的概念。不像普通电商,一个库存跟着SKU,很简单。12306你买了一个车子的某个区间的票之后,这个区间内的其他的票的库存数都会发生变化,而且这个库存数还要考虑座位的分配,非常复杂。

这个场景,就是我上面说的CQRS的应用场景中的:要满足高并发的写、高并发的查询,同时C端的业务模型非常复杂。要同时面对这3点,实现这个系统是很难的。

我认为,这个场景的难点不在于技术层面,而是在于DDD领域建模层面。大家如果对这个场景的领域模型,架构实现,以及示例代码感兴趣,可以看我下面的两个地址:
浅谈12306核心模型设计思路和架构设计
http://www.cnblogs.com/netfocus/p/5187241.html

12306购票领域建模示例代码:
https://github.com/tangxuehua/enode,具体看ENode开源项目中的E12306案例代码。

如果大家对这个领域感兴趣,可以访问我的博客。我博客中录制了大量的视频介绍,视频介绍汇总地址:
http://www.cnblogs.com/netfocus/p/4707789.html

谢谢大家!