从单线程到多进程/多线程并发,含异常处理与最佳实践

在 ARM 架构的嵌入式 Linux 系统(如 Raspberry Pi、工业网关、IoT 设备)中,SQLite3 因其零配置、无服务、单文件、低资源占用等特性,成为首选的本地数据库。然而,许多开发者仅满足于“能读写”,忽视了在多线程/多进程并发、断电异常、存储磨损等嵌入式典型场景下的稳定性问题。本文将从 数据库打开/关闭、增删改查、并发控制、异常恢复 全维度,系统性总结 SQLite3 在 ARM Linux 上的生产级用法


一、基础使用:正确打开与关闭数据库

1. 打开数据库(sqlite3_open_v2 推荐)

#include <sqlite3.h>

sqlite3 *db;

int rc = sqlite3_open_v2(

    "/data/app.db",        // 路径(建议使用 RAMFS 或 wear-leveling 文件系统)

    &db,

    SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE |

        SQLITE_OPEN_NOMUTEX,   // 多线程模式选择(见下文)

    NULL

    );

if (rc != SQLITE_OK) {

    fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));

    sqlite3_close(db); // 即使失败也需 close

    return -1;

}

✅ 关键参数说明

  • 路径选择

    :避免直接写 eMMC/SD 卡根分区,建议挂载 tmpfs 或使用 F2FS/ext4 with journal。

  • SQLITE_OPEN_NOMUTEX

    :若应用自行管理线程同步(推荐),可提升性能;否则用默认 SERIALIZED 模式。

  • 错误检查

    :必须检查返回值!磁盘满、权限不足、路径不存在均会导致失败。

2. 关闭数据库(确保资源释放)

// 关闭前执行 checkpoint(WAL 模式必需)

sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_TRUNCATE, NULL, NULL);

int rc = sqlite3_close(db);

if (rc == SQLITE_BUSY) {

    // 仍有未完成的语句或备份

    sqlite3_stmt *stmt;

    while ((stmt = sqlite3_next_stmt(db, NULL)) != NULL) {

        sqlite3_finalize(stmt); // 释放所有 prepared statements

    }

    sqlite3_close(db); // 再次尝试

}

⚠️ 嵌入式注意

  • 不要强制 kill 进程而不 close

     → 可能导致 WAL 文件残留、数据库锁死。

  • 断电前尽量调用 sqlite3_close

    ,或启用 PRAGMA synchronous=FULL + UPS。


二、增删改查(CRUD):安全高效的执行方式

1. 使用 Prepared Statements(防注入 + 性能优化)

const char *sql = "INSERT INTO sensors (id, value, ts) VALUES (?, ?, ?)";

sqlite3_stmt *stmt;

rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);

if (rc != SQLITE_OK) goto error;

sqlite3_bind_int(stmt, 1, sensor_id);

sqlite3_bind_double(stmt, 2, value);

sqlite3_bind_int64(stmt, 3, time(NULL));

rc = sqlite3_step(stmt);

if (rc != SQLITE_DONE) goto error;

sqlite3_finalize(stmt);

return 0;

error:

        fprintf(stderr, "SQL error: %s\n", sqlite3_errmsg(db));

sqlite3_finalize(stmt);

return -1;

✅ 优势

  • 自动转义参数,杜绝 SQL 注入;

  • 编译一次,执行多次(适合循环插入);

  • 错误信息精确到具体语句。

2. 批量操作:使用事务(Transaction)

sqlite3_exec(db, "BEGIN IMMEDIATE;", NULL, NULL, NULL);

for (int i = 0; i < N; i++) {

    // 执行多条 INSERT/UPDATE

}

sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);

💡 性能对比

  • 无事务:1000 条 INSERT ≈ 30 秒(SD 卡)

  • 有事务:1000 条 INSERT ≈ 0.2 秒
    嵌入式设备务必使用事务!


三、多线程访问:三种模式详解

SQLite3 提供三种线程模式(编译时决定,默认为 SERIALIZED):

模式

编译选项

特点

适用场景

Single-thread SQLITE_THREADSAFE=0

完全不支持多线程

单线程应用

Multi-thread SQLITE_THREADSAFE=2

同一 db 对象不能共享,不同 db 可并发

多线程各持 db 句柄

