鸿蒙Qt数据库实战:SQLite死锁与沙箱路径陷阱

1. 灾难现场

在开发一款本地记事本应用时,我们使用了Qt自带的QSqlDatabase (SQLite) 来存储数据。
代码在Windows上运行得行云流水,但在鸿蒙真机上测试时,出现了两个严重问题:

  1. 无法创建数据库QSqlDatabase::open() 返回 false,错误信息提示 unable to open database file
  2. 频繁死锁:当我们在后台线程同步数据的同时,在主线程读取列表,程序抛出 database is locked 错误。

2. 陷阱一:沙箱路径

在Desktop OS上,我们可以随意在 D:/data.db./data.db 创建文件。
但在鸿蒙系统中,应用被严格限制在沙箱内。

错误代码:

db.setDatabaseName("data.db"); // 试图在当前工作目录创建

在鸿蒙应用启动时,当前工作目录(CWD)可能并不是一个可写的目录,或者是一个临时的系统路径。

正确姿势:
必须使用QStandardPaths获取应用私有数据目录。

QString dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
// 输出示例: /data/app/el2/100/base/com.example.note/files/
QDir dir(dataDir);
if (!dir.exists()) dir.mkpath(".");

QString dbPath = dir.filePath("note.db");
db.setDatabaseName(dbPath);

注意: 确保目录存在!QSqlDatabase不会自动创建父目录。

3. 陷阱二:并发死锁 (Database is locked)

SQLite默认使用回滚日志(Journal)模式,在这种模式下,写操作是独占的。当Qt主线程在读取(Select),而后台线程试图写入(Insert)时,就会发生冲突。

在鸿蒙的文件系统上,IO性能可能不如PC SSD,导致锁等待时间变长,更容易触发 busy 错误。

锁等待示意图

MainThread (Reader)SQLiteWorkerThread (Writer)MainThreadWorkerThreadSELECT * FROM notes (Shared Lock)BEGIN TRANSACTIONINSERT INTO notes (Reserved Lock)COMMIT (Needs Exclusive Lock)BUSY! (Reader has Shared Lock)Wait... Timeout... ERROR!MainThread (Reader)SQLiteWorkerThread (Writer)MainThreadWorkerThread

4. 解决方案:开启WAL模式

WAL (Write-Ahead Logging) 模式允许读写并发。读操作不再阻塞写操作,写操作也不阻塞读操作。

代码实现:

在打开数据库后,立即执行Pragma指令。

bool initDatabase() {
    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(getDbPath());
    
    if (!db.open()) {
        qCritical() << "Open failed:" << db.lastError();
        return false;
    }

    // 开启WAL模式
    QSqlQuery query;
    if (!query.exec("PRAGMA journal_mode = WAL")) {
        qWarning() << "Failed to set WAL mode";
    } else {
        if (query.next()) {
            qDebug() << "Journal mode set to:" << query.value(0).toString();
        }
    }

    // 增加忙等待超时 (默认可能很短)
    query.exec("PRAGMA busy_timeout = 5000"); // 5秒
    
    return true;
}

5. 架构优化:单例模式管理连接

在多线程环境使用QSqlDatabase,Qt文档建议每个线程使用不同的连接名(Connection Name)。
但为了管理方便,我们建议封装一个线程安全的单例类。

// DBManager.h
class DBManager {
public:
    static DBManager& instance();
    
    // 获取当前线程的数据库连接
    QSqlDatabase getDatabase();
    
private:
    // 线程本地存储,确保每个线程有独立的连接名
    static thread_local QString m_connectionName;
};
// DBManager.cpp
thread_local QString DBManager::m_connectionName;

QSqlDatabase DBManager::getDatabase() {
    if (m_connectionName.isEmpty()) {
        // 生成唯一连接名,如 "conn_0x1234abcd"
        m_connectionName = QString("conn_%1").arg((quintptr)QThread::currentThreadId());
    }

    if (!QSqlDatabase::contains(m_connectionName)) {
        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", m_connectionName);
        db.setDatabaseName(getGlobalDbPath());
        db.open();
        // Set WAL...
    }
    
    return QSqlDatabase::database(m_connectionName);
}

6. 总结

在鸿蒙上使用SQLite,核心要点只有三个:

  1. 路径对不对:必须用 QStandardPaths::AppDataLocation
  2. 模式开没开:强烈建议开启 WAL 模式提升并发性能。
  3. 连接管没管:多线程必须用不同的连接名,切忌跨线程共用 QSqlDatabase 对象。

做好了这三点,SQLite在鸿蒙上依然是那个轻量、可靠的数据存储王者。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

淼学派对

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值