事件驱动的思考

最近一直在思考一个问题:有没有这样一种可能,就是一个领域模型的状态不依赖于外部,它只负责接收外部的事件,然后根据这些事件做出响应;响应分两种:

  1. 根据模型当前的内存状态进行业务逻辑处理,然后产生事件,注意:这个过程不会改变模型当前的内存状态;
  2. 根据事件改变自己的状态;

另外,也是最重要的,领域模型不用关心自己所产生的事件到底怎么样了,比如不关心有没有持久化,不关心是否和别的事件有并发冲突。它只管根据自己当前的内存状态做上面这两点的响应;

如果这样的设想有可能,那领域模型就是真正的中央业务逻辑处理器了,和CPU很类似了。这样它才能真正快起来。

简单的说就是:事件->模型->事件
模型只管响应事件,然后响应处理,然后产生新的事件

领域模型就是一黑盒,它只能帮你处理业务逻辑,其他的什么处理结果它一概不关心;当然,领域模型肯定有它自己的状态,但这个状态是驻留在内存的,和领域模型是一体的。

我为什么会有这个想法是因为,我在想,为什么要让领域模型的处理逻辑依赖于它的处理结果是否被正确顺利持久化了?感觉这很荒唐。
既然领域模型有自己的内存状态空间,他的所有逻辑也应该只依赖于这个状态空间,不再依赖于其他任何外部的东西。

当然,以前我们设计的IRepository,实际背后都是直接从数据库取。这样的话,领域模型的状态空间就是数据库了。但是这样其实很不好,为什么不用内存作为领域模型的状态空间呢?

现在再想想LMAX就是我刚才的想法的一个实际例子。

事件->模型->事件,这样的设计,理论上并不需要必须要求单线程来访问模型,因为领域模型不依赖于任何外部的状态,只依赖于自己所在存活内存空间;单线程有一个很大的好处就是可以防止并发冲突的产生。我们其实完全支持多线程或集群的方式,只不过这样会有可能访问到的领域对象的状态是了老的,因为不同的机器之间的领域模型内存对象的状态需要做一些同步,访问到老数据的可能性的大小取决于并发的大小以及机器之间数据同步的快慢;
LMAX之所以用单线程,是考虑了,这单线程的领域模型和性能之间,性能已经非常高其足以达到他们的要求了。

这样的架构,我觉得领域模型中的任何一个对象的一次完整的状态更新至少会响应两个事件,举个例子:

  1. 先响应ChangeNoteCommand(command也是一种事件,可以理解为NoteChangeRequested),然后Note模型产生一个NoteChanged事件,注意,此时模型自己的状态还未改变,此时只是先产生了一个事件表示什么事情发生了;
  2. 然后该事件(NoteChanged)最终又被发送到领域模型让其响应,此时,领域模型才去更改自己的Note状态并将最新状态保存到自己的内存空间,如一个dict中或redis中;

经过对这两个事件的响应,才完成了Note的最终状态的修改;而我们以前都是从数据库取Note,然后更改,然后保存到数据库。这样不慢才怪!

通过上面的两次事件响应,可以换来领域模型对事件的极快的响应,因为完全无IO。
剩下的我们只要考虑(我目前考虑了以下六个问题):

  1. 消息的序列化和反序列化;
  2. 消息传递的速度;
  3. 事件持久化的速度;
  4. 并发冲突后重试的设计;
  5. 消息丢失了怎么办;
  6. 集群部署时,各台服务器之间内存的同步如何实现;

需要明白的是:这些都不是领域模型该考虑的问题。这些外围的任何问题,都不要让领域模型自己去考虑,我们应该对出现的各种问题逐个寻求解决方案。

每个问题的解决方案我大概理了下我的对策:

  1. 消息的序列化和反序列化:这个简单,用BinaryFormatter,或更快的开源序列化组件,对于事件这样大小的对象可以达到每秒10W次每秒;
  2. 消息传递的速度:用MSMQ/RabbitMq,等带持久化功能的队列组件;如果嫌太慢,就用ZeroMq(无消息持久化功能),但可以达到30W消息每秒;
  3. 事件持久化的速度:由于事件都是跟着单个聚合根,所以我们只要确保单个聚合根的事件不会冲突(即没有重复的版本号的事件);为了更快的持久化,我们可以对事件按照聚合根或者其他方式进行分区存放,不同的服务器存放不同的聚合根的事件;这样通过集群持久化的方式可以实现多事件同时被持久化,从而提高整体的事件持久化吞吐量;如单个mongodb server每秒持久化5000个,那10个mongodb server就能每秒持久化5W个;
  4. 并发冲突后怎么办:一般来说就是选择重试,但为了确保不会出现不可控的局面(可能由于某种原因一直在重试,引起消息堵塞),那需要设置一个最大的重试次数;超过最大重试次数后不再重试,然后记录日志,以供以后查找问题;这里的重试的意思是:重新找到对应该事件的command,然后再次发送该command给领域模型处理;
  5. 消息丢失:丢失就丢失了呗,呵呵;要是你觉得消息决不能丢失,那就用可靠的带持久化功能的消息传输队列,如MSMQ;当然,就算消息丢失了,我们很多时候都要想想有没有影响的,一般来说,消息丢失,至少我们是知道程序有问题了的,因为模型的状态此时一定是不对的。我们可以通过在消息发出时和接收时记录日志,这样方便以后查找消息是在哪个环节丢的;
  6. 任何其他的异常出现,这个我觉得如果都是托管代码,那可以在必要的地方加try catch,然后记录日志。至于是否要重试,还要看情形;
  7. 另外,如果是多线程访问模型,或集群访问,那很多时候访问到的内存的领域对象的状态都是老的,那怎么办?其实这不是问题,因为事件持久化的时候会被检测到这种并发重复,然后对应的command会被重试。
  8. 如果一个事件被成功的持久化了,那如何让各台应用服务器知道?这个我觉得也简单,就是当事件持久化完成后,通过zeromq publish给所有的应用服务器,每台应用服务器都有一个后台的线程在不停的接收已被成功持久化了的事件,然后根据这些事件更新自己内存空间中的领域对象的状态。这一步完全可以由框架自动做掉;这里相当于我上面提到的第二个事件(NoteChanged)是由框架自动处理的,不需要用户写代码干预;前面说到,因为是publish-subscribe模式,所以各台应用服务器上的数据就会自然保持同步了;

