Rust高效序列化存储库sequential-storage的使用,提供顺序数据存储与快速读写功能
Rust高效序列化存储库sequential-storage的使用,提供顺序数据存储与快速读写功能
Sequential-storage是一个用于在闪存中存储数据并最小化擦除周期的Rust库。它提供了两种数据结构:
- Map:键值对存储
- Queue:FIFO存储
每个数据结构都位于自己的模块中。请参阅模块文档以获取更多信息和示例。
注意: 确保不要在闪存中混合使用数据结构!例如,不能从推送队列的闪存区域中获取键值项。
状态
该库已在Tweede golf内部和外部的多个生产项目中使用。
闪存中的表示形式尚未稳定。这也遵循语义化版本控制:
- 对闪存表示形式的重大更改将导致主版本号增加
- 功能添加将导致次版本号增加
- 这总是向后兼容的。例如,由1.0.0创建的数据可由1.1.0使用
- 这可能不向前兼容。例如,由1.0.1创建的数据可能无法由1.0.0使用
- 1.0之后,补丁版本仅修复错误且不更改闪存表示形式
示例
以下是使用sequential-storage的完整示例代码:
use sequential_storage::map::{Map, NoCache};
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
// 假设我们有一个实现NorFlash trait的闪存设备
struct MockFlash {
data: Vec<u8>,
page_size: usize,
}
impl NorFlash for MockFlash {
const READ_SIZE: usize = 1;
const WRITE_SIZE: usize = 1;
const ERASE_SIZE: usize = 4096; // 假设页大小为4KB
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
// 擦除闪存区域
Ok(())
}
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
// 写入数据到闪存
Ok(())
}
}
impl ReadNorFlash for MockFlash {
const READ_SIZE: usize = 1;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
// 从闪存读取数据
Ok(())
}
fn capacity(&self) -> usize {
self.data.len()
}
}
fn main() {
// 创建模拟闪存设备
let mut flash = MockFlash {
data: vec![0xFF; 4096 * 4], // 4页闪存
page_size: 4096,
};
// 创建Map实例,不使用缓存
let mut map = Map::<_, u32, String, NoCache>::new(&mut flash);
// 存储键值对
map.insert(&1, &"Value1".to_string()).unwrap();
map.insert(&2, &"Value2".to_string()).unwrap();
// 读取值
let value = map.get(&1).unwrap();
println!("Value for key 1: {}", value);
// 删除键
map.remove(&1).unwrap();
// 检查键是否存在
println!("Contains key 1: {}", map.contains_key(&1).unwrap());
}
完整示例代码
以下是一个更完整的示例,展示了如何使用sequential-storage的Map和Queue功能:
use sequential_storage::{map::{Map, PagePointerCache}, queue::Queue};
use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
// 实现NorFlash trait的闪存模拟
struct FlashDevice {
data: Vec<u8>,
erase_size: usize,
}
impl NorFlash for FlashDevice {
const READ_SIZE: usize = 1;
const WRITE_SIZE: usize = 1;
const ERASE_SIZE: usize = 4096; // 4KB页大小
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
let start = from as usize;
let end = to as usize;
self.data[start..end].fill(0xFF);
Ok(())
}
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
let start = offset as usize;
self.data[start..start + bytes.len()].copy_from_slice(bytes);
Ok(())
}
}
impl ReadNorFlash for FlashDevice {
const READ_SIZE: usize = 1;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
let start = offset as usize;
bytes.copy_from_slice(&self.data[start..start + bytes.len()]);
Ok(())
}
fn capacity(&self) -> usize {
self.data.len()
}
}
fn main() {
// 初始化闪存设备 (8页,每页4KB)
let mut flash = FlashDevice {
data: vec![0xFF; 4096 * 8],
erase_size: 4096,
};
// 使用PagePointerCache的Map示例
{
let mut map = Map::<_, u32, String, PagePointerCache>::new(&mut flash);
// 插入数据
map.insert(&1, &"Config1".to_string()).unwrap();
map.insert(&2, &"Config2".to_string()).unwrap();
// 读取并打印数据
if let Some(val) = map.get(&1).unwrap() {
println!("Config1: {}", val);
}
// 更新数据
map.insert(&1, &"UpdatedConfig1".to_string()).unwrap();
// 遍历所有键
for key in map.keys().unwrap() {
println!("Key: {}", key);
}
}
// Queue示例
{
let mut queue = Queue::new(&mut flash);
// 推送数据
queue.push(b"Message1").unwrap();
queue.push(b"Message2").unwrap();
// 弹出并打印数据
while let Some(data) = queue.pop().unwrap() {
println!("Popped: {:?}", data);
}
}
}
特性
- 键值数据存储(Map)
- 用户定义的键和值类型
- 非常适合创建配置
- FIFO队列数据存储(Queue)
- 在内存中存储字节切片
- 非常适合缓存数据
- 简单API:
- 只需调用你认为需要的函数
- 无需担心其他任何事情
- 项目头CRC保护
- 项目数据CRC保护
- 断电安全
- 系统总是正常或完全可恢复
- 忽略损坏的项目
- 可选缓存以加速操作
- 磨损均衡
- 页面循环使用,因此所有页面都会被擦除相同的次数
- 基于embedded-storage构建
- 这是唯一必需的依赖项
缓存选项
有多种缓存选项可以加速操作:
缓存类型 | RAM字节 | Map闪存读取次数 | Map闪存读取字节 | Queue闪存读取次数 | Queue闪存读取字节 |
---|---|---|---|---|---|
NoCache | 0 | 100% | 100% | 100% | 100% |
PageStateCache | 1 * 页数 | 77% | 97% | 41% | 85% |
PagePointerCache | 9 * 页数 | 70% | 89% | 6% | 14% |
KeyPointerCache | 9 * 页数 + (sizeof(KEY) + 4) * 键数 | 6.2% | 8.2% | - | - |
内部工作原理
为了节省擦除周期,该库仅在页面上真正追加数据。具体如何完成取决于使用的是Map还是Queue。
页面和页面状态
闪存被划分为多个页面。页面可以处于三种状态:
- 打开 - 页面处于擦除状态
- 部分打开 - 页面已被写入,但尚未满
- 关闭 - 页面已被完全写入
页面的状态被编码到页面的第一个和最后一个字中。如果两个字都是FF(擦除),则页面是打开的。如果第一个字用标记写入,则页面是部分打开的。如果两个字都被写入,则页面是关闭的。
项目结构
所有数据都存储为一个项目。一个项目由包含数据长度的头、该长度的CRC、数据CRC和一些数据组成。当其数据CRC字段为0时,项目被视为已擦除。
长度是一个u16,因此任何项目不能长于0xFFFF或页面大小 - 项目头(填充到字边界) - 页面状态(2个字)
。
Map工作流程
Map将每个键值存储为一个项目。每个新值都附加在最后一个部分打开的页面或最后一个关闭页面之后的第一个打开页面上。
一旦页面满了,它将被关闭,下一个页面将需要存储项目。然而,有时需要擦除一个旧页面。检查要擦除页面上所有项目。如果在页面上找到的项目没有比更新的值,它将从要擦除的页面复制到当前部分打开的页面。
Queue工作流程
当推送时,搜索放置项目的最年轻位置。如果它不适合,它将返回错误或擦除旧页面(如果指定它可以)。
窥视和弹出查看它能找到的最旧数据。当弹出时,项目也会被擦除。当使用peek_many时,可以查看从最旧到最新的所有数据。
Rust高效序列化存储库sequential-storage使用指南
介绍
sequential-storage是一个专注于高效顺序数据存储与快速读写的Rust库。它提供了简单的API来存储和检索序列化数据,特别适合日志记录、事件溯源和时间序列数据等场景。
主要特性
- 高效的顺序写入性能
- 快速的顺序读取能力
- 支持任意可序列化类型
- 内置压缩选项
- 线程安全设计
使用方法
添加依赖
首先在Cargo.toml中添加依赖:
[dependencies]
sequential-storage = "0.3"
serde = { version = "1.0", features = ["derive"] }
基本使用示例
use sequential_storage::SequentialStorage;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Event {
timestamp: u64,
message: String,
severity: u8,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 创建或打开存储文件
let mut storage = SequentialStorage::open("events.bin")?;
// 写入数据
let event1 = Event {
timestamp: 1625097600,
message: "System started".to_string(),
severity: 1,
};
let event2 = Event {
timestamp: 1625097605,
message: "User logged in".to_string(),
severity: 2,
};
storage.write(&event1)?;
storage.write(&event2)?;
// 读取所有数据
let events: Vec<Event> = storage.read_all()?;
for event in events {
println!("{:?}", event);
}
Ok(())
}
高级用法
使用压缩
use sequential_storage::{SequentialStorage, Compression};
let mut storage = SequentialStorage::open("compressed_events.bin")?
.with_compression(Compression::Zstd);
批量写入
let events = vec![
Event { /* ... */ },
Event { /* ... */ },
Event { /* ... */ },
];
storage.write_batch(&events)?;
迭代读取
let mut iter = storage.iter::<Event>();
while let Some(event) = iter.next()? {
println!("{:?}", event);
}
性能优化建议
- 对于大批量写入,使用
write_batch
而不是多次write
- 考虑使用压缩,特别是对于文本数据
- 适当调整缓冲区大小
- 对于频繁访问的数据,可以缓存读取结果
注意事项
- 存储文件不是跨平台兼容的,不同架构可能需要重新生成
- 删除数据需要重建存储文件
- 确保所有存储的类型都实现了
Serialize
和Deserialize
sequential-storage为顺序数据存储提供了简单高效的解决方案,特别适合需要快速追加写入和顺序读取的场景。
完整示例代码
下面是一个完整的示例,展示了sequential-storage库的主要功能:
use sequential_storage::{SequentialStorage, Compression};
use serde::{Serialize, Deserialize};
use std::error::Error;
// 定义要存储的数据结构
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct LogEntry {
id: u64,
level: String,
content: String,
}
fn main() -> Result<(), Box<dyn Error>> {
// 示例1:基本读写操作
basic_operations()?;
// 示例2:使用压缩
with_compression()?;
// 示例3:批量操作
batch_operations()?;
Ok(())
}
// 基本操作示例
fn basic_operations() -> Result<(), Box<dyn Error>> {
println!("=== 基本操作示例 ===");
// 创建或打开存储文件
let mut storage = SequentialStorage::open("basic_logs.bin")?;
// 写入单个日志条目
let entry1 = LogEntry {
id: 1,
level: "INFO".to_string(),
content: "Application started".to_string(),
};
storage.write(&entry1)?;
// 读取所有条目
let entries: Vec<LogEntry> = storage.read_all()?;
println!("读取到 {} 条日志", entries.len());
assert_eq!(entries[0], entry1);
Ok(())
}
// 使用压缩的示例
fn with_compression() -> Result<(), Box<dyn Error>> {
println!("\n=== 使用压缩示例 ===");
// 创建带压缩的存储
let mut storage = SequentialStorage::open("compressed_logs.bin")?
.with_compression(Compression::Zstd);
// 写入多个日志条目
for i in 0..5 {
let entry = LogEntry {
id: i,
level: if i % 2 == 0 { "INFO".to_string() } else { "WARN".to_string() },
content: format!("Log message {}", i),
};
storage.write(&entry)?;
}
// 使用迭代器读取
let mut iter = storage.iter::<LogEntry>();
let mut count = 0;
while let Some(entry) = iter.next()? {
println!("{:?}", entry);
count += 1;
}
println!("共读取 {} 条压缩日志", count);
Ok(())
}
// 批量操作示例
fn batch_operations() -> Result<(), Box<dyn Error>> {
println!("\n=== 批量操作示例 ===");
let mut storage = SequentialStorage::open("batch_logs.bin")?;
// 准备批量数据
let mut batch = Vec::new();
for i in 0..1000 {
batch.push(LogEntry {
id: i,
level: "DEBUG".to_string(),
content: format!("Debug message {}", i),
});
}
// 批量写入
storage.write_batch(&batch)?;
println!("已批量写入 1000 条日志");
// 读取并验证
let entries: Vec<LogEntry> = storage.read_all()?;
assert_eq!(entries.len(), 1000);
println!("成功读取 1000 条日志");
Ok(())
}
这个完整示例展示了:
- 基本的单个数据读写操作
- 使用Zstd压缩存储数据
- 批量写入大量数据的高效操作
- 使用迭代器逐步读取数据
- 完整的数据序列化和反序列化过程
您可以根据实际需求调整数据结构、压缩方式和批量操作的大小。