SQLite WAL 模式在 Android 上的踩坑记录

SQLite 是移动端常用的嵌入式数据库。3.7.0 版本引入的 WAL(Write-Ahead Logging)模式能提升写入性能,但在 Android 平台配合 Cocos2d-x 使用时踩了不少坑。这篇记录 WAL 的原理、优缺点,以及 Android 上的问题和解决方案。

WAL 模式原理

什么是 WAL

WAL(Write-Ahead Logging,预写日志)是一种数据库事务实现机制,其核心思想是:修改不直接写入数据库文件,而是先写入独立的日志文件

工作流程

1
2
3
4
5
6
7
8
9
10
┌─────────┐     ┌─────────┐     ┌─────────┐
│ 写入请求 │ --> │ WAL文件 │ --> │ DB文件 │
│ │ │-wal │ │ .db │
└─────────┘ └─────────┘ └─────────┘


┌─────────┐
│ SHM文件 │
│ -shm │
└─────────┘

事务处理流程:

  1. 写入阶段:所有修改先写入 WAL 文件
  2. 提交阶段:在 WAL 文件中标记事务为已提交
  3. 回滚阶段:事务失败时,WAL 中的记录被忽略
  4. 检查点阶段:定期将 WAL 中的修改写回主数据库文件

三个相关文件

启用 WAL 模式后,每个数据库对应三个文件:

文件 扩展名 作用
主数据库文件 .db 存储实际数据
WAL 日志文件 .db-wal 存储未提交的修改
共享内存文件 .db-shm 存储 WAL 索引信息

WAL 模式的优缺点

优点

1. 读写并发能力

1
2
3
4
5
6
7
8
9
┌─────────┐          ┌─────────┐
│ 读事务 │ <------> │ 数据库 │
│ Reader │ │ .db │
└─────────┘ └────┬────┘

┌─────────┐ ┌────┴────┐
│ 写事务 │ ------> │ WAL文件 │
│ Writer │ │ -wal │
└─────────┘ └─────────┘
  • 读操作和写操作可以并发执行
  • 读操作不会被写操作阻塞
  • 写操作之间仍然需要串行化(SQLite 的锁机制)

2. 性能提升

  • 减少磁盘 I/O 次数
  • 不需要每次写入时同时更新数据库文件和日志
  • 批量写入效率更高

3. 崩溃恢复

  • WAL 文件保留未提交的修改
  • 数据库崩溃后可以快速恢复
  • 自动回滚未完成的事务

4. 磁盘 I/O 可预测

  • 顺序写入 WAL 文件
  • 减少随机 I/O
  • 延长闪存寿命(对移动设备重要)

缺点

1. 共享内存限制

  • 访问数据库的所有程序必须在同一主机
  • 依赖共享内存技术
  • 不支持网络文件系统(NFS)

2. 文件数量增加

1
2
3
database.db
database.db-wal # WAL 日志文件
database.db-shm # 共享内存索引
  • 文件管理复杂度增加
  • 备份时需要同时处理三个文件

3. 大数据量性能下降

  • 当 WAL 文件达到 GB 级别时,性能显著下降
  • 检查点操作变得耗时
  • 需要及时执行检查点

4. 版本兼容性

  • SQLite 3.7.0 之前的版本无法识别 WAL 模式数据库
  • 旧版本工具可能报错

激活 WAL 模式

在 Cocos2d-x 中启用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "sqlite3.h"

bool enableWAL(sqlite3* dataBaseConnect) {
char* err_msg = NULL;

// 执行 PRAGMA 命令启用 WAL
const char* sql = "PRAGMA journal_mode=WAL;";

int result = sqlite3_exec(
dataBaseConnect,
sql,
NULL, // 回调函数
NULL, // 回调参数
&err_msg // 错误信息
);

if (result != SQLITE_OK) {
CCLOG("DataBase PRAGMA journal_mode=WAL failed with %s", err_msg);
sqlite3_free(err_msg);
return false;
}

CCLOG("WAL mode enabled successfully");
return true;
}

验证 WAL 状态

1
2
3
4
5
6
7
8
9
10
11
// 检查当前日志模式
const char* checkSQL = "PRAGMA journal_mode;";
sqlite3_exec(dataBaseConnect, checkSQL, callback, NULL, NULL);

// 回调函数处理结果
static int callback(void* data, int argc, char** argv, char** azColName) {
for (int i = 0; i < argc; i++) {
CCLOG("Journal mode: %s", argv[i] ? argv[i] : "NULL");
}
return 0;
}