另外,这种架构,传输的是事件,事件都是很小的,所以不用担心消息传输的性能。

对于以上的想法,有人有下面的两点担心:

  1. 事件是否就是解决当前复杂软件架构的银弹?
  2. 系统中如果出现海量的事件是否会出现另一种灾难?

我记得不知道是谁说过,OO的本质就是消息通信。command也好,event也好,或者直接的方法调用也好,本质上都是对象与对象之间的消息通信。

方法调用太生硬(这点我记得你曾今也提到过,当然我觉得聚合内很适合用方法调用来实现聚合内的对象的通信)
command, event本质上都是通过message作为媒介,实现对象与对象之间的通信。这让我想起有一位高人曾经说过的一个比喻,下面是摘录的他的原话:

“现在的SOA、ESB之类的东西是不是就像打造一个企业的“神经脉络”,而“OO”是不是就像“神经元”,它们之间的通讯就是靠生物电脉冲,这就是消息驱动。”

所以,我在想,软件实现用户的需求,是不是也应该有很多的对象以及很多的消息(event)这两样东西作为核心组成,对象相当于神经元,消息相当于生物电脉冲。整个软件在运行过程中就是这样一个由对象以及消息组成的网络。

至于复杂性,我觉得框架可以帮我们实现消息通信的部分,而我们程序员要做的就是定义对象结构,然后让对象具有发送消息和接收消息的行为功能。我觉得这点并不是很复杂吧!

最近我一直在努力实现我这个想法,因为我师兄说:“我现在不相信什么架构,just show me the code”。

有想法和能实现出来是两回事,你有多少能力,你的设计能力,对细节的把控能力,程序员内在素养,一看代码便知,呵呵。

有人回复说:事件本身没有错,我想强调的是“事件”的定位问题。“事件”是一个界与另一个界交互的方式,但界是分层次的。用人体比喻很好理解,细胞之间的事件,组织之间的事件,器官之间的事件。构建这样的事件体系是非常复杂的,目前的技术很难达到,不是一个EventBus就可以解决的。

针对上面的说法,我觉得这里主要还是一个编程思路的转变问题。事件驱动天生是一种异步编程。我之所以想自己搞一个这样的框架,主要是因为:

  1. 事件驱动的编程模型让model不在有任何负担,让model只面向in memory,从而实现高性能不是梦了;
  2. 事件的version机制让我们方便的实现乐观并发,确保单个聚合根内强一致,聚合根之间最终一致;然后配合框架自动实现的重试功能,可以在并发冲突后自动重试,这样极大避免command的执行失败率;
  3. 事件数据不是关系型数据,所以事件产生者和处理者都可以多个,这意味着我们做集群非常容易,且事件的存储可以任意拆分,只要确保同一个聚合根的事件放在一起即可,不同聚合根的事件理论上都可以放在不同的服务器上,这样我们持久化事件也可以并发,我们只要对聚合根id+commitSequence这两个字段建立唯一索引即可。从而克服事件持久化(IO操作)慢的瓶颈;

在这么多诱人的特性面前,我们还有什么说不的理由呢?困难不要紧,我们可以一步步来,呵呵。总比没有想法好,你说呢?

-------------------------------------------------------------------------------------------------------------
目前就想到这些。后续再完善思路。
 
知识决定命运,学习积累知识,而正确的思维方式是一切高效学习的基础。所以学会如何清晰地思考问题是非常重要的!
 
