本地优先的笔记应用,数据到底怎么管?——SQLite + 空间隔离 + 内容寻址附件的工程拆解

0. 背景:为什么"数据主权"是硬需求

云笔记把数据放在它家服务器,用的是"使用权"。对一款本地优先 的笔记应用,核心命题是:默认数据就在本机,离线也能完整用,格式开放可迁移。这决定了存储层要优先考虑:

  1. 可移植:数据能从本机拷走、能导出;
  2. 可控:不依赖账号/云,服务停了数据也不丢;
  3. 可离线:全文检索、附件、备份都要在本机闭环。

本文拆解 ShuyoNote 的存储层设计(本地优先笔记),核心是 「权威数据源 + 派生索引」双写 ,以及空间隔离 + 内容寻址附件。

1. 整体存储布局:meta.db + spaces/

数据分两层:

bash 复制代码
{app-data}/
├── meta.db                 # 应用级:工作空间清单 / 模板 / 插件状态 / 同步配置
└── spaces/
    └── {ws_id}/            # 每个工作空间一个独立的 SQLite 库(物理隔离)
        └── .db             # 该空间的所有页面/块/附件索引/关系
attachments/                # 全局内容寻址附件(跨空间去重)

为什么每空间独立库(物理隔离):

  • 隔离:一个空间的数据损坏/迁移不影响其它空间;
  • 导出/导入:单空间能打成自包含 zip(含其附件子集)搬走,不污染其它空间;
  • 多空间并存 :应用级状态(工作空间清单、模板、同步配置)放 meta.db,空间级数据互不干扰。

meta.db 管"有哪些空间、模板、插件、同步配置";spaces/{id}/ 管"这个空间里有什么内容"。两者职责清晰。

2. 数据模型:页 = 一份文档,块是一等公民

把"页"抽象为一棵文档树,块是树的节点:

bash 复制代码
pages(id, parent_id, kind, title, content, ...)   # 页/文件夹,parent_id 构成树
blocks(id, page_id, block_id, ...)                 # 块索引:块 -> 页 反查
backlinks(id, src, dst, kind, ...)                 # 页面级 + 块级引用关系
  • 一页 = 一份文档 :编辑器(Lexical)把页内容序列化存储,块是 Lexical 根级节点,每个顶层块带稳定的 blockId。
  • 块可引用/反链 :((块ID)) 块引用 → blocks 反查;blocks 表维护「块→页」,backlinks 记录页面+块级引用关系,支撑块级反链、关系图。
  • 为什么块独立寻址:块是"可复用"的最小单元(引用、嵌入、反链都要定位到块级),所以给块一个稳定 ID,并建反向索引。

3. 附件:内容寻址(SHA-256)

附件不走"路径-文件名",而是内容寻址:附件按内容哈希存储,相同内容只存一份。

bash 复制代码
attachments/
  ab/                 # 桶:取内容 sha256 的前 2 个十六进制字符,避免单目录文件过多
    3f2a8e...         # 文件名 = 内容的完整 sha256(64 位 hex),内容即地址
  • 去重:跨文件夹/空间,同一张图、同一个文件只存一份(内容寻址天然去重,省空间)。
  • 引用 :页面/块里存 hash 引用,而不是复制字节;引用到页面时用"文件卡片 + 下载"。
  • 可靠:内容即地址,篡改会造成 hash 不匹配(可校验),也是同步增量传输的基础(只传变化的部分)。

代价 :需要一层「hash → 附件元数据」的映射(attachments 表:hash、size、mime、引用计数),并处理孤立附件清理(以引用集为准回收未被引用的字节)。

4. 全文检索:SQLite FTS5 + trigram 做中文

本地要在本机就能搜,不联网。用 SQLite FTS5:

sql 复制代码
CREATE VIRTUAL TABLE post_search USING fts5(title, body, tags, tokenize='trigram');
-- 写入时同步索引;查询时 MATCH + LIKE 兜底
  • trigram 分词 :中文按 3-gram 切分,支持子串检索(中文不靠空格分词,trigram 能覆盖"中文子串")。
  • 可重建 :FTS 是派生索引,可由正文(权威数据源)重建,坏了/旧了能重新生成。
  • 兜底 :短词(不足 3 个字符)FTS 匹配不到,用 LIKE '%xxx%' 兜底(中文常见场景)。

