鸿蒙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 创建文件。

但在鸿蒙系统中,应用被严格限制在沙箱内。

错误代码:

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

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

正确姿势:

必须使用QStandardPaths获取应用私有数据目录。

cpp 复制代码
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) SQLite WorkerThread (Writer) MainThread WorkerThread SELECT * FROM notes (Shared Lock) BEGIN TRANSACTION INSERT INTO notes (Reserved Lock) COMMIT (Needs Exclusive Lock) BUSY! (Reader has Shared Lock) Wait... Timeout... ERROR! MainThread (Reader) SQLite WorkerThread (Writer) MainThread WorkerThread

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

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

代码实现:

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

cpp 复制代码
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)。

但为了管理方便,我们建议封装一个线程安全的单例类。

cpp 复制代码
// DBManager.h
class DBManager {
public:
    static DBManager& instance();
    
    // 获取当前线程的数据库连接
    QSqlDatabase getDatabase();
    
private:
    // 线程本地存储,确保每个线程有独立的连接名
    static thread_local QString m_connectionName;
};
cpp 复制代码
// 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在鸿蒙上依然是那个轻量、可靠的数据存储王者。

相关推荐
曹牧9 分钟前
Oracle:五笔码
数据库·oracle
今晚务必早点睡10 分钟前
MySQL 新手避坑指南:安装、区分、检查一步到位
数据库·mysql·adb
JIngJaneIL13 分钟前
基于java+ vue畅游游戏销售管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot·游戏
詹姆斯爱研究Java14 分钟前
基于Django的租房网站的设计与实现
数据库·python·django
deng-c-f14 分钟前
Linux C/C++ 学习日记(50):连接池
数据库·学习·连接池
2401_8603195219 分钟前
react-native-calendarsReact Native库来帮助你处理日期和时间,实现鸿蒙跨平台开发日历组件
react native·react.js·harmonyos
运维行者_21 分钟前
APM 性能监控是什么?从应用监控与网站监控了解基础概念
网络·数据库·云原生·容器·kubernetes·智能路由器·运维开发
全栈小521 分钟前
【数据库】当InfluxDB遇到天花板:金仓数据库如何重构时序性能极限?
数据库·重构
赵财猫._.22 分钟前
React Native鸿蒙开发实战(九):复杂业务场景实战与架构设计
react native·react.js·harmonyos
颜颜yan_35 分钟前
时序数据库性能较量:金仓数据库如何在高负载场景中领跑InfluxDB
数据库·时序数据库