2. 原生SQLite C API集成与初始化
在移动端与嵌入式领域,直接调用SQLite原生C API是实现本地数据持久化的最底层、最高效的方式。本章聚焦于通过C语言接口完成数据库的打开、错误处理、连接池构建及线程安全配置,掌握这些要点是后续所有SQLite集成方案(如FMDB、WCDB、Room等)的基石。
2.1 初始化核心流程
SQLite初始化包含三个阶段:全局配置(线程安全模式)、数据库打开(sqlite3_open或sqlite3_open_v2)、连接池管理。流程如下图所示:
2.2 sqlite3_open 与 sqlite3_open_v2
SQLite提供两个主要函数打开数据库:
| 函数 | 原型 | 特点 |
|---|---|---|
sqlite3_open | int sqlite3_open(const char *filename, sqlite3 **ppDb) | 使用默认打开标志(SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE),适合简单场景 |
sqlite3_open_v2 | int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs) | 可自定义标志(如只读、完整互斥、共享缓存等),推荐生产环境使用 |
最佳实践:始终使用
sqlite3_open_v2 并显式指定 SQLITE_OPEN_FULLMUTEX(线程安全)或 SQLITE_OPEN_NOMUTEX(单线程),避免隐式行为。典型初始化代码示例:
#include <sqlite3.h>
sqlite3 *db = NULL;
int rc = sqlite3_open_v2("app.db", &db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
NULL);
if (rc != SQLITE_OK) {
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return rc;
}
// 使用数据库...
sqlite3_close(db);
2.3 错误码处理机制
SQLite几乎所有API均返回整型错误码,常用错误码及含义:
| 错误码 | 宏定义 | 典型原因 |
|---|---|---|
| 0 | SQLITE_OK | 操作成功 |
| 1 | SQLITE_ERROR | SQL错误或缺失数据库 |
| 14 | SQLITE_CANTOPEN | 无法打开文件(路径错误、权限不足) |
| 11 | SQLITE_CORRUPT | 数据库文件损坏 |
| 5 | SQLITE_BUSY | 数据库被锁定(多线程竞争) |
| 6 | SQLITE_LOCKED | 表被锁定(连接内竞争) |
| 8 | SQLITE_READONLY | 尝试写入只读数据库 |
常见陷阱:很多开发者只检查
rc == SQLITE_OK,但忽略了 SQLITE_BUSY 等可重试错误。在连接池场景中,建议使用 sqlite3_busy_timeout 设置超时,而非依赖外部重试循环。2.4 连接池管理策略
SQLite本身不支持连接池——每个 sqlite3_open 创建一个独立连接。但在多线程高并发场景下,共享一个连接会引发 SQLITE_BUSY。实现连接池的要点:
- 连接数量:建议 =
std::thread::hardware_concurrency() + 1,避免过多文件描述符消耗。 - 封装资源管理:使用RAII(如C++的
std::unique_ptr)自动关闭连接。 - 队列与借用:预创建N个连接放入队列,业务线程调用时借用,归还时重置(
sqlite3_reset)。 - WAL模式配合:启用
PRAGMA journal_mode=WAL后,读操作不阻塞写操作,降低连接池压力。
连接池核心实现示意:
class SQLitePool {
std::vector<sqlite3*> connections_;
std::mutex mtx_;
std::condition_variable cv_;
public:
explicit SQLitePool(size_t size) {
for (size_t i = 0; i < size; ++i) {
sqlite3 *db;
sqlite3_open_v2("app.db", &db, flags, NULL);
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
connections_.push_back(db);
}
}
sqlite3* Acquire() {
std::unique_lock lock(mtx_);
cv_.wait(lock, [this]{ return !connections_.empty(); });
auto db = connections_.back();
connections_.pop_back();
return db;
}
void Release(sqlite3 *db) {
std::lock_guard lock(mtx_);
connections_.push_back(db);
cv_.notify_one();
}
~SQLitePool() {
for (auto db : connections_) sqlite3_close(db);
}
};
2.5 线程安全配置要点
SQLite线程安全分为三个级别,由编译时宏 SQLITE_THREADSAFE 和运行时标志共同控制:
| 级别 | 编译值 | 运行时打开标志(open_v2) | 行为 |
|---|---|---|---|
| 完整互斥(推荐) | 1(默认) | SQLITE_OPEN_FULLMUTEX | 所有连接自动使用内部互斥锁,线程安全 |
| 单线程 | 1 | SQLITE_OPEN_NOMUTEX | 不启用互斥,速度最快但必须由外部保证串行化 |
| 禁用线程安全 | 0 | 忽略 | 编译时关闭,任何跨线程访问都是未定义行为 |
线程安全配置建议:
- 移动端(iOS/Android)默认使用完整互斥模式,无需额外操作。
- 如果自行编译SQLite,务必设置
- 在连接池中,每个连接应使用
- 移动端(iOS/Android)默认使用完整互斥模式,无需额外操作。
- 如果自行编译SQLite,务必设置
SQLITE_THREADSAFE=1。- 在连接池中,每个连接应使用
SQLITE_OPEN_FULLMUTEX,池本身再用锁保护队列,构成双层保护。全局配置示例(仅在第一次调用sqlite3_open前执行):
// 设置配置
sqlite3_config(SQLITE_CONFIG_MULTITHREAD); // 多线程模式(但每个连接仍需标志)
// 或
sqlite3_config(SQLITE_CONFIG_SERIALIZED); // 全序列化模式(自动互斥)
// 设置忙超时(毫秒)
sqlite3_busy_timeout(db, 5000);
2.6 调试与常见错误
- 打开失败
SQLITE_CANTOPEN:检查文件路径权限(Android上应用目录getFilesDir(),iOS上NSDocumentDirectory)。 - 内存泄漏:每次
sqlite3_open必须对应一次sqlite3_close;使用RAII或智能指针管理。 - SQLITE_BUSY 死循环:未设置
busy_timeout且多线程同时写同一数据库。 - 交叉编译链接错误:确保链接
libsqlite3,并指定-lpthread(POSIX系统)。
2.7 性能与稳定性优化
- WAL模式:提高并发读写性能,减少
SQLITE_BUSY。执行PRAGMA journal_mode=WAL;。 - 缓存大小:
PRAGMA cache_size=-8000(8MB)降低磁盘IO。 - 同步模式:
PRAGMA synchronous=NORMAL;平衡安全性与速度。 - 连接池预热:应用启动时提前创建连接并执行一次
SELECT 1触发缓存。