背景

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

EventStore文件存储设计的更多相关文章

  1. Kafka深入理解-1:Kafka高效的文件存储设计

    文章摘自:美团点评技术团队  Kafka文件存储机制那些事 Kafka是什么 Kafka是最初由Linkedin公司开发,是一个分布式.分区的.多副本的.多订阅者,基于zookeeper协调的分布式日 ...

  2. 海量小文件存储与Ceph实践

    海量小文件存储(简称LOSF,lots of small files)出现后,就一直是业界的难题,众多博文(如[1])对此问题进行了阐述与分析,许多互联网公司也针对自己的具体场景研发了自己的存储方案( ...

  3. kafka知识体系-kafka设计和原理分析-kafka文件存储机制

    kafka文件存储机制 topic中partition存储分布 假设实验环境中Kafka集群只有一个broker,xxx/message-folder为数据文件存储根目录,在Kafka broker中 ...

  4. 深入理解Sqlserver文件存储之页和应用 (转)

    我们每天都在使用数据库,我们部门使用最多的关系数据库有Sqlserver,Oracle,有没有想过这些数据库是怎么存放到操作系统的文件中的?有时候为了能够设计出最优的表结构,写出高性能的Sqlserv ...

  5. AppBoxFuture(五): 分布式文件存储-Store Everything

      本来本篇是想介绍前端组件化开发用户界面,发现框架还未实现文件存储,原本计划是后续设计开发的,索性把计划提前,所以本篇将介绍基于Raft实现分布式的文件存储引擎. 一. 实现思路   既然是分布式存 ...

  6. Kafka文件存储机制及partition和offset

    转载自:  https://yq.aliyun.com/ziliao/65771 参考:  Kafka集群partition replication默认自动分配分析    如何为kafka选择合适的p ...

  7. Hadoop小文件存储方案

    原文地址:https://www.cnblogs.com/ballwql/p/8944025.html HDFS总体架构 在介绍文件存储方案之前,我觉得有必要先介绍下关于HDFS存储架构方面的一些知识 ...

  8. Kafka#4:存储设计 分布式设计 源码分析

    https://sites.google.com/a/mammatustech.com/mammatusmain/kafka-architecture/4-kafka-detailed-archite ...

  9. Kafka文件存储机制及offset存取

    Kafka是什么 Kafka是最初由Linkedin公司开发,是一个分布式.分区的.多副本的.多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx ...

随机推荐

  1. [COI2007] Patrik 音乐会的等待 (单调栈,模拟)

    题目描述 N个人正在排队进入一个音乐会.人们等得很无聊,于是他们开始转来转去,想在队伍里寻找自己的熟人.队列中任意两个人A和B,如果他们是相邻或他们之间没有人比A或B高,那么他们是可以互相看得见的. ...

  2. 做运动(Dijkstra+并查集+MST)

    上面的题解是这样,这道题我真的脑残,其实打代码的时候就意识到了许多,可以用Dfs+Dij+二分,这样还可以卡一卡 但是我打了spfa+spfa+二分,这个显然很慢,类似的题目我好像还做过一道的,就是在 ...

  3. vs2005做的留言本——天轰川下载

    原文发布时间为:2008-08-01 -- 来源于本人的百度文章 [由搬家工具导入] 这个虽然单纯是个留言本,但是在功能上我都使用了尽量不重复的解决方法,所以我自认为非常适合入门级的朋友看,而且用了我 ...

  4. PatentTips - GPU Support for Blending

    Graphics processing units (GPUs) are specialized hardware units used to render 2-dimensional (2-D) a ...

  5. 关于制表符\t

    “制表符代表八个空格”的说法不准确.制表符的作用是将光标移到最接近8的倍数的位置,使得后面的输出从此开始.换句话说,如果所有数据都紧跟在制表符后面输出,则这些数据只能从第9列.第17列.第25列... ...

  6. Scrapy学习-3-Request回调巧用

    基于twisted的异步回调 使得页面爬取有阶段性和连续性 from scrapy.http import Request from urllib import parse def parse(sel ...

  7. Codeforces 849B Tell Your World (计算几何)

    题目链接 Tell Your World 题意 给出N个点(i, xi),问是否存在两条平行的直线,使得每一个点恰好在两条直线的其中一条上. 每条直线必须穿过至少一个点. 考虑每个点和第1个点的斜率, ...

  8. 更改bootstrap的默认样式

    很久没用bootstrap,对与按自己的需求修改样式都忘了. 一上来就添加了新的class类,重写css样式让其覆盖原有的样式,实际上不起作用.因为没考虑的选择器的优先级.面试的时候会问到一些这个问题 ...

  9. Liunx 下Redis 的安装

    一.Redis 的简介 Redis是一款开源的.高性能的键-值存储.它常被称作是一款数据结构服务器,它是一个key-value存储系统.和Memcache类似,Memecache只支持字符窜的数据类型 ...

  10. TreeView 与 ListView

    ListView: viewStyle icon 大图标 list  列表,单列 report 报表 smallIcon 小图标 largeImage 与icon对应 smallImage 与saml ...