c++中的SQLite.数据库是BUSY(多线程)

SQLite in C++. DB is BUSY (Multithread)

本文关键字:多线程 BUSY 中的 SQLite 数据库 c++      更新时间:2023-10-16

我遇到了一个问题。我在我的c++项目中使用SQLite3。在日志中,我有错误:DB是locked error code 5。据我所知,错误码5表示DB忙。为了解决这个问题,我开始使用WAL日志模式。但这没有用。

在我的程序中,我有两个连接到同一个DB。我对两个DB连接都使用互斥锁。我用下面的代码打开连接:

if (sqlite3_open_v2(db_path.c_str(), &this->db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, 0) ) {
    LOG4CPLUS_FATAL(this->logger, "Can not open/create DB " << sqlite3_errmsg(db));
    sqlite3_close(this->db);
}
if (sqlite3_exec(this->db, "PRAGMA journal_mode = WAL;", 0, 0, &err)) {
    LOG4CPLUS_ERROR(this->logger, "SQL det journal mode error: " << err);
    sqlite3_free(err);
}

第一个连接用于向DB插入数据。它每秒发生4次。
第二个连接用于启动事务、选择、更新、删除数据和提交。每5秒发生一次。

第一个连接出现错误。

请帮我解决这个问题。

更新:

第一次连接:

void readings_collector::flushToDb()
{
    this->db_mutex.lock();
    LOG4CPLUS_DEBUG(this->logger, "Flush to DB start.");
    const char *query = "INSERT INTO `readings` (`sensor_id`, `value`, `error`, `timestamp`) VALUES (?,?,?,?)";
    sqlite3_stmt *stmt = NULL;
    int rc = sqlite3_prepare_v2(this->db, query, -1, &stmt, NULL);
    if (SQLITE_OK != rc) {
        LOG4CPLUS_ERROR(this->logger, "sqlite prepare insert statment error: " << sqlite3_errmsg(this->db));
    }
    LOG4CPLUS_TRACE(this->logger, "--------------------");
    LOG4CPLUS_TRACE(this->logger, this->readings.size());
    while(!this->readings.empty()) {
        sensor::reading temp_reading = this->readings.front();
        this->readings.pop();
        LOG4CPLUS_TRACE(this->logger, "Reading " << temp_reading.sensor_id << " : " << temp_reading.value << " : " << temp_reading.error << " : " << temp_reading.timestamp);
        sqlite3_clear_bindings(stmt);
        sqlite3_bind_int(stmt, 1, temp_reading.sensor_id);
        sqlite3_bind_text(stmt, 2, temp_reading.value.c_str(), sizeof(temp_reading.value.c_str()), NULL);
        sqlite3_bind_int(stmt, 3, temp_reading.error);
        sqlite3_bind_int(stmt, 4, temp_reading.timestamp);
        rc = sqlite3_step(stmt);
        if (SQLITE_DONE != rc) {
            LOG4CPLUS_ERROR(this->logger, "sqlite insert statment exec error: " << sqlite3_errmsg(this->db) << "; status: " << rc);
        }
    }
    sqlite3_finalize(stmt);
    LOG4CPLUS_TRACE(this->logger, "Flush to DB finish.");
    this->db_mutex.unlock();
}

第二连接:

void dataSend_task::sendData()
{
    this->db_mutex.lock();
    char *err = 0;
    LOG4CPLUS_INFO(this->logger, "Send data function");
    if (sqlite3_exec(this->db, "BEGIN TRANSACTION", 0, 0, &err)) {
        LOG4CPLUS_ERROR(this->logger, "SQL exec error: " << err);
        sqlite3_free(err);
    }
    if (sqlite3_exec(this->db, this->SQL_UPDATE_READINGS_QUERY, 0, 0, &err)) {
        LOG4CPLUS_ERROR(this->logger, "SQL exec error: " << err);
        sqlite3_free(err);
    }
    this->json.clear();
    this->readingsCounter = 0;
    if (sqlite3_exec(this->db, this->SQL_SELECT_READINGS_QUERY, +[](void *instance, int x, char **y, char **z) {
                         return static_cast<dataSend_task *>(instance)->callback(0, x, y, z);
                     }, this, &err)) {
        LOG4CPLUS_ERROR(this->logger, "SQL exec error: " << err);
        sqlite3_free(err);
    } else {
        LOG4CPLUS_TRACE(this->logger, "Json data:  " << this->json);
        if (this->curlSend()) {
            if (sqlite3_exec(this->db, this->SQL_DELETE_READINGS_QUERY, 0, 0, &err)) {
                LOG4CPLUS_ERROR(this->logger, "SQL exec error: " << err);
                sqlite3_free(err);
            }
        }
    }
    if (sqlite3_exec(this->db, "COMMIT", 0, 0, &err)) {
        LOG4CPLUS_ERROR(this->logger, "SQL exec error: " << err);
        sqlite3_free(err);
    }
    this->db_mutex.unlock();
    this->json.clear();
}

您无疑已经意识到,SQLite一次只允许一个连接更新数据库。

从您粘贴的代码中,看起来好像您有两个单独的互斥体,一个用于readings_collector实例,另一个用于dataSend_task实例。这可以防止两个函数中的每个函数多次执行,但不能防止两个函数同时运行。

从你的问题中我不清楚互斥锁的目的是什么,但它肯定不会阻止两个连接同时尝试更新数据库。

我可以建议两个方法来解决你的问题。

第一种方法是在两个实例之间使用一个共享互斥锁,这样一次只有一个实例可以更新数据库。

第二种方法是利用SQLite在访问数据库时提供的解决争用的功能。SQLite允许您安装一个"繁忙处理程序",它将在尝试访问已被另一个线程或进程锁定的数据库时被调用。繁忙处理程序可以采取所需的任何操作,但最简单的情况通常只是等待一段时间然后再试一次,这是由内置的繁忙处理程序提供的,您可以通过调用sqlite3_busy_timeout来安装该处理程序。

例如,在打开数据库连接后,您可以这样做:

sqlite3_busy_timeout(this->db, 1000); // Wait 1000mS if busy

也可以通过命令设置这样的超时,使用busy_timeout pragma。

你也可以考虑使用BEGIN IMMEDIATE TRANSACTIONBEGIN EXCLUSIVE TRANSACTION启动你的事务,这样可以保证事务在没有阻塞的情况下完成。

请查看这两个Stack Overflow帖子。它们似乎与你的问题有关。

同一个sqlite's数据库的不同连接可以并发开始事务吗?

如果你阅读SQLite文档,你会看到它支持多个只读连接,不能写入数据库从多个连接,因为它不是为

从多个连接并发读写Sqlite数据库数据

多个进程可以同时打开同一个sqlite数据库在时间上,可以同时满足多个读访问。

在写操作的情况下,对数据库的单次写操作将锁定数据库短时间内,什么都没有,即使是阅读,也可以访问数据库文件。

从3.7.0版本开始,一个新的"预写日志记录"(WAL)选项是可用的。读和写可以同时进行。缺省情况下,未启用WAL功能。要打开WAL,请参考Sqlite文档。