Serialized SQLITE_THREADSAFE=1

(默认)

内部加 mutex,安全但有性能损耗

通用场景

推荐方案(ARM 嵌入式):

  • 方案 A(推荐):使用 SQLITE_OPEN_NOMUTEX + 应用层 mutex

static pthread_mutex_t db_mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&db_mutex);

// 执行 SQL

pthread_mutex_unlock(&db_mutex);

  • ✅ 优势:避免 SQLite 内部锁竞争,性能更高;可精细控制临界区。

  • 方案 B:每个线程独立打开数据库(适用于只读场景)

    ⚠️ 注意:写操作仍会通过文件锁互斥,可能引发 SQLITE_BUSY


四、多进程访问:文件锁与 WAL 模式

1. 默认回滚日志模式(Rollback Journal)

  • 多进程写时,第一个写者获得 RESERVED 锁,其他写者阻塞;

  • 若写者崩溃,下次打开自动回滚。

2. WAL 模式(Write-Ahead Logging)——强烈推荐用于多进程

sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);

✅ WAL 优势

  • 读写不互斥:读者可读旧版本,写者写新版本;

  • 多进程并发写性能显著提升;

  • 崩溃恢复更快(只需截断 WAL 文件)。

⚠️ WAL 注意事项

  • 生成 -wal 和 -shm 两个额外文件,必须与 .db 文件同目录

  • 断电可能导致 WAL 未 checkpoint,需在启动时手动触发:

sqlite3_wal_checkpoint_v2(db, NULL, SQLITE_CHECKPOINT_RESTART, NULL, NULL);

3. 处理 SQLITE_BUSY(多进程/线程冲突)

// 设置 busy handler(自动重试)

sqlite3_busy_handler(db, busy_callback, (void*)max_retries);

static int busy_callback(void *data, int count) {

    if (count > *(int*)data) return 0; // 放弃

    usleep(10000); // 休眠 10ms

    return 1;      // 重试

}

🔁 替代方案:使用 sqlite3_busy_timeout(db, 5000) 设置超时(5秒)。


五、异常处理与健壮性保障

1. 常见错误码处理

错误码

含义

应对措施

SQLITE_BUSY

数据库被锁定

重试或通知用户

SQLITE_FULL

磁盘空间不足

清理旧数据、告警

SQLITE_CORRUPT

数据库损坏

从备份恢复

SQLITE_IOERR

I/O 错误(SD 卡故障)

切换存储介质

2. 数据库损坏防护

  • 启用校验和

PRAGMA page_size = 4096;

PRAGMA journal_mode = WAL;

PRAGMA synchronous = FULL; -- 确保写入物理介质

定期备份

// 使用 SQLite3 Backup API 在线备份

sqlite3_backup_init(dest_db, "main", src_db, "main");

  • 监控文件系统健康

    :通过 smartctl(eMMC)或坏块检测。

3. 断电安全策略

  • WAL + TRUNCATE checkpoint

    :减少恢复时间;

  • UPS 或超级电容

    :确保最后事务完成;

  • 应用层双缓冲

    :关键数据先写临时文件,再原子 rename。


六、ARM 嵌入式专项优化

优化项

说明

禁用不必要的功能

编译时 --disable-fts5 --disable-rtree 减小体积

使用内存数据库 :memory:

 用于临时计算(但进程退出即丢失)

调整 page_size

匹配 NAND 页大小(通常 4KB)

避免频繁 VACUUM VACUUM

 会重写整个 DB,加剧 SD 卡磨损


结语:稳定比功能更重要

在 ARM Linux 嵌入式环境中,SQLite3 的使用绝非“打开→执行→关闭”那么简单。真正的生产级应用必须考虑

  • 并发模型的选择与同步;

  • 异常场景的优雅降级;

  • 存储介质的物理特性;

  • 断电/崩溃的恢复能力。

遵循本文的最佳实践,你将构建出一个即使在恶劣环境下也能可靠运行的嵌入式数据库应用。记住:“在嵌入式世界,数据库的稳定性,就是产品的生命线。”

北斗定位,你真的了解吗?——从紫微垣到全球组网的千年征途

C++17--继 C++11 之后又一次实质性增强的 C++ 标准

C++17 std::invoke std::variant

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