配景

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+树索引信息还未竖立,则须要举行全文扫码;
Last modification:March 25, 2020
如果觉得我的文章对你有用,请随意赞赏