呵呵!
<think>好的,我需要向用户解释什么是事件驱动。首先,我应该明确事件驱动的基本概念,确保定义准确。事件驱动是一种编程范式,对吧?核心是事件的触发和处理。那事件是什么?比如用户操作或者系统信号。然后,我需要对比事件驱动和传统轮询的区别,这样用户更容易理解。比如轮询是不断检查状态,而事件驱动是等待事件触发。 接下来,要说明事件驱动的工作流程。事件循环、事件监听器、回调函数这些组成部分需要逐一解释。例如,事件循环持续监测事件,一旦发生,就调用对应的处理函数。这里可能需要举个实际的例子,比如GUI应用中的按钮点击,或者服务器处理HTTP请求。 然后,应用场景也很重要。用户可能想知道事件驱动用在哪些地方。比如图形界面、游戏开发、服务器、IoT设备等。每个场景简单提一下,说明事件驱动如何发挥作用。 还要考虑优缺点。优点是高效、资源占用少,因为不需要轮询;缺点是复杂度高,特别是异步处理带来的问题,比如回调地狱。不过现代编程中,Promise和async/await帮助解决了部分问题,可以稍微提一下。 在结构上,按照用户的要求,分点回答,使用中文,数学公式用正确的LaTeX格式。不过这个问题可能不太涉及数学公式,但如果有涉及的地方,比如时间复杂度的对比,可能需要用$O(n)$之类的表达。不过看起来这里可能用不上,所以主要关注自然语言的解释。 需要确保语言清晰,逐步引导用户理解。可能用户是编程新手,所以避免使用过多术语,或者在使用术语时解释清楚。比如“回调函数”需要说明是预先定义好的处理函数。 检查是否有遗漏的部分。是否提到了事件队列?或者事件驱动的架构组成?可能需要简要提及事件生产者、消费者、分发机制等,但保持简洁。 最后,总结事件驱动的意义,为什么被广泛使用,比如响应式、实时处理的需求增加,适合高并发的场景等。这样用户能有一个全面的认识。 现在把这些思考整理成一个结构化的回答,分步骤解释事件驱动,确保符合用户给出的格式要求,使用中文,不使用Markdown,行内公式用$...$,独立公式用$$...$$,但问题本身可能不需要太多公式,所以重点放在文字解释上。同时保持段落清晰,每部分用数字或项目符号分隔,但用户要求避免Markdown,所以用自然的分段和编号。</think>事件驱动是一种编程范式,其核心思想是**通过事件的触发响应来控制系统流程**。它的运作方式类似于现实生活中的“触发-反应”机制,例如点击按钮后弹出对话框。以下是逐步解析: --- ### 一、核心概念 1. **事件(Event)** 事件是程序运行中发生的特定动作或状态变化,例如: - 用户操作:鼠标点击、键盘输入。 - 系统信号:文件加载完成、网络请求返回。 - 内部消息:定时器到期、数据更新。 2. **事件处理器(Event Handler)** 预先定义的处理函数,用于响应特定事件。例如: ```javascript button.addEventListener("click", function() { console.log("按钮被点击!"); // 点击事件触发时执行 }); ``` --- ### 二、工作流程 1. **事件循环(Event Loop)** 程序持续监听事件队列,按顺序处理事件。流程如下: - **监听**:等待新事件加入队列。 - **提取**:从队列中取出事件。 - **分发**:调用绑定的事件处理器。 - **循环**:重复上述步骤。 2. **异步非阻塞** 事件驱动通常异步结合,避免主线程阻塞。例如: - 网络请求发送后,程序继续执行其他任务,待响应到达时再触发回调函数。 --- ### 三、传统轮询的对比 | **事件驱动** | **轮询(Polling)** | |---------------------------|-----------------------------| | 被动响应事件 | 主动周期性地检查状态 | | 资源占用低(无忙等待) | 资源消耗高(持续占用CPU) | | 实时性强(即时触发) | 响应延迟取决于轮询间隔 | 例如,若用轮询检测键盘输入,需每毫秒循环检查输入状态,而事件驱动仅在输入实际发生时触发处理。 --- ### 四、典型应用场景 1. **图形用户界面(GUI)** 如点击按钮、拖动窗口等操作均通过事件驱动实现。 2. **服务器开发** Node.js 使用事件驱动处理高并发请求,性能显著优于传统多线程模型。 3. **游戏开发** 玩家操作(移动、攻击)和游戏状态更新(碰撞检测)均依赖事件响应。 4. **物联网(IoT)** 传感器数据到达时立即触发处理逻辑,例如温度超标报警。 --- ### 五、优缺点分析 - **优点** - 高效:资源仅在事件发生时被占用。 - 扩展性:易于添加新事件类型和处理逻辑。 - 适合高并发:如服务器同时处理数千连接。 - **缺点** - 复杂度:回调嵌套可能导致“回调地狱”(可用 Promise/async-await 缓解)。 - 调试困难:异步流程难以跟踪执行顺序。 --- ### 六、技术实现示例 以浏览器环境为例: ```javascript // 1. 定义事件处理器 function handleScroll(event) { console.log(`滚动位置:${window.scrollY}`); } // 2. 绑定事件监听 window.addEventListener("scroll", handleScroll); // 3. 事件触发时自动调用 handleScroll ``` --- 总结来说,事件驱动通过“发生什么,就做什么”的模式,使程序更灵活高效,尤其适合需要快速响应和资源优化的场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值