Event Store-其它存储
背景
ENode是一个CQRS+Event Sourcing架构的开发框架,Event Sourcing需要持久化事件,事件可以持久化在DB,但是DB由于面向的是CRUD场景,是针对数据会不断修改或删除的场景,所以内部实现会比较复杂,性能也相对比较低。而Event Store实际上对数据只有新增和查询的需求,所以我想为Event Sourcing的场景针对性的实现一个Event Store。看了一下业界的一些实现,感觉都没有达到我的期望,所以想自己动手实现一个。下面是我构思的一个Event Store的单机版应该要具备的能力以及对应的设计方案,分享出来和大家讨论。
一、需求概述
- 存储聚合根的事件数据
- 支持事件的版本并发控制,新事件的版本号必须是当前版本号+1
- 支持命令重复判断,即不可以处理重复命令产生的事件
- 支持按聚合根ID查询该聚合根的所有事件
- 支持按聚合根ID+事件版本号查询指定的事件
- 支持按命令ID查询该命令对应的事件数据
- 高性能,写入要尽量快,查询要尽量快
二、事件数据格式

{
"aggregateRootId": "", //聚合根ID
"aggregateRootType": "", //聚合根类型
"eventVersion": "", //事件版本号
"eventTime": "", //事件发生时间
"eventData": "", //事件数据,JSON格式
"commandId": "", //产生该事件的命令ID
"commandTime": "" //产生该事件的命令产生时间
}

三、存储设计
1、核心内存存储设计
- 遵循内存只存储索引数据的原则,尽量充分利用内存;
- aggregateLatestVersionDict,存储每个聚合根的最大事件版本号
- key:aggregateRootId,聚合根ID
- value:
- eventVersion,当前聚合根的最新事件的版本号,也即当前聚合根的版本号
- eventTime,事件产生时间
- eventPosition,事件在事件数据文件中的位置
- commandIdDict,存储命令索引
- key:commandId,命令ID
- value:
- commandTime,命令产生时间
- eventPosition,命令对应的事件在事件数据文件中的位置
2、物理存储的数据
- 事件数据:eventData,单条数据的结构:

{
"aggregateRootId": "", //聚合根ID
"aggregateRootType": "", //聚合根类型
"eventVersion": "", //事件版本号
"eventTime": "", //事件发生时间
"eventData": "", //事件数据,JSON格式
"commandId": "", //产生该事件的命令ID
"commandTime": "", //产生该事件的命令产生的事件
"previousEventPosition": ""//前一个事件在事件文件中的位置
}

