lite-oss-db
v1.1.1
Published
Lightweight SQLite database with real-time OSS sync
Maintainers
Readme
lite-oss-db
轻量级 SQLite 数据库,实时同步到阿里云 OSS(或自定义对象存储)。
特性
- 🔄 双模式架构 — 读写模式(单写者 + 自动同步)与只读模式(轮询 + 自动刷新)
- ☁️ 实时备份 — 本地 SQLite 文件变化后自动同步快照到 OSS(防抖机制)
- 🔒 分布式锁 — 单写多读并发模型,基于 OSS 文件锁实现
- 🛡️ 自我修复 — 运行时本地文件意外删除时,自动从内存恢复并重建文件
- 🏷️ ETag 变化检测 — 精确识别远程变化,避免不必要的下载
- 🔐 可选加密 — AES-256-CTR + HMAC-SHA256,流式加解密,低内存占用
- 📦 定时备份 — 可配置的定时备份到独立前缀,支持自动清理旧备份
- 🚀 零核心依赖 — 仅一个运行时依赖 (
chokidar),核心逻辑全部基于 Node.js 原生 API - 🔌 运行时无关 — 通过适配器模式支持
better-sqlite3(Node.js)和bun:sqlite(Bun)
目录
安装
lite-oss-db 的核心不直接依赖任何 SQLite 驱动或 OSS 客户端。你需要根据使用环境安装对应的 peer dependencies。
Node.js 环境
pnpm add lite-oss-db ali-oss better-sqlite3
# 或
npm install lite-oss-db ali-oss better-sqlite3Bun 环境
bun add lite-oss-db ali-oss
# bun:sqlite 是 Bun 内置模块,无需额外安装快速开始
Node.js 环境(better-sqlite3)
import { OssDb, AliyunOssAdapter, BetterSqlite3Adapter } from 'lite-oss-db';
import OSS from 'ali-oss';
import Database from 'better-sqlite3';
// 1. 创建 OSS 客户端
const client = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID!,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET!,
bucket: 'my-app-db-bucket',
});
// 2. 创建 OssDb 实例
const db = new OssDb({
dbPath: './local.db',
adapter: new AliyunOssAdapter(client),
remotePath: 'prod/app.sqlite',
dbFactory: BetterSqlite3Adapter.createFactory(Database),
syncDebounceMs: 2000,
lockTtlMs: 10000,
});
// 3. 初始化(自动从 OSS 恢复 + 获取锁)
await db.init();
// 4. 像普通 SQLite 一样使用
db.db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
db.db.prepare('INSERT INTO users (name) VALUES (?)').run('Alice');
// 查询
const users = db.db.prepare('SELECT * FROM users').all();
console.log(users);
// 5. 关闭(自动最后一次同步 + 释放锁)
await db.close();Bun 环境(bun:sqlite)
BunSqliteAdapter 是内置的 preset,与 BetterSqlite3Adapter 使用方式一致。
import { OssDb, AliyunOssAdapter, BunSqliteAdapter } from 'lite-oss-db';
import { Database } from 'bun:sqlite';
import OSS from 'ali-oss';
// 1. 创建 OSS 客户端
const client = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID!,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET!,
bucket: 'my-app-db-bucket',
});
// 2. 创建 OssDb 实例(注意 dbFactory 使用 BunSqliteAdapter)
const db = new OssDb({
dbPath: './local.db',
adapter: new AliyunOssAdapter(client),
remotePath: 'prod/app.sqlite',
dbFactory: BunSqliteAdapter.createFactory(Database),
syncDebounceMs: 2000,
lockTtlMs: 10000,
});
// 3. 初始化
await db.init();
// 4. 使用方式与 Node.js 完全相同
db.db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
db.db.prepare('INSERT INTO users (name) VALUES (?)').run('Alice');
const users = db.db.prepare('SELECT * FROM users').all();
console.log(users);
// 5. 关闭
await db.close();Node.js vs Bun 区别总结: 唯一的区别是
dbFactory参数:
- Node.js:
BetterSqlite3Adapter.createFactory(Database)—Database来自better-sqlite3- Bun:
BunSqliteAdapter.createFactory(Database)—Database来自bun:sqlite其余所有 API(
init、close、db.exec、db.prepare、事件等)完全一致。
只读模式(适用于两种环境)
只读模式适用于需要跟踪远程数据库变化的读副本场景(如 API 服务器、Dashboard、报表系统等)。
const replica = new OssDb({
mode: 'readonly',
dbPath: './local-replica.db',
adapter: new AliyunOssAdapter(client),
dbFactory: BetterSqlite3Adapter.createFactory(Database), // 或 BunSqliteAdapter.createFactory(Database)
poll: {
intervalMs: 10000, // 每 10 秒检查远程更新
autoReload: true, // 检测到变化后自动刷新数据库
},
});
await replica.init();
// 方式一:监听事件,在数据更新时执行操作
replica.on('data:updated', () => {
console.log('数据已刷新!');
const users = replica.db.prepare('SELECT * FROM users').all();
console.log('最新数据:', users);
});
// 方式二:手动检查更新
const changed = await replica.checkForUpdates();
if (changed) {
console.log('有新数据');
}
// 方式三:检查数据库是否就绪(rehydration 期间返回 false)
if (replica.isReady) {
const count = replica.db.prepare('SELECT COUNT(*) as c FROM users').get();
console.log(count);
}
await replica.close();配置参考
OssDbConfig — 主配置
创建 OssDb 实例时传入的配置对象。
必填参数
| 参数 | 类型 | 说明 |
|:---|:---|:---|
| dbPath | string | 本地 SQLite 数据库文件路径。如 './data/app.db' |
| adapter | StorageAdapter | 存储适配器实例。内置 AliyunOssAdapter、MockAdapter,也可自定义 |
| dbFactory | DatabaseFactory | 数据库工厂函数。传入路径和选项返回 IDatabase 实例 |
DatabaseFactory签名:(path: string, options?: { readonly?: boolean }) => IDatabase
模式选项
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| mode | 'readwrite' \| 'readonly' | 'readwrite' | 工作模式。readwrite 获取锁并同步写入;readonly 以只读方式打开,可配合轮询 |
| remotePath | string | 'db.sqlite' | 远程存储中的对象路径(key)。锁文件自动派生为 {remotePath}.lock |
读写模式选项(mode: 'readwrite')
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| workerId | string | randomUUID() | 当前实例的唯一标识符,用于分布式锁。多实例部署时建议显式设置 |
| lockTtlMs | number | 10000 | 分布式锁的 TTL(毫秒)。锁每 lockTtlMs / 3 自动续期一次 |
| syncDebounceMs | number | 2000 | 文件变化后的同步防抖时间(毫秒)。在此时间内的连续写入会合并为一次同步 |
| maxWaitMs | number | 10000 | 最大等待时间(毫秒)。即使文件持续变化,到达此时间也会强制同步,防止无限延迟 |
| scheduledBackup | ScheduledBackupConfig | undefined | 定时备份配置。如果设置,将按间隔自动创建带时间戳的备份 |
只读模式选项(mode: 'readonly')
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| poll | PollConfig | undefined | 轮询配置。如果设置(即使为空对象 {}),启用远程变化检测 |
通用选项
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| logger | Logger \| false | 内置 console logger | 自定义日志实例,或传 false 完全禁用日志输出 |
| encryption | EncryptionConfig | undefined | 加密配置。如果设置,上传到 OSS 的数据会被 AES-256-CTR 加密 |
PollConfig — 轮询配置
配置只读模式下的远程变化轮询行为。
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| intervalMs | number | 30000 | 检查远程更新的间隔(毫秒) |
| autoReload | boolean | true | 检测到远程变化时是否自动重新加载数据库。设为 false 时仅发出 poll:check 事件 |
// 示例:每 5 秒检查一次,自动刷新
poll: {
intervalMs: 5000,
autoReload: true,
}
// 示例:每 60 秒检查一次,手动处理更新
poll: {
intervalMs: 60000,
autoReload: false,
}ScheduledBackupConfig — 定时备份配置
配置读写模式下的自动定时备份。备份使用 ISO 时间戳命名,存储在指定前缀下。
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| intervalMs | number | 必填 | 备份间隔(毫秒)。如 3600000 表示每小时备份一次 |
| prefix | string | 必填 | 远程存储的路径前缀。备份文件存储为 {prefix}/{timestamp}.db |
| maxBackups | number | undefined(不限制) | 保留的最大备份数。超出后自动删除最旧的备份 |
// 示例:每小时备份一次,最多保留 24 个
scheduledBackup: {
intervalMs: 3600000, // 1 小时
prefix: 'backups/prod', // 存储路径: backups/prod/2026-02-17T06-00-00-000Z.db
maxBackups: 24, // 保留最近 24 个
}EncryptionConfig — 加密配置
配置上传到 OSS 前的数据库文件加密。使用 AES-256-CTR + HMAC-SHA256。
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| key | string \| Buffer | 必填 | 加密密码(字符串)或 32 字节原始密钥(Buffer) |
| isRawKey | boolean | false | 是否直接使用 key 作为加密密钥而不进行 PBKDF2 派生。如果 key 是 32 字节 Buffer 且已满足安全要求,设为 true 可跳过派生步骤提升性能 |
// 方式一:使用密码(自动 PBKDF2 派生,100,000 次迭代)
encryption: {
key: 'my-secret-password-here',
}
// 方式二:使用原始 32 字节密钥(跳过派生)
encryption: {
key: crypto.randomBytes(32),
isRawKey: true,
}注意事项:
- 本地
.db文件始终是未加密的,方便直接使用标准 SQLite 工具访问- 仅上传到 OSS 的数据是加密的
- 传输安全依赖 HTTPS(阿里云 OSS 默认 HTTPS)
- 加密和解密都使用流式处理(
EncryptTransform/DecryptTransform),大文件不会撑爆内存
Logger — 日志接口
你可以传入自定义日志器,或使用内置的 console 日志器。
interface Logger {
debug(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
}// 使用内置日志(默认)
const db = new OssDb({ ... });
// 禁用日志
const db = new OssDb({ ..., logger: false });
// 自定义日志(如 pino, winston)
import pino from 'pino';
const db = new OssDb({
...,
logger: pino({ level: 'info' }),
});API 参考
属性
| 属性 | 类型 | 说明 |
|:---|:---|:---|
| db | IDatabase | 底层数据库实例。在读写模式下可读写,只读模式下仅可读 |
| mode | 'readwrite' \| 'readonly' | 当前工作模式 |
| isWriteMode | boolean | 是否处于写入模式(仅读写模式下获取到锁时为 true) |
| isReady | boolean | 数据库是否就绪(只读模式 rehydration 期间返回 false) |
方法
init(): Promise<void>
初始化数据库。执行以下步骤:
- 两种模式共有:从远程存储恢复数据库(hydration),使用 ETag 检测是否需要下载
- 读写模式:尝试获取分布式锁 → 成功则以读写模式打开,失败则降级为只读 → 启动文件变化监听和自动同步
- 只读模式:以只读模式打开 → 如果配置了
poll,启动轮询
await db.init();close(): Promise<void>
关闭数据库。执行以下步骤:
- 读写模式:执行最后一次同步 → 释放锁 → 停止文件监听 → 关闭数据库
- 只读模式:停止轮询 → 关闭数据库
await db.close();createBackup(name?, prefix?): Promise<string>
手动创建备份并上传到远程存储。使用 SQLite 的 Online Backup API 保证数据一致性。
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| name | string | ISO 时间戳 | 备份名称(不含 .db 后缀) |
| prefix | string | scheduledBackup.prefix || 'backups' | 远程路径前缀 |
const path = await db.createBackup('before-migration', 'backups/v2');
console.log(`备份已创建: ${path}`); // 'backups/v2/before-migration.db'listBackups(prefix?): Promise<Array<{ name, path, createdAt }>>
列出远程存储中的备份文件,按时间倒序排列。
| 参数 | 类型 | 默认值 | 说明 |
|:---|:---|:---|:---|
| prefix | string | scheduledBackup.prefix || 'backups' | 远程路径前缀 |
const backups = await db.listBackups();
// [{ name: '2026-02-17T06-00-00', path: 'backups/...', createdAt: Date }]checkForUpdates(): Promise<boolean>
仅只读模式。手动检查远程是否有更新,如果有则自动 rehydrate。
const changed = await db.checkForUpdates();
if (changed) {
console.log('数据已更新');
}on(event, listener): this
注册事件监听器。类型安全。
db.on('sync:complete', () => console.log('同步完成'));
db.on('sync:error', (err) => console.error('同步失败:', err));
db.on('data:updated', () => console.log('只读副本已刷新'));事件
同步事件(仅读写模式)
| 事件 | 回调签名 | 触发时机 |
|:---|:---|:---|
| sync:start | () => void | 开始将本地快照同步到远程 |
| sync:complete | () => void | 同步成功完成 |
| sync:error | (error: Error) => void | 同步失败 |
锁事件(仅读写模式)
| 事件 | 回调签名 | 触发时机 |
|:---|:---|:---|
| lock:acquired | () => void | 成功获取分布式锁 |
| lock:lost | () => void | 锁续期失败,丧失写入权限 |
| lock:renewed | () => void | 锁续期成功 |
恢复事件(两种模式)
| 事件 | 回调签名 | 触发时机 |
|:---|:---|:---|
| hydration:start | () => void | 开始从远程下载数据库 |
| hydration:complete | () => void | 恢复完成 |
| hydration:skipped | () => void | 本地已是最新(ETag 匹配),跳过下载 |
| hydration:error | (error: Error) => void | 恢复失败 |
轮询事件(仅只读模式)
| 事件 | 回调签名 | 触发时机 |
|:---|:---|:---|
| poll:check | () => void | 开始一次远程检查 |
| poll:no-change | () => void | 检查结果:远程无变化 |
| poll:error | (error: Error) => void | 检查失败 |
数据事件(仅只读模式)
| 事件 | 回调签名 | 触发时机 |
|:---|:---|:---|
| data:updated | () => void | 数据库已从远程刷新完成 |
架构说明
lite-oss-db 将对象存储(OSS)视为 "单一数据源"(Source of Truth)。
读写模式流程
init()
├─ hydrate: 检查 OSS → ETag 对比 → 下载/跳过
├─ acquireLock: 原子创建 {remotePath}.lock
│ ├─ 成功 → Writer 模式
│ │ ├─ 打开 DB (readwrite)
│ │ ├─ 启动文件监听 (chokidar)
│ │ ├─ 启动锁续期 (每 lockTtlMs/3)
│ │ └─ [可选] 启动定时备份
│ └─ 失败 → 降级为 Reader(只读打开)
└─ ready
文件变化 → debounce(syncDebounceMs) → maxWait(maxWaitMs) → snapshot()
├─ SQLite backup API → .backup 文件
├─ [可选] 加密
├─ 上传到 OSS
└─ 清理 .backup 文件
close()
├─ 最后一次 snapshot()
├─ 释放锁 (delete .lock)
├─ 停止文件监听
└─ 关闭 DB只读模式流程
init()
├─ hydrate: 检查 OSS → ETag 对比 → 下载/跳过
├─ 打开 DB (readonly)
└─ [可选] 启动 PollManager
PollManager (每 intervalMs):
├─ head(remotePath) → 获取 ETag
├─ ETag 相同 → emit('poll:no-change')
└─ ETag 不同 → rehydrate()
├─ 关闭 DB
├─ 重新下载
├─ 重新打开 DB (readonly)
└─ emit('data:updated')
close()
├─ 停止 PollManager
└─ 关闭 DB并发与分布式锁
为防止多实例同时写入导致数据冲突,lite-oss-db 使用基于 OSS 文件的 单写者租约(Single Writer Lease):
- 锁文件:
{remotePath}.lock— JSON 文件,包含workerId和expiresAt - 获取锁:
init()时原子创建锁文件(forbidOverwrite: true)- 成功 → Writer 模式,以读写方式打开数据库
- 失败 → Reader 模式,以只读方式打开数据库
- 锁续期:Writer 每
lockTtlMs / 3毫秒更新expiresAt,包含重试逻辑 - 过期锁窃取:当检测到锁已过期时,使用
delete+ 原子create模式安全窃取,避免竞态条件 - 锁释放:
close()时删除锁文件
⚠️ 注意: 此模型不支持多主写入。同一时刻只能有一个写入者。
自我修复机制
lite-oss-db 使用 chokidar 监控本地数据库文件(包括 -wal 文件)。
当本地数据库文件被意外删除时:
- 检测到
unlink事件 - 立即从内存中的数据库句柄创建快照(句柄仍然有效)
- 上传快照到 OSS 确保数据安全
- 从快照恢复本地文件,应用继续运行
💡 虽然自我修复可以恢复数据,但仍建议在修复后重启应用以确保文件描述符干净。
加密方案
| 层面 | 状态 | |:---|:---| | OSS 存储(静态) | ✅ AES-256-CTR + HMAC-SHA256 加密 | | 传输中 | ✅ HTTPS(阿里云 OSS 默认) | | 本地文件 | ❌ 未加密(便于使用标准 SQLite 工具) |
加密格式(二进制布局):
[version: 1B] [salt: 16B] [iv: 16B] [ciphertext: *B] [hmac: 32B]- 密钥派生:PBKDF2(SHA-256, 100,000 次迭代)— 从密码字符串派生 32 字节密钥
- 流式处理:加密和解密均使用 Node.js Transform Stream,内存占用恒定,不受文件大小影响
- 完整性验证:HMAC-SHA256 覆盖 salt + iv + ciphertext,防篡改
自定义适配器
StorageAdapter 接口
要支持其他对象存储(如 AWS S3、MinIO、腾讯 COS 等),实现此接口即可。
interface StorageAdapter {
/** 上传数据 */
put(path: string, buffer: Buffer, options?: PutOptions): Promise<void>;
/** 上传流 */
putStream(path: string, stream: any, options?: PutOptions): Promise<void>;
/** 下载数据,不存在返回 null */
get(path: string): Promise<Buffer | null>;
/** 获取元数据(ETag, size, updatedAt),不存在返回 null */
head(path: string): Promise<ObjectMetadata | null>;
/** 删除对象 */
delete(path: string): Promise<void>;
/** 按前缀列出对象 */
list(prefix: string): Promise<ObjectInfo[]>;
}
interface PutOptions {
/** 如果为 true,对象已存在时操作失败(原子创建) */
forbidOverwrite?: boolean;
}
interface ObjectMetadata {
size: number;
etag: string;
updatedAt: Date;
}
interface ObjectInfo {
name: string;
updatedAt: Date;
}内置的 AliyunOssAdapter 使用了一个最小化的 AliOssClient 接口,避免对 ali-oss 包的硬依赖:
import { AliyunOssAdapter } from 'lite-oss-db';
import type { AliOssClient } from 'lite-oss-db';
// 任何满足 AliOssClient 接口的对象都可以传入
const adapter = new AliyunOssAdapter(ossClient);IDatabase 接口
要支持其他 SQLite 驱动,实现此接口即可。
interface IDatabase {
exec(sql: string): unknown;
prepare(sql: string): {
run(...args: any[]): unknown;
get(...args: any[]): unknown;
all(...args: any[]): unknown[];
};
/** @deprecated 推荐使用 backup(),serialize() 会将整个数据库加载到内存 */
serialize(): Buffer;
backup(destination: string): Promise<void>;
pragma(pragma: string, options?: { simple?: boolean }): unknown;
close(): void;
readonly: boolean;
}
type DatabaseFactory = (path: string, options?: { readonly?: boolean }) => IDatabase;内置了两个数据库适配器,分别对应 Node.js 和 Bun:
// Node.js — better-sqlite3
import { BetterSqlite3Adapter } from 'lite-oss-db';
import Database from 'better-sqlite3';
const factory = BetterSqlite3Adapter.createFactory(Database);
// Bun — bun:sqlite
import { BunSqliteAdapter } from 'lite-oss-db';
import { Database } from 'bun:sqlite';
const factory = BunSqliteAdapter.createFactory(Database);完整配置示例
import { OssDb, AliyunOssAdapter, BetterSqlite3Adapter } from 'lite-oss-db';
import OSS from 'ali-oss';
import Database from 'better-sqlite3';
// 读写模式 + 加密 + 定时备份(全量配置)
const db = new OssDb({
// === 必填 ===
dbPath: './data/app.db',
adapter: new AliyunOssAdapter(new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: process.env.AK!,
accessKeySecret: process.env.SK!,
bucket: 'my-bucket',
})),
dbFactory: BetterSqlite3Adapter.createFactory(Database),
// === 模式 ===
mode: 'readwrite', // 'readwrite' | 'readonly'
remotePath: 'prod/app.db', // OSS 中的对象路径
// === 读写模式选项 ===
workerId: 'server-1', // 分布式锁标识
lockTtlMs: 15000, // 锁 TTL 15 秒
syncDebounceMs: 3000, // 防抖 3 秒
maxWaitMs: 15000, // 最大等待 15 秒
// === 定时备份 ===
scheduledBackup: {
intervalMs: 3600000, // 每小时
prefix: 'backups/prod',
maxBackups: 48, // 保留 48 个
},
// === 加密 ===
encryption: {
key: process.env.DB_SECRET!,
},
// === 日志 ===
logger: false, // 禁用日志
});开发
# 克隆仓库
git clone <repo-url> && cd lite-oss-db
# 安装依赖
pnpm install
# 运行测试(使用内存 MockAdapter,无需云凭据)
pnpm test
# 类型检查
npx tsc --noEmit
# 运行示例
npx tsx examples/demo.ts
# 构建
pnpm buildLicense
ISC
