哈喽~你们好,时间过的真快,关于DDD领域驱动设计的讲解基本就差很少了,原本想着周四再开一篇,感受没有太多的内容了,剩下的一个就是验证的问题,就和以前的JWT很相似,就不打开一个章节了,并且这个也不是领域驱动设计范畴以内的,下一个系列 Ids 的讲解中,可能会穿插着讲一讲,而后到时候正好一块儿完善了。html
虽然是完结了,不过内心仍是不是很开森呀,经过小伙伴的反馈,而后我也咨询了官方的建议,好像这个DDD领域驱动设计系列,并无获得不少的支持,影响力彻底比不过第一个系列《从壹开始先后端分离》,缘由多是,我也没有在项目中真正的使用过DDD的缘由吧,也或许是写的比较生硬,主要我也一直在研究,不过我这里必定要说一下,仍是要多看看的,不必定要看我讲的,能够看看书也行,或者看看别人的博客,DDD领域驱动设计思想真的很不错,而后还夹带着CQRS命令查询职责分离、Bus总线思想、EDA事件驱动思想、ES事件溯源思想(今天要说到的)、git
举个溯源的例子(我瞎想的):github
可能不是很恰当,也可能他们根本不是这么作的,我只是用这个感受来讲明什么是溯源:sql
你们在玩儿消消乐的时候,偶尔会遇到这个状况,好比玩儿了二十步的时候,忽然闪退了,而后咱们从新进去,从新进这一关的时候,会看到系统快速的把咱们的这二十步进行了还原,有一个过程步骤,也许没人注意,那我想问下,这个是怎么保存的呢?难道直接获取的当前关卡的状态么,有一丝丝的溯源的意味。数据库
你们本身思考其余的例子,好比银行查帐,天天的数据汇总等等。编程
消息队列等等这些之前没有接触到的思想设计,也为微服务打下了必定的基础,若是没有这些基础,你是很难理解为何要使用微服务的,这里咱们就先来回顾一下这些天咱们都说了什么内容吧:json
FluentValidator
算上今天的内容,正好是十二篇,也是个人比较喜欢的一个数字(以前在文章中说到过这个缘由,这里就很少说了),也是很辛苦写了这么多,但愿有时间有精力的时候,仍是要多看看的,多品品思想,这样咱们就不会一直问一些虚无缥缈的问题了,虽然我如今是越学的多,越不会的多😂。后端
可能你也发现了今天的题目有些不同,由于我以前说过,要在圣诞节简单搞个小活动,既然说了,就不能食言,不过目前我写了十六万字了,就一个小伙伴给了我一块钱红包😂,好失败,因此我就简单来个小福利吧,由于这个系列的名字就是Christ3D,当时就是想着在圣诞节前能说完,还能够,紧赶慢赶的说完了,我就想着一个给粉丝一个小小小福利:跨域
具体的参与形式看文章末尾:(已结束)缓存
一、免费给送三本书,多是《实现领域驱动设计》这本书,或者《领域驱动设计 软件核心复杂性应对之道》,还有我本人的签名+贺卡哟哈哈;
二、原本想抽十位粉丝,送精装的圣诞节苹果,可是考虑食品安全问题就算了,直接到时候发红包吧(时间地点保密,提示:为了老粉丝);
言归正传,今天的重点仍是要好好的说说新知识——事件溯(su)源,Event Source,也有人翻译事件采购,或者是事件回溯,或者直接就是ES,其实都是一个意思,要是下次你发现这几个词语的时候,都是指的事件溯源,其实事件溯源已经有一只脚迈进了微服务的你们族了,甚至能够说已经在微服务的一员了,他配合着事件总线EventBus、消息队列等,在微服务的工做中起着必定的做用。固然今天只是简单的入门讲解,要是想打开真正的微服务的大门,就须要你们本身去探索,固然,我也会继续跟进这个讲解,下一个系列 Ids ,其实也是微服务的一个分支,慢慢来,但愿你们多捧场啦!
立刻开始今天的讲解,仍是一天一问吧,但愿你们带着这个问题通读本文,本身能想到合适的答案:
一、你认为事件存储 EventStore 和 日志记录的区别是什么?
这里要给你们再强调两点:
一、CQRS、EDA和ES这些其实已经不在DDD设计的范围以内,只不过这些技术都是一块儿使用的,多个技术的相互结合使用,才能发挥很大的做用,因此说本系列教程是
DDD+CQRS+EDA+ES的结合体,之后被别人问到的时候,可别说事件溯源就是领域驱动设计的一部分哟。
二、事件溯源不是一两句能说清的,这篇文章只是一个启蒙的做用,等你们从事微服务工做的时候,就知道它深层次的意义了,切不可和平时的 CURD 项目生搬硬套作比较。
(我写的十二篇文章中的知识点,这里基本都有了,也算是一个圆满了,集齐七颗啦😀)
事件溯源其实很好理解,首先从字面上的理解:
事件就是 Event,溯是一个动词,能够理解为追溯,回溯的意思,源表明原始、源头的意思,合起来表示一个事件追踪源的过程。
这个时候你仔细想想,咱们在和领域专家(默认他们不懂技术)讨论用户下单流程的时候,专家必定会说:客户首先选择一个商品,而后添加到购物车,确认无误下单,接着用户支付,支付成功后,就给用户发货。而咱们呢,咱们做为一个开发人员,和领域专家讨论的时候,天然而然的也是这么思考的,对不对!(你确定在讨论需求的时候用的不是数据库的思惟!),只不过咱们后期开发的时候,拘泥于技术和数据优先的思惟,不得不转向CURD的道路了,固然这个没有什么错误,我只是说明一点,事件存储真的离咱们不远。
那咱们平时是怎么作的呢,这里说一个特别简单的:
从这个特别简单的流程中咱们能够看到,平时咱们都是直接操做的 Order 这个领域聚合根,一直在修改模型状态,这个看似正常的操做下,有一些问题,是咱们创建在每一步都正常执行的状况下,不过通常总会出现一些问题,特别是分布式的环境中。
然而,事件溯源与上述的状况刚好相反,它并不关心当前状态,而是关注持续不断的变化事件。
举个例子,假设咱们有一个“购物车”,咱们能够建立购物车,往里面添加商品或移除商品,而后结帐。
购物车的生命周期能够包含以下一系列事件:
建立购物车
往购物车里添加商品
再次往购物车里添加商品
从购物车里移除商品
结帐
这些就是一个购物车的生命周期,包含了一系列事件。这就是事件溯源,很是简单吧?
几乎全部的流程均可以被当作一系列事件。在与领域专家交谈时,他们不会说起“表”和“链接”,他们会将流程描述成一系列事件以及能够应用在这些事件上的规则。
事件溯源不是万能的,不过它能够在某一些领域发挥很大的做用,这个在之后的微服务设计中,会更能体现出来,那咱们就简单说两点:
传统的应用中,数据库里存的是Domain Model的实例的当前状态,好比某个储户银行帐户的存款数,一般是一个数字.若是考虑到以下的三个情形,咱们可能付出的代价比较大:
1) 老规则:问题跟踪
若是某个储户的帐户出现问题,那么咱们只有从大到PB的日志中去分析用户的帐户数据是如何出错的,并且咱们在作日志的时候,不可能全部的都考虑到,就算是把所有数据都保存,时间都记下来,操做者都备份,那ATM机信息呢?(可能不恰当,只是说明咱们总有想不到的地方),但若是一旦日志不够详细,找出问题根源基本只能靠猜了。
2) 新需求:趋势分析
历史数据的做用在于分析将来的趋势,若是仅仅从浩如烟海的日志中寻找规律,咱们还得单独写逻辑,对日志进行建模,清洗,其实咱们已经能接受,日志就是用来记录异常信息的,这个时候咱们就很崩溃了。
3) 更奇葩:事务回滚
在介绍事务修正模式中,咱们讲到某个步骤发生错误,以前的各个节点能够本身独立地完成回滚,回滚的依据就是记录的操做步骤及相关参数,根据这些有用信息就能够每一个节点自行回滚到原始状态,而且在失败的时候能够retry
可见存储对于Domain Model 的各个事件仍是很是有用的,尤为是对于复杂的系统,这也就是咱们今天要讨论的事件溯源模式.
大多数的应用都和数据打交道,最多见的打交道方式就是将用户在使用过程当中的数据最终状态同步到数据库中。例如,在传统的增删改查(CURD)模式中,一个典型的数据过程就是从数据库中读出数据,修改完后再把修改后的数据更新到数据库中——一般来讲,在这个更新过程这张数据表是被锁住的。
这种传统的增删改查(CURD)方式存在一些局限性:
这是一个大问题。在以表做为驱动的系统里,你只保存了系统的当前状态,你根本就没法知道系统是如何达到当前状态的。若是我问你“这个用户修改了几回邮件地址”,你有办法回答吗?或者我再问“有多少人把一件商品添加到购物车里,而后又移除掉,直到一个月以后才买了那件商品”,你就更无法回答了。你存储数据的方式丢掉了不少有用的业务信息!
尽管它是一个简单的模式,但使用它有不少优势:
事件日志具备很高的商业价值;
它在DDD和事件驱动架构下运行得很是好。
调试用应用程序状态中全部变动的来源;
它容许您重放失败的事件;
易于调试,您能够将目标实体的全部事件复制到您的机器并调试每一个事件,以了解应用程序如何达到特定状态(忽略从生产环境复制数据的安全隐患);
容许您使用追溯事件模式重建/修复您的状态。
许多做者还将优先级做为时间查询的能力,但我认为查询多个后续事件不是一项简单的任务。所以,我一般认为时间查询是快照模式的一个优势。
有许多理由使用Event Sourcing,当你浏览Greg Young的系列文章和谈话你会发现下面要点:
1. 它不是一个新概念,真实世界中许多领域都很像它,看看你的银行帐户状态,好比储蓄卡,它打印出一笔笔进出明细和当前余额,这一笔笔表明了领域事件。
2.经过重播事件,咱们可以获得对象的任什么时候刻状态(这里应该用正确术语:聚合aggregate),好比储蓄卡每笔记录的当前余额表明你这个帐户聚合对象的某刻时刻的状态,这可能会极大地帮助咱们理解领域知识,当前状态是怎么来,由于什么改变?方便调试关键问题的错误
3.领域中当前状态和存储数据库中的数据没有任何耦合,而传统上咱们都是将应用状态存储到数据库中,好比储蓄卡当前余额100元存储到数据库中,如今咱们存储致使余额的进出事件了,存款了多少钱,取
款了多少钱,这一笔笔领域事件都会记录在数据库中。
4.Append-only追加模型存储这些事件,易于扩展,这样咱们不管读写都有很好地性能,读取可以转为查询优化,也能够转为写优化(由于没有读,写得很快),读写分离。
5除了能够存储用户意图数据,也就是操做事件,事件存储顺序可以用来分析用户正在作什么,通往大数据。
6.咱们能避免了对象与关系数据库的不匹配。
7.审计日志是免费的,一次审计日志全部变化,由于没有状态改变,只有事件。
这样不会浪费时间吗?
一点也不。通常来讲,要执行约束,只须要得到事件的一个很小子集。经过简单的数据库查询就能够得到有用的历史事件,在加载完这些事件后重放它们,把它们“投射”出来,以此构建你的数据集。这样的操做实际上是很快的,由于你使用的是本地的处理器,而不是执行一系列SQL查询(跨域网络的调用要比本地操做慢得多,至少会相差两个数量等级)。
你能够在后台构建数据集,而后把中间结果保存在数据库里。这样,用户就能够在很短的时间内查询到这些数据。
下面是一些困难:
1.定义事件是一件艺术,须要熟悉的领域建模,DDD领域驱动设计是关键。
2.须要软件和硬件支持事件采购,在之后几年,你会看到这个领域的不少解决方案。
3.这方面是新生事物,可指导的经验太少。
4.限制与真正成熟的DDD/ES技能。
其余带来的问题还有:
1.须要超级大的存储消耗。云存储解决。
2.比较慢也不是问题,由于咱们优化优化IO来实现快照和持久。并利用基于事件的自然“推”性质,咱们能够获得当即失效缓存。简而言之,可以事后有多个插入,须要这种多个的技术解决方案。
3.脆弱(丢失失过去的一个事件将致使整个流腐败)不是一个问题,由于你能够决定本身的SLA水平去(经过复制和冗余)。使用Git的方法,能够可靠地检测在任何一个副本的腐败事件包括SHA1签名针对它的内容和之前的事件签名计算。
在同步调用中不太直观,由于须要首先将请求转换为事件。
不管什么时候部署重大更新,若是您想要向后兼容(也称为“事件升级”),你将被迫迁移事件历史记录。
某些实现可能须要额外的工做来检查最新事件的状态,以确保全部事件都已被处理。
事件可能包含私有数据,因此不要忘记确保事件日志获得适当保护。
这种模式在如下几种场景中是最理想的解决方案:
这种模式在如下几种场景中可能并不适用:
CQRS与事件溯源有着相辅相成的关系。CQRS容许事件溯源做为领域的数据存储机制。然而,使用事件溯源的一个最大的缺点是,你没法向你的系统提出相似“请告诉我全部名字为Greg的用户”这样的问题,这是因为事件溯源没法提供对象的当前状态而引发的。CQRS惟一支持的查询就是:GetById - 经过ID来得到某个聚合。下图为基于CQRS/ES的应用系统结构:
CQRS常常和事件溯源模式结合使用
基于CQRS的系统使用分离的读和写模型,每个都对应相应的任务而且通常储存在不一样的数据库中。当和事件溯源模式一块儿使用的时候,一系列的事件存储至关于“写”模型,是全部信息的可信赖来源(authoritative source )。基于CQRS的系统的读模型提供了数据的物化视图,常常是一种高度格式化的视图形式。这些视图对应相应的界面而且展现了应用程序的需求,帮助最大化展现和查询效率。
使用一系列的事件看成“写”而不是某一个时间点的数据,避免了更新的冲突而且最大化性能和系统的伸缩性,这些事件能够被异步地产生被用来展现数据的物化视图。
由于事件数据库是全部信息的可信赖来源,当系统改进的时候,有可能删除物化视图而且展现全部过去的时间来产生一个新的数据,或者当读模型必须改变的时候。物化视图是一个长久的数据缓存。
当将CQRS和事件溯源模式结合起来的时候,考虑如下几点:
CQRS最核心的概念是Command、Event,“将数据(Data)看作是事实(Fact)。每一个事实都是过去的痕迹,虽然这种过去能够遗忘,但却没法改变。” 这一思想直接发展了Event Source,即将这些事件的发生过程记录下来,使得咱们能够追溯业务流程。CQRS对设计者的影响,是将领域逻辑,尤为是业务流程,皆看作是一种领域对象状态迁移的过程。这一点与REST将HTTP应用协议看作是应用状态迁移的引擎,有着殊途同归之妙。
一、必须本身实现事务的统一commit和rollback:这个是不管哪种方式,都必须面对的问题。彻底逃不掉。在DDD中有一个叫
Saga
的概念,专门用于统理这种复杂交互业务的,CQRS/ES架构下,因为自己就是最终一致性,因此都实现了Saga
,可使用该机制来作微服务下的transaction治理。
二、请求幂等:请求发送后,因为各类缘由,未能收到正确响应,而被请求端已经正确执行了操做。若是这时重发请求,则会形成重复操做。
CQRS/ES架构下经过AggregateRootId、Version、CommandId三种标识来识别相同command,目前的开源框架都实现了幂等支持。
三、并发:单点上,CQRS/ES中按事件的先来后到严格执行,内存中
Aggregate
的状态由单一线程原子操做进行改变。
多节点上,经过EventStore的broker机制,毫秒级将事件复制到其余节点,保证同步性,同时支持版本回退。(Eventuate)
你们请注意,下边的这一个流程,就和咱们平时开发的顺序是同样的,好比先创建模型,而后仓储层,而后应用服务层,最后是调用的过程,东西虽然不少,可是很简单,慢慢看都能看懂。
同时也复习下咱们DDD领域驱动设计是如何搭建环境的,正好在最后一篇和第一篇遥相呼应。
namespace Christ3D.Domain.Core.Events { /// <summary> /// 抽象类Message,用来获取咱们事件执行过程当中的类名 /// 而后而且添加聚合根 /// </summary> public abstract class Message : IRequest { public string MessageType { get; protected set; } public Guid AggregateId { get; protected set; } protected Message() { MessageType = GetType().Name; } } }
同时在该文件夹下,新建 存储事件 模型StoredEvent.cs
public class StoredEvent : Event { /// <summary> /// 构造方式实例化 /// </summary> /// <param name="theEvent"></param> /// <param name="data"></param> /// <param name="user"></param> public StoredEvent(Event theEvent, string data, string user) { Id = Guid.NewGuid(); AggregateId = theEvent.AggregateId; MessageType = theEvent.MessageType; Data = data; User = user; } // 为了EFCore能正确CodeFirst protected StoredEvent() { } // 事件存储Id public Guid Id { get; private set; } // 存储的数据 public string Data { get; private set; } // 用户信息 public string User { get; private set; } }
namespace Christ3D.Infra.Data.Mappings { /// <summary> /// 事件存储模型Map /// </summary> public class StoredEventMap : IEntityTypeConfiguration<StoredEvent> { public void Configure(EntityTypeBuilder<StoredEvent> builder) { builder.Property(c => c.Timestamp) .HasColumnName("CreationDate"); builder.Property(c => c.MessageType) .HasColumnName("Action") .HasColumnType("varchar(100)"); } } }
二、而后再上下文文件夹 Context 下,新建事件存储Sql上下文 EventStoreSQLContext.cs
namespace Christ3D.Infra.Data.Context { /// <summary> /// 事件存储数据库上下文,继承 DbContext /// /// </summary> public class EventStoreSQLContext : DbContext { // 事件存储模型 public DbSet<StoredEvent> StoredEvent { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new StoredEventMap()); base.OnModelCreating(modelBuilder); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // 获取连接字符串 var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") .Build(); // 使用默认的sql数据库链接 optionsBuilder.UseSqlServer(config.GetConnectionString("DefaultConnection")); } } }
这里要说明下,由于已经建立了两个上下文,之后迁移的时候,就要加上 上下文名称 了:
namespace Christ3D.Infra.Data.Repository.EventSourcing { /// <summary> /// 事件存储仓储接口 /// 继承IDisposable ,可手动回收 /// </summary> public interface IEventStoreRepository : IDisposable { void Store(StoredEvent theEvent); IList<StoredEvent> All(Guid aggregateId); } }
二、而后对上边的接口进行实现
namespace Christ3D.Infra.Data.Repository.EventSourcing { /// <summary> /// 事件仓储数据库仓储实现类 /// </summary> public class EventStoreSQLRepository : IEventStoreRepository { // 注入事件存储数据库上下文 private readonly EventStoreSQLContext _context; public EventStoreSQLRepository(EventStoreSQLContext context) { _context = context; } /// <summary> /// 根据聚合id 获取所有的事件 /// 这个聚合是指领域模型的聚合根模型 /// </summary> /// <param name="aggregateId"> 聚合根id 好比:订单模型id</param> /// <returns></returns> public IList<StoredEvent> All(Guid aggregateId) { return (from e in _context.StoredEvent where e.AggregateId == aggregateId select e).ToList(); } /// <summary> /// 将命令事件持久化 /// </summary> /// <param name="theEvent"></param> public void Store(StoredEvent theEvent) { _context.StoredEvent.Add(theEvent); _context.SaveChanges(); } /// <summary> /// 手动回收 /// </summary> public void Dispose() { _context.Dispose(); } } }
这个时候,咱们的事件存储模型、上下文和仓储层已经创建好了,也就是说咱们能够对咱们的事件模型进行持久化了,接下来就是在创建服务了,用来调用仓储的服务,就好像咱们的应用服务层的概念。
建完了基础设施层,那咱们接下来就须要创建服务层了,并对其进行调用:
一、仍是在核心领域层中的Events文件夹下,创建接口
namespace Christ3D.Domain.Core.Events { /// <summary> /// 领域存储服务接口 /// </summary> public interface IEventStoreService { /// <summary> /// 将命令模型进行保存 /// </summary> /// <typeparam name="T"> 泛型:Event命令模型</typeparam> /// <param name="theEvent"></param> void Save<T>(T theEvent) where T : Event; } }
二、而后再来实现该接口
在应用层 Christ3D.Application 中,新建 EventSourcing 文件夹,用来对咱们的事件存储进行溯源,而后新建 事件存储服务类 SqlEventStoreService.cs
namespace Christ3D.Infra.Data.EventSourcing { /// <summary> /// 事件存储服务类 /// </summary> public class SqlEventStoreService : IEventStoreService { // 注入咱们的仓储接口 private readonly IEventStoreRepository _eventStoreRepository; public SqlEventStoreService(IEventStoreRepository eventStoreRepository) { _eventStoreRepository = eventStoreRepository; } /// <summary> /// 保存事件模型统一方法 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="theEvent"></param> public void Save<T>(T theEvent) where T : Event { // 对事件模型序列化 var serializedData = JsonConvert.SerializeObject(theEvent); var storedEvent = new StoredEvent( theEvent, serializedData, "Laozhang"); _eventStoreRepository.Store(storedEvent); } } }
这个时候你会问了,那咱们如今都写好了,在哪里使用呢,欸?!聪明,既然是事件存储,那就是在事件保存的时候,进行存储,请往下看。
/// <summary> /// 引起事件的实现方法 /// </summary> /// <typeparam name="T">泛型 继承 Event:INotification</typeparam> /// <param name="event">事件模型,好比StudentRegisteredEvent</param> /// <returns></returns> public Task RaiseEvent<T>(T @event) where T : Event { // 除了领域通知之外的事件都保存下来 if (!@event.MessageType.Equals("DomainNotification")) _eventStoreService?.Save(@event); // MediatR中介者模式中的第二种方法,发布/订阅模式 return _mediator.Publish(@event); }
DDD领域驱动设计就到这里到一段落了,江湖很远,话很少说,我们下一系列再见!
//一、聚合根是什么?或者说是什么数据结构?(言之成理便可) //二、个人项目中,有几条总线,分别是? //三、个人项目中,在使用领域通知处理器以前,我是用什么不当的临时方法来处理验证错误信息的?(提示:在自定义视图组件中)