- 事件索引:eventIndex,单条数据的结构:
{
"aggregateRootId": "", //聚合根ID
"eventVersion": "", //事件版本号
"eventTime": "", //事件产生时间
"eventPosition": "", //事件在事件数据文件中的位置
}
- 命令索引:commandIndex,存储内容:存储所有命令的ID及其对应的事件所在文件的位置
{
"commandId": "", //聚合根ID
"commandTime": "", //命令产生时间
"eventPosition": "", //事件在事件数据文件中的位置
}
3、事件数据存储
- 同步顺序写eventDataChunk文件,一个文件大小为1GB,写满一个文件后写入下一个文件;
- 写入每个事件时,同时写入当前事件的前一个事件所在的文件位置,以便将来可以一次性将某个聚合根的所有事件从文件查找出来;
4、事件索引存储
- 异步顺序写eventIndexChunk文件,一个文件大小为1GB,写满一个文件后写入下一个文件;
- 对于已经写满的不会再变化的文件的内容,使用后台线程进行B+树索引整理,索引的排序依据是聚合根ID+事件版本号;B+树设计为3层,根节点包含1000个子节点,每个子节点再包含1000个子节点,这样叶子节点共有100W个。每个叶子节点我们保存20个版本索引,则单个文件共可保存最多2000W个版本索引,10个文件为2亿个版本索引;单机存储2亿个事件索引,应该可以满足大部分应用场景了;3层,则查找任意一个节点,只需要3次IO访问;
- 由于是后台线程对已经写完的文件进行B+树索引整理,B+树是在内存建立,建立完成后,将最新的内容写入新文件,原子替换老的eventIndexChunk文件;所以,这块的逻辑处理应该不会对服务的主逻辑产生较大的影响;
- 采用BloomFilter优化查询性能,使用BloomFilter来快速判断某个eventIndexChunk文件中是否包含某个聚合根ID,如果不在,则不用从B+树去检索该聚合根的版本号了;如果在,则取检索;通过这个设计,当我们要获取某个聚合根的最大版本号时,不需要对每个eventIndexChunk文件进行B+树查询,而是先通过BloomFilter快速判断当前的eventIndexChunk文件是否包含该聚合根的信息,大大提升检索效率;BloomFilter的二进制Bit数据占用内存小,可以在每个eventIndexChunk文件被扫描时,和文件头的信息一起加载到内存;
5、命令索引存储
- 异步顺序写commandIndexChunk文件,一个文件大小为1GB,写满一个文件后写入下一个文件;
- 同事件索引存储,进行B+树索引建立,索引的排序依据是命令ID;
- 同事件索引存储,采用BloomFilter优化查询性能;
四、框架逻辑设计
1、查询某个聚合根的最大版本号
- EventStore启动时,会加载所有的eventIndexChunk文件的元数据到内存,比如文件号、文件头、BloomFilter等信息,但不真实加载文件内容,文件数不会太多,最多也就几十个;
- 根据聚合根ID+BloomFilter算法,快速确定应该到哪个eventIndexChunk文件中去查找该聚合根的最新版本号,eventIndexChunk文件从新到旧遍历,因为某个聚合根ID的最大版本号一定是在最新的eventIndexChunk文件中的;
- 在找到的eventIndexChunk中使用B+树查找算法,找到对应的叶子节点;
- 在找到的叶子节点,使用二分查找算法(由于单个节点的聚合根ID不多,顺序查找即可),找到指定聚合根的最新版本号;
2、查询某个聚合根的所有事件
- 先通过上面的算法找出该聚合根的最大版本号的事件在事件数据文件中的位置;
- 然后从该位置获取事件完整数据;
- 再根据事件数据中记录的上一个事件在事件数据文件中的位置,查找上一个事件的数据;
- 以此类推,直到找到该聚合根的第一个事件的数据;
3、查询某个命令对应的事件数据
- 先尝试从内存查询该命令的索引信息,如果存在,则直接获取该命令对应的事件在事件数据文件中的位置,即eventPosition;如果不存在,则尝试从命令的索引文件中查找,结合BloomFilter和B+树查找算法进行查找;
- 如果找到了eventPosition,则根据eventPosition到事件数据文件中查找对应的事件数据即可;如果未找到,则返回空;
4、追加一个新事件的处理逻辑
- 根据aggregateLatestVersionDict判断事件版本号是否合法,必须是聚合根的当前版本号+1,如果当前版本号不存在,则首先尝试从eventIndexChunk文件查找当前聚合根的最大版本号,如果还是查找不到,说明当前聚合根确实不存在任何事件,则当前事件版本号必须为1;
- 根据commandIdDict判断命令ID是否重复,如果commandIdDict中不存在该命令,尝试从commandIndexChunk文件中查找,也是B+树的方式;这里需要设计一个配置项,让开发者配置是否需要继续从commandIndexChunk文件查找命令ID。有时我们只希望从内存查找即可,不希望再从磁盘查找了,因为判断命令是否重复我们很多时候只希望检查最近一段时间内的命令,检查全部命令代价过大,意义也不是很大;
- 如果事件的版本号合法、命令ID不重复,则Append的方式写入事件数据到eventDataChunk;
- 写入完成后,更新aggregateLatestVersionDict、commandIdDict,、BloomFilter的Bit数组,以及将当前的事件放入内存的一个双缓冲队列;队列消费者异步批量将事件索引和命令索引写入对应的索引文件;
- 返回事件写入结果;
5、其他逻辑
- 异步线程定时批量持久化事件索引;
- 异步线程定时批量持久化命令索引;
- 异步线程定时清理不需要放在内存的聚合根最新版本号信息(aggregateLatestVersionDict中的key),根据eventTime判断,只保留最近1周有过变化(产生过事件)的聚合根;
- 异步线程定时清理不需要放在内存的命令索引(commandIdDict中的key),根据commandTime判断,只保留最近1周的命令ID;
- 异步线程定时进行事件索引和命令索引的B+树索引的建立,即对已经写入完成的eventIndexChunk和commandIndexChunk文件的内部重构;
- eventIndexChunk和commandIndexChunk文件标记为写入完成前,要把BloomFilter的Bit数组内容写入文件中;
- 其他EventStore的启动逻辑,比如启动时加载一定数量的索引数据到内存,以及索引数据相比事件数据是否有漏掉或无效的检查;
- 其他逻辑支持,如支持聚合根的快照存储,从文件查找数据时,如果文件的B+树索引信息还未建立,则需要进行全文扫码;
Event Store-其它存储的更多相关文章
- Event Store 2.0发布,带来了安全支持和测试版Projections库
Event Store 2.0版本于上周发布,它带来了安全支持允许锁定Event Store和在事件流上设置访问控制列表.其主要新特性包括: HTTP和TCP之上的身份认证,包括账户管理 测试版Pro ...
- event store
Event Store The documentation has now moved to the wiki in this repository. For a quick start, look ...
- Lucene——Field.Store(存储域选项)及Field.Index(索引选项)
Field.Store.YES或者NO(存储域选项) 设置为YES表示或把这个域中的内容完全存储到文件中,方便进行文本的还原 设置为NO表示把这个域的内容不存储到文件中,但是可以被索引,此时内容无法完 ...
- vue——store全局存储
业务场景:刷新页面时,首次拉取所有配置,存储到store状态管理用于全局调用: import Vue from 'vue' import Vuex from 'vuex' import userInf ...
- vuex store刷新存储状态
app.vue 平时不想把信息存到session里,只有无可奈何的时候才准备村 <script> export default { name: 'App', created() { //刷 ...
- 使用vuex中的store存储数据
Vuex是一个专门为Vue.js应用程序开发的状态管理模式,这个状态自管理应用包括三个模式 state 驱动应用的数据源 view 以声明方式将state映射到视图 actions 响应在view上的 ...
- Event Sourcing Pattern 事件源模式
Use an append-only store to record the full series of events that describe actions taken on data in ...
- Event Sourcing - ENode(一)
分布式系统 摩尔定律如果一直能实现,不管是涉及或者实现一个OLTP的系统,我们是不是都会轻松点,用硬件堆就可以了.但是现在硬件已经在求变了,那么我们也得求变,云的概念如此之火,本质就是设施虚拟化,也可 ...
- 从event loop规范探究javaScript异步及浏览器更新渲染时机
异步的思考 event loops隐藏得比较深,很多人对它很陌生.但提起异步,相信每个人都知道.异步背后的“靠山”就是event loops.这里的异步准确的说应该叫浏览器的event loops或者 ...
- HTML5本地存储(Local Storage) 的前世今生
长久以来本地存储能力一直是桌面应用区别于Web应用的一个主要优势.对于桌面应用(或者原生应用),操作系统一般都提供了一个抽象层用来帮助应用程序保存其本地数据 例如(用户配置信息或者运行时状态等). 常 ...
随机推荐
- 3.16 Linux删除文件或目录(rm命令)
当 Linux 系统使用很长时间之后,可能会有一些已经没用的文件(即垃圾),这些文件不但会消耗宝贵的硬盘资源,还是降低系统的运行效率,因此需要及时地清理. rm 是强大的删除命令,它可以永久性地删除文 ...
- JESD79-5C_v1.30-2024 JEDEC DDR5 SOLID STATE TECHNOLOGY ASSOCIATION 最新内存技术规范
JESD79-5C_v1.30-2024 JEDEC DDR5 SOLID STATE TECHNOLOGY ASSOCIATION 最新DDR5内存技术规范 JEDEC 技术协会公布新 DDR5 ...
- 2024 BUPT Programming Contest F
简要题意 多测,给定一个 \(n \times n\) 矩阵,矩阵中的每一个元素的计算方式如下: 矩阵的行和列唯一决定两个整数对 \((a, b)\),矩阵第 \(a(0 \le a < n)\ ...
- AI运动小程序开发常见问题集锦一
截止到现在写博文时,我们的AI运动识别小程序插件已经迭代了23个版本,成功应用于健身.体育.体测.AR互动等场景:为了让正在集成或者计划进行功能扩展优化的用户,少走弯路.投入更少的开发资源,我们归集了 ...
- SoftReference,WeakReference&WeakHashMap
zz http://java.chinaitlab.com/oop/716371.html 在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象.也就是说,只有对象处于 ...
- linux故障排查工具之dmesg
dmesg命令是用来在Unix-like系统中显示内核的相关信息的.dmesg全称是display message (or display driver),即显示信息. 实际上,dmesg命令是从内核 ...
- Educational Codeforces Round 65 (Rated for Div
D - Bicolored RBS 给定一个括号序列,现在你必须对每一个括号涂成蓝色或红色,要求使得涂完后的红色括号和蓝色括号序列都必须是合法的括号序列,设红色括号形成的的括号序列的深度为\(dep_ ...
- Fuzz技术综述与文件Fuzz
文章一开始发表在微信公众号 https://mp.weixin.qq.com/s?__biz=MzUyNzc4Mzk3MQ==&mid=2247486189&idx=1&sn= ...
- docker安装指定版本的gitlab并配置ssl证书
安装gitlab,指定14.5.2版本,为便于与下篇文章:gitlab恢复做准备.如果不需要恢复,或不需要配置ssl证书,请自行精简以下操作. 1. 开启gitlab sh gitlab.sh doc ...
- 对象存储 AVIF 图片压缩,邀您参与免费内测!
对象存储 AVIF 图片压缩免费内测正式开放!AVIF 作为压缩图片中的新主力军,都有哪些特点呢?通过对象存储又要如何使用 AVIF 压缩呢?这篇文章将深入浅出的为您介绍~ 具体介绍 现在硬件设备越 ...