EventStore文件存储设计

背景

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,存储每个聚合根的最大事件版本号

    • eventVersion,当前聚合根的最新事件的版本号,也即当前聚合根的版本号

    • eventTime,事件产生时间

    • eventPosition,事件在事件数据文件中的位置

    • key:aggregateRootId,聚合根ID

    • value:

  • commandIdDict,存储命令索引

    • commandTime,命令产生时间

    • eventPosition,命令对应的事件在事件数据文件中的位置

    • key:commandId,命令ID

    • value:

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+树索引信息还未建立,则需要进行全文扫码;

原文地址:https://www.cnblogs.com/netfocus/p/10861152.html

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com 
640?wx_fmt=jpeg\


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/315937.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

.NET Core 如何禁止.resx文件自动生成Designer.cs

点击上方蓝字关注“汪宇杰博客”在 Visual Studio 中,如果我们在一个 .NET Core 工程里加入了一个资源文件(.resx),那么你会发现有个对应的 .Designer.cs 文件被自动生成了,每次资源文件的内容有变化,这个设…

P1450 [HAOI2008]硬币购物

P1450 [HAOI2008]硬币购物 题意: 共有 4 种硬币。面值分别为c1,c2,c3,c4c_1,c_2,c_3,c_4c1​,c2​,c3​,c4​。 某人去商店买东西,去了 n 次,对于每次购买,他带了 did_idi​枚 i 种硬币,想购买 s 的价值的东西。请问…

.net core百万设备连接服务和硬件需求测试

随着物联网的普及,服务应用将面对大量物联设备处理;早期.NET在通讯上的处理能力一直给人的印像并不怎样,但net core经历过大量的优化后在各个模块的处理性能都有着比较出色的提升,针对网络方向的处理模块也有着显著的提升。以下主…

字符串匹配(多模式匹配篇)

字符串匹配(多模式匹配篇)摘要:问题的提出:众所周知,KMP算法在O(n)的时间中solve单模式串匹配问题。但怎样solve多模式串匹配问题呢?Solve:本文用简要记叙了使用trie树&a…

.net core编写转发服务

我有个小伙伴问我,他需要写一个转发服务的他有很多功能要通过他的服务转发~技术栈又不一定asp.net core,我就想起泥水老前辈的BeetleX.FastHttpApi中午午休,折腾了一会儿前辈,问清楚了FastHttpApi如何配置控制器依赖注入和控制器的…

数据结构(终极线段树篇)

数据结构(终极线段树篇) 摘要: 问题的提出:如何解决多样化的区间操作问题? solve:线段树!!! 关键字: 线段树,可持久化线段树,权值线段…

.NET Core 3.0之深入源码理解Configuration(一)

微软在.NET Core里设计出了全新的配置体系,并以非常灵活、可扩展的方式实现。从其源码来看,其运行机制大致是,根据其Source,创建一个Builder实例,并会向其添加Provider,在我们使用配置信息的时候&#xff0…

摊还分析

