当连接未正确关闭时,为啥使用 WAL 模式的 SQLite 数据库中的数据会丢失?

Posted

技术标签:

【中文标题】当连接未正确关闭时,为啥使用 WAL 模式的 SQLite 数据库中的数据会丢失?【英文标题】:Why data is lost in SQLite database with WAL mode on when connection is not closed properly?当连接未正确关闭时,为什么使用 WAL 模式的 SQLite 数据库中的数据会丢失? 【发布时间】:2021-10-04 21:14:27 【问题描述】:

问题:如果先前的连接未正确关闭,则 SELECT 无法使用 WAL 模式(预写日志)从新连接到 SQLite DB 获取数据。主要问题是:为什么数据会丢失?有什么方法可以让交易丢失?

我正在尝试将数据存储在带有WAL mode 的 SQLite 表中。我将描述 3 种情况:情况 A 导致交易丢失,情况 B 和 C - 不要。

案例A(退出应用,不关闭连接):

    打开应用程序,打开连接 1,打开连接 2(在同一个数据库上) 开始事务(连接 1) INSERT(连接 1) 结束事务(连接 1) 重复步骤 1-3 5 次 SELECT (Connection 1) // 所有数据都存在。 SELECT (Connection 2) // 所有数据都存在。 关闭应用程序(不关闭连接 1) 打开应用程序,打开连接 3 SELECT (Connection 3) // 一些数据丢失了 - 例如,select 可以返回 5 个插入事务中的 2 个。或 5 笔交易中的 0 笔。完全随机。

案例B(退出应用前关闭连接):

    打开应用程序,打开连接 1,打开连接 2(在同一个数据库上) 开始事务(连接 1) INSERT(连接1) 结束事务(连接 1) 重复步骤 1-3 5 次 SELECT (Connection 1) // 所有数据都存在。 SELECT (Connection 2) // 所有数据都存在。 关闭连接 1 关闭申请 打开应用程序,打开连接 3 SELECT (Connection 3) // 所有数据都存在。

案例C(关闭应用前执行检查点+不关闭连接):

    打开应用程序,打开连接 1,打开连接 2(在同一个数据库上) 开始事务(连接 1) INSERT(连接 1) 结束事务(连接 1) 重复步骤 1-3 5 次 SELECT (Connection 1) // 所有数据都存在。 SELECT (Connection 2) // 所有数据都存在。 执行 WAL 检查点 关闭应用程序(不关闭连接 1) 打开应用程序,打开连接 3 SELECT (Connection 3) // 所有数据都存在。

综上所述,当我在关闭应用程序之前从表中SELECT 时,所有数据都存在,但在应用程序关闭错误后(例如,如果应用程序崩溃),我插入的一些数据丢失了。但是,如果我在关闭应用程序之前执行检查点(或在关闭应用程序之前关闭连接)-所有数据都可用。

额外信息:

    如果我重新打开应用程序(案例 A)后执行检查点,则不会出现事务(不要从日志转到主 db 文件)。 WAL 挂钩:我使用sqlite3_wal_hook 注册了回调,以检查事务是否实际提交到 WAL 日志文件,它表明页面已成功写入日志文件。 WAL 文件:我尝试使用 android Studio 中的设备文件资源管理器或通过将其从内部存储 (/data/data/com.package.my/files) 和大多数复制到外部存储来查看 -wal 文件的时间它要么是空的,要么不存在。 线程安全:我尝试在打开数据库时使用 SQLITE_OPEN_FULLMUTEX 标志打开 SERIALIZED 线程安全模式:

sqlite3_open_v2(db_name.c_str(), &handle, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nullptr);

没有任何区别。但是,它会导致从第二个连接读取问题,所以我使用 sqlite3_open 而不使用 SQLITE_OPEN_FULLMUTEX

堆栈:android 7 - JNI - c++ 11 - sqlite 3.27.2

更新。按照@bwt 的建议尝试了PRAGMA synchronous = EXTRAFULL - 没有帮助。

代码:

int wal_hook(void* userdata, sqlite3* handle, const char* dbFilename, int nPages)

    char* pChar;
    pChar = (char*)userdata; // "test"

    printf("Hello hook");
    return SQLITE_OK;


// DB init (executed once on app start)
void initDB() 
    int32 rc = sqlite3_open(db_name.c_str(), &handle); // rc = 0

    // check threadsafe mode
    int stResult = sqlite3_threadsafe(); // stResult = 1

    // register WAL hook
    char* pointerContext = new char[4]();
    pointerContext[0] = 't';
    pointerContext[1] = 'e';
    pointerContext[2] = 's';
    pointerContext[3] = 't';
    sqlite3_wal_hook(handle, wal_hook, pointerContext);

    // turn WAL mode on
    int32 rcWAL = sqlite3_exec(handle, "PRAGMA journal_mode=WAL;", processResults, &result, &errMsg); // rcWAL = 0


// close connection
int32 close() 
    return sqlite3_close(handle);


// WAL checkpoint
sqlite3_exec(handle, "pragma wal_checkpoint;", processResults, &result, &errMsg);

// Insert
EventPtr persist(EventPtr obj) 
    vector<DBData*> args;
    int beginResult = sqlite3_exec(_connector->handle, "BEGIN TRANSACTION;", NULL, NULL, NULL);

    try 
        args.push_back(&obj->eventId);
        // ... push more args

        string query = "insert into events values(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14);";
        int32_t rc = _connector->executePreparedWOresult(query.c_str(),&args);
        if(rc == SQLITE_DONE) 
            int endResult = sqlite3_exec(_connector->handle, "END TRANSACTION;", NULL, NULL, NULL);
            return obj;
        
     catch(...) 


