【sqlite】WAL初探

打开数据库后可以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:

offsetsizedescription
181File format write version. 1 for legacy; 2 for WAL.
191File 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-shmdb-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的格式

offsetsizedescription
0x032WAL头
0x2024第一帧的头
0x380x1000第一帧(第一个页面的所有数据)
0x103824第二帧的头
0x10500x1000第二个帧(除了头部的一些字节其余全为0)
0x205024第三帧的头
0x20680x1000第三帧(第二个页面的所有数据)

头:
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

offsetsizedescriptionmine
0x04Magic number. 0x377f0682 or 0x377f068337 7F 06 82
0x44文件格式的版本,目前是3007000(十进制) = 大端十六进制0x2DE218.00 2D E2 18
0x84Database page size. Example: 102400 00 10 00
0xc4记录检查点执行的次数。 这个值为0,表示这个WAL文件是初始创建的,还没有被检查点过程处理过。00 00 00 00
0x104Salt-1(盐数1)一个随机数,每次检查点后都会改变。 它和Salt-2一起,用于唯一标识一个特定的WAL文件生命周期,防止重复使用或错误的WAL文件。A7 12 EB 0B
0x144Salt-2: 另一个随机数,每次检查点后都会改变。
0x184Checksum-1: 对以上6个数即24字节进行校验和,第一部分E0 D2 15 BA
0x1c4Checksum-2: 第二部分55 4C 62 76
-以下是第一帧-
0x204页号,这个帧是数据库中的第几页的副本。大端序。00 00 00 01
0x244对于提交记录,这里记录提交后页面的总个数。对于其它记录,此处是0.00 00 00 00
0x284Salt-1 (盐值1) 是从WAL header抄过来的,防止不同数据库的WAL混淆A7 12 EB 0B
0x2d4Salt-2 (盐值2) 同上17 75 92 BC
0x304Checksum-1: 累积校验和,包括本页的帧头和帧数据,还会在其它页继续累积。用于完整性验证80 AC CA CA
0x344Checksum-2: 上面校验和的第二部分.62 15 C8 5D
SQLiteWAL(Write-Ahead Logging)机制是一种用于提高数据库并发性和性能的日志实现方式。其核心思想是在执行事务修改之前,先将变更记录到日志文件中,而不是直接修改主数据库文件。这种机制确保了数据修改的持久性和一致性,同时显著减少了数据库锁的争用,从而提升了并发性能。 ### WAL 模式的工作原理 在 WAL 模式下,SQLite 数据库会生成一个额外的 `.wal` 文件(Write-Ahead Log 文件)以及一个 `.shm` 文件(共享内存文件),用于存储日志和索引信息。当事务开始时,所有的修改操作会被记录到 `.wal` 文件中,而不是直接写入主数据库文件。只有当事务提交时,这些日志记录才会被持久化到磁盘中。这种方式确保了事务的持久性,即使在系统崩溃的情况下,也能通过 `.wal` 文件中的日志恢复未提交的事务或完成已提交事务的修改。 具体来说,事务的修改首先会生成日志记录,这些记录包括被修改的数据页和相关的元数据。日志记录会按照顺序写入 `.wal` 文件中,同时 `.shm` 文件会维护这些日志记录的索引,以便快速查找和恢复。当事务提交时,SQLite 会确保日志记录已经写入磁盘,然后才通知应用程序事务提交成功。如果系统在此过程中发生崩溃,重启时 SQLite 会通过 `.wal` 文件中的日志恢复未完成的事务,确保数据的一致性。 ### WAL 模式的使用场景 WAL 模式特别适用于高并发读写操作的场景。由于其允许多个读操作和一个写操作同时进行,而不会阻塞彼此,因此非常适合需要频繁读取和写入的应用程序。例如,在移动设备上的应用程序中,WAL 模式能够显著提升数据库的性能,尤其是在网络请求频繁或用户交互频繁的情况下。 此外,WAL 模式还适用于需要高可靠性的场景。由于事务日志的持久化机制,即使在系统崩溃的情况下,也能够通过日志文件恢复数据,从而保证了数据的完整性和一致性。这种特性使得 WAL 模式在金融、医疗等对数据可靠性要求极高的领域中具有广泛的应用前景。 ### 与其他模式的对比 与传统的 Rollback Journal 机制相比,WAL 模式在性能和并发性方面具有明显优势。Rollback Journal 机制在修改数据库文件之前,会先将原始数据备份到一个临时文件中,然后进行修改。如果事务失败,系统会通过回滚操作恢复原始数据;如果事务成功,则删除备份文件。这种方式虽然简单有效,但在高并发环境下容易导致锁争用,从而降低性能。 相比之下,WAL 模式通过日志记录的方式避免了频繁的文件操作,减少了锁的持有时间,从而提高了并发性能。此外,WAL 模式还支持多读单写操作,进一步提升了系统的吞吐量。 ### 代码示例 以下是一个简单的 SQL 语句,用于启用 SQLite 数据库WAL 模式: ```sql PRAGMA journal_mode=WAL; ``` 执行该语句后,SQLite 数据库将切换到 WAL 模式,并生成相应的 `.wal` 和 `.shm` 文件。 ### 总结 SQLiteWAL 模式通过日志记录的方式,实现了高效的并发控制和数据持久化机制。它不仅提高了数据库的性能,还增强了数据的可靠性,适用于多种应用场景。理解 WAL 模式的工作原理及其适用场景,有助于开发者在实际项目中更好地利用 SQLite 的优势。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值