WAL 相关配置

1
2
3
4
5
6
7
// 设置检查点阈值(默认 1000 页)
const char* checkpointSQL = "PRAGMA wal_autocheckpoint=1000;";
sqlite3_exec(dataBaseConnect, checkpointSQL, NULL, NULL, NULL);

// 手动执行检查点
const char* manualCheckpoint = "PRAGMA wal_checkpoint(TRUNCATE);";
sqlite3_exec(dataBaseConnect, manualCheckpoint, NULL, NULL, NULL);

Android 平台问题分析

问题现象

在 Android 平台使用 WAL 模式时,可能会遇到以下问题:

1
2
3
4
5
错误:WAL 模式启动失败
症状:
- 首次启动应用成功
- 再次启动时报错
- 数据库无法正常打开

问题原因分析

1. 文件句柄泄漏

1
2
3
4
5
6
7
// 错误示例:未正确关闭数据库
void badExample() {
sqlite3* db;
sqlite3_open("data.db", &db);
// 使用数据库...
// 忘记关闭!
}

2. 多进程访问冲突

1
2
3
4
5
6
7
8
进程 A                    进程 B
│ │
│ 持有 WAL 锁 │
│ <──────────────────── │ 尝试获取锁
│ │
│ 崩溃/未释放 │
│ 锁残留 │
│ │ 无法获取锁,失败

3. 文件权限问题

  • Android 不同版本存储权限变化
  • /data/data/<package>/ 与外部存储差异
  • SELinux 策略限制

4. Cocos2d-x 生命周期问题

  • 应用切换到后台时数据库连接未正确处理
  • Activity 重建时旧连接未关闭
  • 多 Activity 共享数据库连接的问题

解决方案

方案一:禁用 WAL 模式

如果 WAL 模式非必需,可以直接使用传统日志模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bool openDatabase(const char* dbPath, bool useWAL = true) {
sqlite3* db;

// 打开数据库
int rc = sqlite3_open(dbPath, &db);
if (rc != SQLITE_OK) {
CCLOG("Cannot open database: %s", sqlite3_errmsg(db));
return false;
}

// 根据参数决定是否启用 WAL
if (useWAL) {
if (!enableWAL(db)) {
CCLOG("WAL mode failed, falling back to DELETE mode");
// 回退到 DELETE 模式
sqlite3_exec(db, "PRAGMA journal_mode=DELETE;", NULL, NULL, NULL);
}
} else {
// 显式使用 DELETE 模式
sqlite3_exec(db, "PRAGMA journal_mode=DELETE;", NULL, NULL, NULL);
}

return true;
}

方案二:正确关闭数据库连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DatabaseManager {
private:
sqlite3* _db = nullptr;

public:
~DatabaseManager() {
close();
}

bool open(const char* path) {
int rc = sqlite3_open(path, &_db);
if (rc != SQLITE_OK) return false;

// 启用 WAL
return enableWAL(_db);
}

void close() {
if (_db) {
// 先执行检查点,确保 WAL 写入主库
sqlite3_exec(_db, "PRAGMA wal_checkpoint(TRUNCATE);", NULL, NULL, NULL);

// 关闭数据库
sqlite3_close(_db);
_db = nullptr;
}
}
};

方案三:处理异常恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
bool openWithRecovery(const char* dbPath) {
sqlite3* db;

// 尝试正常打开
int rc = sqlite3_open(dbPath, &db);

if (rc == SQLITE_OK) {
// 尝试启用 WAL
rc = sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);

if (rc != SQLITE_OK) {
// WAL 模式失败,尝试恢复
sqlite3_close(db);

// 删除 WAL 文件(如果存在)
std::string walPath = std::string(dbPath) + "-wal";
std::string shmPath = std::string(dbPath) + "-shm";

remove(walPath.c_str());
remove(shmPath.c_str());

// 重新打开
rc = sqlite3_open(dbPath, &db);
if (rc != SQLITE_OK) {
return false;
}

// 尝试修复
sqlite3_exec(db, "PRAGMA integrity_check;", NULL, NULL, NULL);
}
}

return rc == SQLITE_OK;
}

