本章解释 Chronicle Queue 的底层实现细节。
Append-Only 数据结构
Chronicle Queue 专为顺序写入和读取而设计。 它还支持随机访问和就地更新。 虽然无法更改现有条目的大小,但可以填充条目以供将来使用。
这种append-only结构对于使用“CPU L2 缓存”一致性总线在线程之间传递数据更有效。 它还可以比尝试在线程之间传递对象更快,因为它避免了随机访问,这在 Java 对象中很常见,其中可能存在大量引用追踪。 此外,持久化到磁盘更高效; HDD 和 SSD 在顺序访问时效率更高,并简化了复制。
什么是内存映射文件?
Chronicle Queue 建立在 Chronicle Bytes 中名为MappedBytes
的类上。 这将文件可视化为映射到文件的无限字节数组。 随着数据的追加,队列将透明地添加内存映射,并在写入数据时增长。
使用内存映射文件的主要好处是队列不再受 JVM 大小甚至主内存大小的限制。 相反,限制是可用磁盘空间。 如果您想将 100 TB 加载到 JVM 中以进行重播,操作系统会为您完成所有繁重的工作。
使用内存映射文件的另一个好处是能够将一部分内存绑定到一个对象。 标头中的关键属性在第一次加载时绑定,之后它们像普通对象一样工作,以线程安全的方式更新堆外内存和文件。 启用诸如“compareAndSet”、原子添加或设置最大值(只会增加值的集合)之类的操作。 由于数据访问是线程安全的,它可以在线程或进程之间共享,与 L2 缓存未命中所花费的时间一样快; 最多 25 纳秒。
队列头
每个队列都以存储基本信息的标头开头,例如 wire类型、滚动周期以及索引是如何执行的。
下面是队列标头的示例:
--- !!meta-data #binary // <1>
header: !SCQStore { // <2>
wireType: !WireType BINARY,
writePosition: 413, // <3>
roll: !SCQSRoll { // <4>
length: !int 86400000,
format: yyyyMMdd,
epoch: 0
},
indexing: !SCQSIndexing { // <5>
indexCount: !short 16384,
indexSpacing: 16,
index2Index: 0,
lastIndex: 0
},
lastAcknowledgedIndexReplicated: 0 // <6>
}
<1>: 第1条消息是以2进制形式编写的元数据
<2>: 标头类型的别名为SCQStore
。
<3>:writePosition
是第一个绑定值。它是已知的被写入的最高字节,并被原子更新。
<4>: 滚动周期是每天1次。
<5>: 此类控制如何按需对其进行索引。 它为索引查找添加元数据项。
<6>: 副本确认的最高消息索引。
📝注意:
SCQStore
“引导”队列本身。 可以提供自定义实现来调整队列的行为,前提是它支持相同的接口。 滚动和索引策略也可以定制
队列文件
队列消息由两部分组成,一个 4 字节的标头,后跟一个 摘录。 摘录,也称为消息,包含实际数据,可以是任何类型,包括文本、数字或序列化的 blob。 无论何种类型,所有信息都存储为摘录中的一系列字节。
图 1. Chronicle Queue 存储有序的消息集合
头的前 30 位包含数据的长度,以便读者知道摘录的大小。 剩余的两个位保留用于:
- 此消息是内部使用的用户数据还是支持队列本身所需的元数据。
- 消息是否完整。
不完整的消息
当消息不完整时,无法阅读。 但是,如果已知长度,编写器可以跳过此类消息并尝试在其后写入。
例如:
- 线程1正在写消息,但它知道消息长度; 它可以从完成标题开始。
- 线程2可以看到不完整的消息头,找地方写的时候跳过这条消息。
这样,多个线程可以同时写入队列。 任何被检测为错误的消息(例如,线程死亡)都可以标记为元数据并被读者跳过。
<<<<<<<<<<<< [完] >>>>>>>>>>>>