关键原则:权威数据源(正文)+ 派生索引(FTS)分离,索引可丢可重建,数据不因索引损坏丢失。

5. 离线 / 多端:WAL + 备份 + 加密

  • WAL 模式 :日志先行,写入快、崩溃安全,且读不阻塞写(可做读写分离 + 只读连接池)。
  • 自动保存:防抖写入,无"保存"按钮。
  • 版本历史:每次保存前快照,可回滚(本地数据的安全网)。
  • 回收站:软删除 + 恢复 + 彻底删除。
  • 整库备份:导出/导入 zip(数据库一致性快照 + 附件目录)。
  • 端到端加密:口令 → Argon2id → 会话内存密钥(不落盘)→ 加密空间库与附件(XChaCha20-Poly1305);锁定态不读。

6. 一段实现(rusqlite + 幂等迁移)

rust 复制代码
// SQLite 连接 + 迁移
let conn = Connection::open(db_path)?;
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.execute_batch(r#"
    CREATE TABLE IF NOT EXISTS pages(id INTEGER PRIMARY KEY, parent_id INTEGER, title TEXT, kind TEXT, content TEXT);
    CREATE TABLE IF NOT EXISTS attachments(hash TEXT PRIMARY KEY, size INTEGER, mime TEXT);
    CREATE VIRTUAL TABLE IF NOT EXISTS post_search USING fts5(title, body, tags, tokenize='trigram');
"#)?;

要点:幂等迁移 (IF NOT EXISTS,版本化);外键 + WAL + pragma 初始化 ;派生索引与权威源分离。

7. 取舍与坑

  1. 双写(正文 + FTS)的一致性:FTS 索引写失败要能重建,别让索引成为单点。
  2. 附件内容寻址的引用管理:删页要处理"引用计数",孤立附件靠后台清理,别一把梭删字节。
  3. 每空间独立库 vs 全库一个 db :独立库隔离好、迁移简单,但全空间搜索要跨库合并(对多库做 UNION/FTS 聚合并结果)。
  4. 中文检索:纯 FTS 对不足 3 个字符的中文不友好,必须配 LIKE 兜底。
  5. 备份一致性 :SQLite WAL 下要拿到一致快照(VACUUM INTO / 在线 backup API),别直接拷文件。

8. 结论

本地优先笔记的存储层,核心是权威数据源 + 派生索引分离 、空间物理隔离 、附件内容寻址 、FTS5 中文检索 、WAL + 备份 + 加密。这套设计让"数据真正在自己手里"落地:

  • 数据在本地、格式可导出(能成 Markdown);
  • 索引可重建、备份可还原;
  • 附件去重省空间、内容可校验。

(本文为 ShuyoNote 存储层工程笔记,欢迎评论区交流取舍。)

相关推荐
数据工匠老o4 小时前
"能用应用层解决的不用存储过程",十年数据库经验总结
数据库·架构
弈栈录5 小时前
Java AI 应用的异步化与高并发设计
java·后端·架构
硅基手札5 小时前
【risc-v专栏 06b】内存架构深挖:PMA / ePMP / Cache / CMO / MMU 细节 / RVWMO 形式化
架构·risc-v
CAD开发小慧6 小时前
MFC工业软件换上4K屏后界面模糊?BCGControlBar Pro如何适配高DPI与多屏
架构
qwfy6 小时前
我要用 AI-native 架构,从零重建一套制造业 ERP/MES
vue.js·spring boot·架构
航飞光电市场经理7 小时前
人员定位系统“全栈自研”技术解析:从射频前端到定位引擎的架构拆解
前端·架构
szxinmai主板定制专家7 小时前
RK3576+CODESYS+RK182X+FPGA异构架构:半导体精密设备一体化控制器方案
人工智能·fpga开发·架构·rk3576+codesys
szxinmai主板定制专家7 小时前
半导体设备控制器:RK3576+CODESYS+RK182X异构架构,实现晶圆视觉检测与运动联动闭环
人工智能·fpga开发·架构·视觉检测·边缘计算·codesys·rk3576
落魄实习生8 小时前
Agent Scope Java 2.x 系列【1】核心架构
java·开发语言·架构
安易算力8 小时前
智算中心网络架构选型:RDMA、InfiniBand与以太网技术深度解析
网络·架构