Showing Posts From
SQLite
漏洞管理平台架构:main.db + 每应用独立 SQLite 的多库设计
数据库拆分设计 /database main.db ← 用户、管理员、应用列表 java.db ← Java 漏洞、CVE、POC、EXP mysql.db ← MySQL 漏洞、CVE、POC、EXP redis.db nginx.db每个应用独立 SQLite 的优点:某应用数据损坏不影响整个系统 可单独备份或迁移到 PostgreSQL 漏洞数据量大时不会拖慢其他应用的查询main.db 表结构 -- 用户表 CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password TEXT, -- bcrypt hash role TEXT, -- superadmin / admin / user status INTEGER, -- 0=禁用 1=正常 created_at DATETIME );-- 应用注册表 CREATE TABLE applications ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, -- "java" db_name TEXT, -- "java.db" db_path TEXT, -- "/database/java.db" created_at DATETIME );应用数据库(每个应用独立) CREATE TABLE vulnerabilities ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, description TEXT, severity TEXT, -- critical/high/medium/low affected_version TEXT, fixed_version TEXT, created_at DATETIME );CREATE TABLE cves ( id INTEGER PRIMARY KEY AUTOINCREMENT, cve_id TEXT, -- CVE-2024-12345 cvss REAL, description TEXT );CREATE TABLE pocs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, file_path TEXT, -- /uploads/java/uuid.py description TEXT );CREATE TABLE exps ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, file_path TEXT, description TEXT );-- 多对多关联 CREATE TABLE vulnerability_pocs (vuln_id INT, poc_id INT); CREATE TABLE vulnerability_exps (vuln_id INT, exp_id INT); CREATE TABLE vulnerability_cves (vuln_id INT, cve_id INT);动态创建应用数据库 添加应用时,后端自动创建 SQLite 文件并初始化表: // NestJS service async createApplication(name: string) { const dbPath = path.join("database", `${name}.db`); const db = new Database(dbPath); // 初始化表结构 db.exec(` CREATE TABLE IF NOT EXISTS vulnerabilities (...); CREATE TABLE IF NOT EXISTS cves (...); CREATE TABLE IF NOT EXISTS pocs (...); CREATE TABLE IF NOT EXISTS exps (...); `); db.close(); // 记录到 main.db await this.mainDb.run( "INSERT INTO applications (name, db_name, db_path) VALUES (?, ?, ?)", [name, `${name}.db`, dbPath] ); }RBAC 三角色权限角色 权限superadmin 全部权限,管理用户和管理员admin 管理漏洞/CVE/POC/EXP,不能添加管理员user 只能查询漏洞和下载 POC/EXPJWT 鉴权: // NestJS Guard @Injectable() export class RolesGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const { user } = context.switchToHttp().getRequest(); const requiredRoles = this.reflector.get<string[]>("roles", context.getHandler()); return requiredRoles.includes(user.role); } }文件安全 POC/EXP 文件禁止通过静态目录暴露: ❌ GET /uploads/java/poc.py (直接静态访问) ✅ GET /api/download/poc/:id (JWT 鉴权后返回文件流)@Get("download/poc/:id") @UseGuards(JwtAuthGuard) async downloadPoc(@Param("id") id: string, @Res() res: Response) { const poc = await this.pocService.findById(id); res.download(poc.file_path, poc.name); }上传文件名用 UUID 避免路径遍历: const filename = `${uuidv4()}${path.extname(file.originalname)}`;版本匹配建议 不要简单字符串比对,用 semver 范围查询: import semver from "semver";function isVulnerable(version: string, affected: string): boolean { // affected: ">=8.0.0 <8.0.31" return semver.satisfies(version, affected); }"8.0.9" < "8.0.31" 用字符串比较会得到错误结果(字典序 9 > 3),必须用 semver 数字化比较。 推荐技术栈层 推荐后端框架 NestJS 或 Next.js App RouterORM Prisma(SQLite 支持好,类型安全)鉴权 JWT + bcrypt前端 Vue3 + Element Plus 或 shadcn/ui部署 PM2 + Nginx 反代
SQLite 大数据量优化:WAL 模式、批量事务、游标分页和索引策略
SQLite 单数据库文件理论上限约 281TB,实际几十 GB 均可正常运行,性能瓶颈通常来自配置和使用方式,而非 SQLite 本身。 必开:WAL 模式 PRAGMA journal_mode=WAL;默认的 DELETE journal 模式写入时会锁全库,WAL 模式允许读写并发,大幅提升写入性能。需要在每次连接时设置(或写入配置持久化)。 减少同步次数 -- 兼顾安全和性能(推荐) PRAGMA synchronous=NORMAL;-- 最高性能(断电可能丢最近几秒数据,非关键数据可用) PRAGMA synchronous=OFF;批量事务(最重要的优化) 单条 INSERT 性能极差,每次写入都会触发一次磁盘同步: // 错误写法:每条 INSERT 是独立事务 for (const item of items) { db.run("INSERT INTO logs VALUES (?)", item); }改为批量事务,性能提升 100-1000 倍: BEGIN; INSERT INTO logs VALUES (1, 'event_a', 1716800000); INSERT INTO logs VALUES (2, 'event_b', 1716800001); -- ... 批量写入 COMMIT;Node.js(better-sqlite3)示例: const insertMany = db.transaction((items) => { const stmt = db.prepare("INSERT INTO logs (id, type, ts) VALUES (?, ?, ?)"); for (const item of items) { stmt.run(item.id, item.type, item.ts); } });insertMany(items); // 一次事务写入全部Python(sqlite3)示例: conn.execute("BEGIN") conn.executemany("INSERT INTO logs VALUES (?, ?, ?)", items) conn.execute("COMMIT")建议每批 500-5000 条,批次过大会增加内存压力。 游标分页(替代 OFFSET) OFFSET 越大越慢,因为需要扫描并跳过前面所有行: -- 慢:OFFSET 100万,需要扫描 100万行 SELECT * FROM logs LIMIT 20 OFFSET 1000000;改用游标分页(Keyset Pagination): -- 快:直接从上次最后一条的 id 开始 SELECT * FROM logs WHERE id > ? LIMIT 20;记录上次最后一条的 id 作为下次查询的起始点,无论翻到多后面性能都一致。 索引策略 查看查询执行计划,确认是否走索引: EXPLAIN QUERY PLAN SELECT * FROM logs WHERE user_id = 123 AND created_at > 1716800000;原则:高频 WHERE 条件字段建索引 复合查询建复合索引(字段顺序与查询条件一致) 低选择度字段(如布尔值)单独建索引意义不大 写入密集型表控制索引数量,每个索引都会拖慢写入其他配置 -- 增大缓存(默认 -2000 即 2MB,建议改大) PRAGMA cache_size=-32000; -- 32MB-- 内存临时表(减少磁盘 I/O) PRAGMA temp_store=MEMORY;-- 预写 WAL 检查点 PRAGMA wal_autocheckpoint=1000;大字段(图片、视频、大 JSON、二进制 Blob)建议存文件系统,数据库只存文件路径,避免单数据库文件膨胀过快。
SQLite 存几十 GB 数据的六个优化点
有个错觉——SQLite 只适合小项目。其实很多人低估了它。理论上 SQLite 单库能到 281 TB,实际几十 GB、一亿行都能稳定跑。前提是这几个优化必须做。 先给个使用边界数据量 SQLite 适不适合< 10 GB 完全没问题,甚至比 Postgres 简单10 GB ~ 100 GB 可以,需要认真优化> 100 GB 谨慎,考虑分片 / 换数据库高并发写入 不推荐,SQLite 写是全库锁(WAL 缓解但仍单写)多机共享文件 别玩,NFS 上的 SQLite 特别容易坏适合 SQLite 的典型场景:本地缓存、日志、爬虫存档、IoT 边缘节点、桌面/Electron 应用、单机分析、中小型业务。 1. WAL 模式(必开) PRAGMA journal_mode = WAL;作用:读和写可以并发(默认 journal 会锁全库) 崩溃恢复快 大量写入性能大幅提升一次性开完之后写进库设置,不用每次连接都设。 2. 调 synchronous PRAGMA synchronous = NORMAL; -- 折中,推荐 -- PRAGMA synchronous = OFF; -- 极限性能,接受掉电丢数据FULL(默认)= 每次事务 fsync 磁盘、NORMAL = 检查点时 fsync、OFF = 完全信操作系统。日志/缓存场景 NORMAL 完全够用,性能差好几倍。 3. 批量事务(提升最猛的一条) 单条 insert: for (const row of rows) { db.prepare("INSERT INTO logs VALUES (?, ?)").run(row.a, row.b); }一亿行你等到明年。改成批量: const insert = db.prepare("INSERT INTO logs VALUES (?, ?)"); const many = db.transaction((rows) => { for (const row of rows) insert.run(row.a, row.b); }); many(rows);性能能差 100 ~ 1000 倍。SQLite 每个事务外面套一次 fsync,逐条 insert 就是每行都 fsync。 4. 索引不要滥建 EXPLAIN QUERY PLAN SELECT * FROM logs WHERE user_id = ? AND ts > ?;索引越多,写入越慢(每行 insert 都要维护索引) 只给高频过滤字段建索引 复合索引按 WHERE 里最常见的过滤顺序建:(user_id, ts) 大文本 / JSON 字段别建普通索引,需要就用 FTS55. 分页别用 OFFSET -- 慢,越翻越慢:OFFSET 需要跳过前 N 行 SELECT * FROM logs ORDER BY id LIMIT 20 OFFSET 1000000;-- 快,游标分页:直接从上一页最后一个 id 之后取 SELECT * FROM logs WHERE id > ? ORDER BY id LIMIT 20;学名叫 Keyset Pagination,任何大表分页都该用。 6. 大字段不要塞 blob 图片、视频、大 JSON 直接扔 SQLite 会拖慢一切——包括那些跟大字段无关的查询。 正确做法:文件系统 / 对象存储放实体 SQLite 只存路径 / 元数据或者用 SQLite 官方推荐 的经验值:< 100 KB 存库里更快,> 100 KB 存文件系统更快。 7. 加上几个 PRAGMA PRAGMA cache_size = -100000; -- 100 MB 缓存(负数表示 KB) PRAGMA temp_store = MEMORY; -- 临时表放内存 PRAGMA mmap_size = 268435456; -- 256 MB mmap,减少 syscall PRAGMA busy_timeout = 5000; -- 遇到锁等 5 秒再报错一句话总结 WAL + 批量事务 + Keyset 分页——这三条打下来,几十 GB 的 SQLite 一点都不难。别忘了大字段挪出去、别乱建索引。
Node 内置 SQLite 报 statement has been finalized 的原因
Node 22 开始有了内置的 node:sqlite,同步 API 用起来非常顺。但启动时挂: ExperimentalWarning: SQLite is an experimental feature Failed to start server Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) ... code: 'ERR_INVALID_STATE'这个错的字面意思很明确:你在一个已经 finalize() 过的 prepared statement 上继续调用 all() / get() / iterate()。 三种最常见的成因 1. 手动过早 finalize const stmt = db.prepare("SELECT * FROM devices"); try { return stmt.all(); } finally { stmt.finalize(); }看上去正确。但如果 stmt.all() 返回的是迭代器(iterate())而不是数组(all()),外层还在懒消费,finalize 一走就崩。用 all() 拿到具体数组的写法没这个问题。 2. 用了 using 自动 finalize(Node 22 的新语法) export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.iterate(); // ← 迭代器,出了作用域 stmt 就被 finalize 了 }using 借用了 TC39 的 Explicit Resource Management 提案,出作用域时会自动调 [Symbol.dispose](),也就是 finalize()。返回迭代器给外部继续用,外部一 next() 就炸。 规则:using + iterate() 不能混,要么用 all() 一次拿全部再返回,要么调用方也 using。 3. 模块级缓存了 stmt // 模块顶部 const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); }看着挺合理——避免每次 prepare 的开销。但代码其它地方可能:单元测试用 db.close() 关掉了数据库(stmt 一起 finalize) 有热重载/HMR 重新 import 了模块(旧 stmt 引用还留着) 显式调用了 readStmt.finalize() 想清理,然后忘了之后再走这个函数就 ERR_INVALID_STATE。 推荐写法 每次现 prepare,让 GC 管理: export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 立刻取完,不返回 iterator }现代 SQLite 的 prepare 开销很小,不需要为性能提前优化。 要缓存的话,配套复用而不是复用 + finalize: const cache = new Map<string, ReturnType<typeof db.prepare>>();function s(sql: string) { let stmt = cache.get(sql); if (!stmt) { stmt = db.prepare(sql); cache.set(sql, stmt); } return stmt; }// 用: s("SELECT * FROM device_states").all();不在缓存生命周期外手动 finalize。数据库 close 时统一 cache.clear()。 要用 using 就一次性拿完数据: export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 数组,可以安全跨作用域 }一句话总结 statement has been finalized = 你正在用一个死掉的 stmt。别返回迭代器给作用域外、别在模块级缓存又手动 finalize。改成"每次 prepare + all()"最省心。
Node.js SQLite statement has been finalized 错误:原因与修复
Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) { code: 'ERR_INVALID_STATE' }这个错误来自 Node.js 22+ 的内置 node:sqlite 模块,对已经调用了 finalize() 的 StatementSync 对象再次操作时抛出。 原因一:全局缓存 Statement 被意外 finalize // 错误写法:模块级共享 statement const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); // 若 readStmt 被 finalize 则崩溃 }如果在某处调用了 readStmt.finalize(),后续所有使用都会报错。 原因二:using 关键字自动释放 Node.js SQLite 支持 using 声明(Explicit Resource Management),离开作用域后自动调用 finalize(): // 错误:返回迭代器后 stmt 已被 finalize function getRows() { using stmt = db.prepare("SELECT * FROM logs"); return stmt.iterate(); // 离开函数后 stmt 自动 finalize,迭代器失效 }修复:每次使用时重新 prepare // 正确写法:每次都 prepare,不缓存 export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); const result = stmt.all(); stmt.finalize(); return result; }或者更简洁,不手动调用 finalize(Node.js 会在 GC 时自动释放): export function readDeviceStates() { return db.prepare("SELECT * FROM device_states").all(); }需要性能优化时:用对象包装 如果 prepare 开销较大,可以在模块初始化时准备,但确保 finalize 只在明确不再使用时调用: class DeviceRepository { private readStmt: StatementSync; constructor(private db: DatabaseSync) { this.readStmt = db.prepare("SELECT * FROM device_states"); } getAll() { return this.readStmt.all(); } close() { this.readStmt.finalize(); this.db.close(); } }生命周期由对象管理,close() 明确释放资源。 ExperimentalWarning 的处理 (node:2436) ExperimentalWarning: SQLite is an experimental featurenode:sqlite 在 Node.js 22 中是实验性功能,可以用以下方式消除警告: node --no-experimental-warnings server.js或在代码中: process.removeAllListeners('warning');生产环境建议关注 Node.js 版本更新,等待 SQLite 模块稳定。