摊还分析 1何为摊还分析? 摊还分析主要求解数据结构维护序列执行的所有操作的平均时间,来评价操作的代价,从而保证最坏情况下每个操作的平均性能。 2聚合分析 2.1何为聚合分析? 若长度为n的操作序列最坏情况下所花费时间为T(…

Bigraph Extension

Bigraph Extension 题意: 有2n个点,n为偶数,n个点属于集合A,n个点属于集合B。起初在途中有m个无向边,边的两侧端点分别在两个集合里,任何两个边都没有公共交点。 现在你可以执行任意次操作: 在…

微服务划分的姿势

我们知道微服务是一种理念,没有确切的定义和边界,好比设计原则,是属于抽象的概念。在定义不明确的情况下谈划分也是一种各说各话,具体问题需要具体分析,所以这篇文章谈到的划分也不是绝对标准,仅供参考。有…

点(树)分治

0.引言 对于树上问题&#xff0c;有许多特殊的求解方法&#xff0c;如&#xff1a;树链剖分。点分治算法也是其中之一&#xff0c;常用于解决树上路径问题。 1.0.问题的引入 给定一棵树&#xff0c;求这棵树的直径&#xff08;树上最长链长度&#xff0c;n<10^5&#xff…

斜率优化Convex Hull Trick

斜率优化 一、简单DP 首先从一道简单题引入。 [IOI2002]任务安排 Description N个任务排成一个序列在一台机器上等待完成&#xff08;顺序不得改变&#xff09;&#xff0c;这N个任务被分成若干批&#xff0c;每批包含相邻的若干任务。从时刻0开始&#xff0c;这些任务被分…

分布式部署携程Apollo构建配置中心

一、开场白在系统设计里我们有很多配置希望独立于系统之外&#xff0c;而又能够被系统实时读取。但是在传统的系统设计里&#xff0c;配置信息通常是耦合在系统内的&#xff0c;比如.net里通常会放在App.config或者web.config里&#xff0c;.net core则是appsettings.json里&am…

[COCI 2017-2018-2]-San

[COCI 2017-2018-2]-San san(1s64M) 游戏世界中有N个楼从左到右排列&#xff0c;从左到右编号为1到N&#xff0c;第i幢楼的高度为Hi,楼上的金币数为Gi,游戏可以从任意一个楼开始且包涵几步。每一步玩家可以从当前位置向右跳&#xff08;可以跳过一些楼&#xff09;但必须跳到…

领域模型架构 eShopOnWeb项目分析 上

一.概述本篇继续探讨web应用架构&#xff0c;讲基于DDD风格下最初的领域模型架构&#xff0c;不同于DDD风格下CQRS架构&#xff0c;二者架构主要区别是领域层的变化。 架构的演变是从领域模型到CQRS, 一开始DDD是用领域模型的分层架构&#xff0c;用单一的领域模型处理业务逻辑…

最小生成树--Boruvka算法

参考文章 介绍 第一次听说这个算法。。 对于最小生成树一定学过prim和krusal&#xff0c;prim复杂度是O(n2)或者O(elogn)O(n^2)或者O(elogn)O(n2)或者O(elogn),krusal复杂度是O(eloge)O(eloge)O(eloge)&#xff0c;这里介绍一下Boruvka算法 Boruvka算法解决某些特定问题非常好…

[NOIP2016]愤怒的小鸟(状压DP)

[NOIP2016]愤怒的小鸟&#xff08;状压DP&#xff09; 题目描述 输入输出格式 输入格式&#xff1a; 第一行包含一个正整数 T&#xff0c;表示游戏的关卡总数。 下面依次输入这 T个关卡的信息。每个关卡第一行包含两个非负整数 n,m&#xff0c;分别表示该关卡中的小猪数量和…

给 asp.net core 写一个简单的健康检查

给 asp.net core 写一个简单的健康检查Intro健康检查可以帮助我们知道应用的当前状态是不是处于良好状态&#xff0c;现在无论是 docker 还是 k8s 还是现在大多数的服务注册发现大多都提供了健康检查机制来检测应用的健康状态&#xff0c;如果应用本身就提供一个健康检查的机制…

从阿里中台战略看企业IT架构转型之道(下)

此文是我阅读《企业IT架构转型之道》一书的学习笔记的下半部分&#xff0c;所有内容出自钟华老师的这本书。上半部分Part1~Part5请点击这里Part 6 异步与缓存原则异步化事务 > 核心是ACID柔性事务 > 基础是CAP理论和BASE理论&#xff0c;因为互联网应用最核心的需求是高可…

CF1543C. Need for Pink Slips

CF1543C. Need for Pink Slips 题意&#xff1a; 题解&#xff1a; 其实具体的计算方法在说明里面都写了&#xff1a;对于第一个数据&#xff1a; 0.2 0.2 0.6 0.2组成方案如下&#xff1a; 就是c和m如果大于v就减&#xff0c;小于v就变成0&#xff0c;到p直接停止 所以直接…