方案四:使用单例模式管理连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class SQLiteManager {
public:
static SQLiteManager* getInstance() {
static SQLiteManager instance;
return &instance;
}

bool init(const char* dbPath) {
std::lock_guard<std::mutex> lock(_mutex);

if (_initialized) return true;

int rc = sqlite3_open(dbPath, &_db);
if (rc != SQLITE_OK) {
CCLOG("Failed to open database: %s", sqlite3_errmsg(_db));
return false;
}

// 尝试启用 WAL,失败则使用 DELETE 模式
rc = sqlite3_exec(_db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
if (rc != SQLITE_OK) {
CCLOG("WAL mode not available, using DELETE mode");
sqlite3_exec(_db, "PRAGMA journal_mode=DELETE;", NULL, NULL, NULL);
}

_initialized = true;
return true;
}

void cleanup() {
std::lock_guard<std::mutex> lock(_mutex);

if (_db) {
sqlite3_exec(_db, "PRAGMA wal_checkpoint(TRUNCATE);", NULL, NULL, NULL);
sqlite3_close(_db);
_db = nullptr;
_initialized = false;
}
}

sqlite3* getDB() { return _db; }

private:
SQLiteManager() = default;
~SQLiteManager() { cleanup(); }

sqlite3* _db = nullptr;
bool _initialized = false;
std::mutex _mutex;
};

// 在 AppDelegate 中使用
void AppDelegate::applicationDidEnterBackground() {
SQLiteManager::getInstance()->cleanup();
}

void AppDelegate::applicationWillEnterForeground() {
SQLiteManager::getInstance()->init("data.db");
}

最佳实践

1. 数据库版本管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool migrateDatabase(sqlite3* db) {
// 检查当前版本
int version = getUserVersion(db);

// 执行升级脚本
if (version < 2) {
sqlite3_exec(db,
"ALTER TABLE users ADD COLUMN email TEXT;",
NULL, NULL, NULL);
setUserVersion(db, 2);
}

if (version < 3) {
// 更多升级...
setUserVersion(db, 3);
}

return true;
}

int getUserVersion(sqlite3* db) {
sqlite3_stmt* stmt;
int version = 0;

if (sqlite3_prepare_v2(db, "PRAGMA user_version;", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
version = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
}

return version;
}

2. 事务处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool executeTransaction(sqlite3* db, std::function<bool()> operations) {
char* errMsg = NULL;

// 开始事务
if (sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &errMsg) != SQLITE_OK) {
CCLOG("Begin transaction failed: %s", errMsg);
sqlite3_free(errMsg);
return false;
}

// 执行操作
bool success = operations();

if (success) {
// 提交
if (sqlite3_exec(db, "COMMIT;", NULL, NULL, &errMsg) != SQLITE_OK) {
CCLOG("Commit failed: %s", errMsg);
sqlite3_free(errMsg);
return false;
}
} else {
// 回滚
sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
}

return success;
}

3. 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DBErrorHandler {
public:
static void handleError(int errorCode, const char* context) {
switch (errorCode) {
case SQLITE_BUSY:
CCLOG("[%s] Database is busy", context);
// 可以添加重试逻辑
break;

case SQLITE_LOCKED:
CCLOG("[%s] Database is locked", context);
break;

case SQLITE_CORRUPT:
CCLOG("[%s] Database is corrupt!", context);
// 可能需要恢复或重建
break;

case SQLITE_READONLY:
CCLOG("[%s] Database is read-only", context);
break;

default:
CCLOG("[%s] SQLite error: %d", context, errorCode);
break;
}
}
};

性能对比

指标 DELETE 模式 WAL 模式 提升
写入速度 100% 150-200% 50-100%
读取并发 阻塞 支持 显著提升
崩溃恢复 明显
并发读性能 受限 优秀 大幅提升

总结

WAL 模式确实能提升 SQLite 的性能和并发能力,但在 Android 平台使用时要注意:

  1. 正确管理数据库连接:在应用生命周期事件里(切后台、切前台)正确处理数据库
  2. 处理启动失败:准备回退方案,WAL 模式失败时切换到 DELETE 模式
  3. 文件管理:注意 WAL 和 SHM 文件的处理
  4. 单例模式:用单例管理数据库连接,避免多实例冲突
  5. 平台适配:Android 平台可能需要特殊处理,充分测试后再启用

跨平台游戏开发的建议:

  • Win32 调试阶段可以用 WAL 模式
  • Android 发布前要充分测试
  • 根据实际测试结果决定是否启用 WAL
  • 做好 WAL 模式失败的优雅降级

合理的架构设计和错误处理,能在享受 WAL 性能优势的同时保证稳定性。