// SELECT
vector<EventPtr> readAll()

    string query = "select * from Events;";
    ResultSetPtr result= _connector->executePrepared(query.c_str(), NULL);
    vector<EventPtr> vec;
    for(int32_t i = 0; i < result->size(); i ++)
        EventPtr event(new Event);
        // init event
        vec.push_back(EventPtr(event));
    
    return vec;


// executePreparedWOresult
int32 executePreparedWOresult(const string& query, vector<DBData*> *args)
    sqlite3_stmt *stmt;
    cout << query ;
    sqlite3_prepare_v2(handle, query.c_str(), query.size(), &stmt, NULL);

    for(uint32 i = 0;args && i < args->size(); i ++)
        switch(args->at(i)->getType())
              // statement bindings (sqlite3_bind_text, etc.) 
        
    

    int32 rc = sqlite3_step(stmt);
    sqlite3_finalize(stmt);
    return rc;


// executePrepared
ResultSetPtr executePrepared(const char *query, const vector<DBData*> *args)
    ResultSetPtr res = ResultSetPtr(new ResultSet);
    sqlite3_stmt *stmt;
    int32_t rs = sqlite3_prepare_v2(handle, query, strlen(query), &stmt, NULL);

    for(uint32 i = 0;args && i < args->size(); i ++)
        switch(args->at(i)->getType())
              // statement bindings (sqlite3_bind_text, etc.) 
        
    
    int32 count = sqlite3_column_count(stmt);
    int32 rc;
    ResultRow row;
    int32 rows = 0;
    while((rc = sqlite3_step(stmt)) == SQLITE_ROW)
         rows ++;
         for(int32 i = 0; i < count; i++)
              // multiple row.push_back-s for all columns
         
         res->push_back(row);
         row.clear();
    
    sqlite3_finalize(stmt);
    return res;


// LUA parts: --------------------------------------------------------------


// 1.

local query = [[ SELECT val from Parameters WHERE name = "column_name"]]
local period = 0
for row in db:nrows(query) do 
    if row["val"] ~= nil then
        period = row["val"]
    end
end

// 2.

local table = 
if json ~= nil then
    table["event_id"] = in_json["event_id"]
    local query = [[ SELECT * FROM Events WHERE event_id = "%s" ]]
    query = string.format(query, table["event_id"])    
    for row in db:nrows(query) do
        table = row
    end
else
    json = 
    local query = [[ SELECT * FROM Events order by created DESC LIMIT 1; ]]
    for row in db:nrows(query) do
        table = row
    end
end

// 3.

function getRow(con, sql)
    local iRow = nil
    for a in con:nrows(sql) do
        iRow = a
    end
    return iRow
end
local termRow = getRow(db,[[SELECT value FROM parameters WHERE name='column_name']])

// 4.

local stmt = db:prepare("SELECT value FROM parameters WHERE name='column_name'")
local cnt = 0
for row in stmt:nrows() do
    cnt = cnt + 1
end
stmt:finalize() 

// 5.

local param = "N"
for Parameter in db_transport:nrows([[SELECT val FROM Parameters WHERE name = 'param']]) do  
    param = SParameter["val"]
end

【问题讨论】:

你在哪里提交事务? PRAGMA synchronous = EXTRA 有帮助吗?它改变了the journal file is synced @Shawn in persist(): sqlite3_exec(_connector->handle, "END TRANSACTION;", NULL, NULL, NULL);我在发布代码示例时犯了错误(现已修复) @bwt 尝试了您的建议 PRAGMA syncrounous=EXTRA - 没有帮助,很遗憾 @Superlokkus 感谢您的建议!我会检查pragma synchronous=EXTRA + 锁定模式独占并返回结果 【参考方案1】:

这一切都归结为我使用了两个不同的 SQLite 实例。

    C++ 使用的静态 libsqlite3 (3.27.2) 内部使用 sqlite 3.24.0 的 Lua 使用的静态 lsqlite3 (lsqlite3complete)

这里列出了为什么它是坏主意的更详细解释:https://www.sqlite.org/howtocorrupt.html#multiple_copies_of_sqlite_linked_into_the_same_application

解决方案: 在 C++ 端使用动态 libsqlite3.so 和链接到动态 libsqlite3.so 和 liblua.so 的动态 lsqlite3.so。这使得在 C++ 和 Lua 端使用相同的 sqlite3 库实例成为可能。

在与 LuaSQLite3 开发人员讨论后,更新了 lua.sqlite.org 上有关 lsqlite3complete 的文档。

http://lua.sqlite.org/index.cgi/doc/tip/doc/lsqlite3.wiki#overview

【讨论】:

以上是关于当连接未正确关闭时,为啥使用 WAL 模式的 SQLite 数据库中的数据会丢失?的主要内容,如果未能解决你的问题,请参考以下文章

当连接关闭时,未提交的事务会发生啥?

为啥当未接受的套接字发送消息时,套接字不会抛出错误?

GRDB使用SQLite的WAL模式

备份时的Android 9数据库日志模式WAL问题

为啥当 XMPP 连接断开时,出席类型用户从不可用变为可用

C ++中的基本字符串连接未正确输出[关闭]