打开数据库后可以PRAGMA journal_mode;查看
事实上journal_mode一共有6种:
DELETE/TRUNCATE/PERSIST:类似的,都是journal模式,把原始内容先备份出来,再往.db里面写
.
MEMORY:小事务频繁写入提交很快,断电就会出问题
.
WAL:大事务批量写入比MEMORY快,读取速度极快,综合能力最好,使用最广
.
OFF:写入极快,安全性等于无
以上6种,在设置PRAGMA journal_mode=xxx;时,只有设置WAL会持久化,其它的都会在下一次打开db时变回DELETE。
.db头中也可以看到端倪,其实也就是0x12开始,WAL模式是0x0202,其它模式都是0x0101:
| offset | size | description |
|---|---|---|
| 18 | 1 | File format write version. 1 for legacy; 2 for WAL. |
| 19 | 1 | File format read version. 1 for legacy; 2 for WAL. |
OK。我们来研究一下WAL
看看deepseek的总结:
WAL (Write-Ahead Logging) - 大多数现代应用的推荐选择
.
读取速度: ★★★★★ (极快)读操作不会与写操作竞争数据库文件的锁。读操作直接读主数据库文件,而写操作在WAL文件上进行。
支持高并发读取,一个线程在写入时,其他线程可以无阻塞地读取(读取器看到的是写入开始前的快照)。
.
写入速度: ★★★★☆ (很快)写入是“追加”到WAL文件末尾,这通常比随机写入主数据库文件更快,尤其是在HDD上。
缺点是提交事务时,需要调用fsync确保数据落盘,并且需要定期执行检查点 将WAL内容写回主数据库。
.
适用场景:需要高并发读写的应用(如Web服务器、桌面应用)。
读远多于写的场景。
追求最佳综合性能的通用场景。
db-wal/db-shm与db-journal的生命周期对比
| db-wal/db-shm | db-journal | |
|---|---|---|
| open db 后 | 不存在 | 不存在 |
| create table; 后 | 存在 | 不存在 |
| insert; 后 | 存在 | 存在 |
| commit; 后 | 存在 | 不存在 |
| close db 后 | 不存在 | 不存在 |
可以看到db-wal/db-shm的生命周期比db-journal要长的。
代码如下:
#include <stdio.h>
#include "sqlite3.h"
// 回调函数用于显示PRAGMA查询结果
static int callback(void* data, int argc, char** argv, char** azColName) {
for (int i = 0; i < argc; i++) {
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
return 0;
}
int main() {
sqlite3* db;
char* err_msg = NULL;
int rc;
// 删除之前的测试数据库(如果存在)
remove("int.db");
// 打开数据库连接
rc = sqlite3_open("int.db", &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
return 1;
}
printf("1. 数据库连接已建立\n");
// 先查询当前的日志模式
printf("2. 当前日志模式: ");
rc = sqlite3_exec(db, "PRAGMA journal_mode;", callback, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "查询日志模式失败: %s\n", err_msg);
sqlite3_free(err_msg);
return 1;
}
// 启用WAL模式
printf("3. 设置WAL模式: ");
rc = sqlite3_exec(db, "PRAGMA journal_mode=WAL;", callback, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "启用WAL模式失败: %s\n", err_msg);
sqlite3_free(err_msg);
return 1;
}
// 创建测试表
const char* create_table_sql =
"CREATE TABLE test_ints ("
"tiny INT,"
"small INT, "
"medium INT,"
"large INT,"
"negative INT"
")";
rc = sqlite3_exec(db, create_table_sql, NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "创建表失败: %s\n", err_msg);
sqlite3_free(err_msg);
return 1;
}
printf("4. 测试表已创建\n");
// 开始一个事务(显式开始)
rc = sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "开始事务失败: %s\n", err_msg);
sqlite3_free(err_msg);
return 1;
}
printf("5. 事务已开始\n");
// 插入数据但不提交
const char* insert_sql =
"INSERT INTO test_ints VALUES (1, 128, 32768, 2147483647, -1);"
"INSERT INTO test_ints VALUES (127, 255, 65535, 4294967295, -128);"
"INSERT INTO test_ints VALUES (0, 16384, 1000000, 1000000000, -32768);";
rc = sqlite3_exec(db, insert_sql, NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "插入数据失败: %s\n", err_msg);
sqlite3_free(err_msg);
return 1;
}
printf("6. 数据已插入但未提交\n");
// 提交事务
rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
fprintf(stderr, "提交事务失败: %s\n", err_msg);
sqlite3_free(err_msg);
}
else {
printf("7. 事务已提交\n");
}
sqlite3_close(db);
return 0;
}
通过在COMMIT之后查看WAL文件的二进制,我们可以分析WAL的格式
| offset | size | description |
|---|---|---|
| 0x0 | 32 | WAL头 |
| 0x20 | 24 | 第一帧的头 |
| 0x38 | 0x1000 | 第一帧(第一个页面的所有数据) |
| 0x1038 | 24 | 第二帧的头 |
| 0x1050 | 0x1000 | 第二个帧(除了头部的一些字节其余全为0) |
| 0x2050 | 24 | 第三帧的头 |
| 0x2068 | 0x1000 | 第三帧(第二个页面的所有数据) |
头:
37 7F 06 82 00 2D E2 18 00 00 10 00 00 00 00 00 A7 12 EB 0B 17 75 92 BC E0 D2 15 BA 55 4C 62 76 00 00 00 01 00 00 00 00 A7 12 EB 0B 17 75 92 BC 80 AC CA CA 62 15 C8 5D
| offset | size | description | mine |
|---|---|---|---|
| 0x0 | 4 | Magic number. 0x377f0682 or 0x377f0683 | 37 7F 06 82 |
| 0x4 | 4 | 文件格式的版本,目前是3007000(十进制) = 大端十六进制0x2DE218. | 00 2D E2 18 |
| 0x8 | 4 | Database page size. Example: 1024 | 00 00 10 00 |
| 0xc | 4 | 记录检查点执行的次数。 这个值为0,表示这个WAL文件是初始创建的,还没有被检查点过程处理过。 | 00 00 00 00 |
| 0x10 | 4 | Salt-1(盐数1)一个随机数,每次检查点后都会改变。 它和Salt-2一起,用于唯一标识一个特定的WAL文件生命周期,防止重复使用或错误的WAL文件。 | A7 12 EB 0B |
| 0x14 | 4 | Salt-2: 另一个随机数,每次检查点后都会改变。 | |
| 0x18 | 4 | Checksum-1: 对以上6个数即24字节进行校验和,第一部分 | E0 D2 15 BA |
| 0x1c | 4 | Checksum-2: 第二部分 | 55 4C 62 76 |
| – | – | -以下是第一帧- | – |
| 0x20 | 4 | 页号,这个帧是数据库中的第几页的副本。大端序。 | 00 00 00 01 |
| 0x24 | 4 | 对于提交记录,这里记录提交后页面的总个数。对于其它记录,此处是0. | 00 00 00 00 |
| 0x28 | 4 | Salt-1 (盐值1) 是从WAL header抄过来的,防止不同数据库的WAL混淆 | A7 12 EB 0B |
| 0x2d | 4 | Salt-2 (盐值2) 同上 | 17 75 92 BC |
| 0x30 | 4 | Checksum-1: 累积校验和,包括本页的帧头和帧数据,还会在其它页继续累积。用于完整性验证 | 80 AC CA CA |
| 0x34 | 4 | Checksum-2: 上面校验和的第二部分. | 62 15 C8 5D |
2990

被折叠的 条评论
为什么被折叠?



