鸿蒙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在鸿蒙上依然是那个轻量、可靠的数据存储王者。

相关推荐
罗光记17 分钟前
低空基础设施新突破!优刻得 ×IDEA联合发布 OpenSILAS一体机
数据库·经验分享·其他·百度·facebook
合作小小程序员小小店18 分钟前
web网页开发,在线%餐饮点餐%系统,基于Idea,html,css,jQuery,java,ssm,mysql。
java·前端·数据库·html·intellij-idea·springboot
p***434828 分钟前
SQL在业务智能中的分析函数
数据库·sql
j***29481 小时前
【MySQL — 数据库基础】深入理解数据库服务与数据库关系、MySQL连接创建、客户端工具及架构解析
数据库·mysql·架构
不羁的木木1 小时前
【开源鸿蒙跨平台开发学习笔记】Day02:React Native 开发 HarmonyOS-环境搭建篇(填坑记录)
笔记·学习·react native·harmonyos·har
tuokuac2 小时前
SQL中AND和逗号,的区别
java·数据库·sql
lqj_本人2 小时前
鸿蒙Qt网络通信:HTTPS握手失败与证书陷阱
qt·https·harmonyos
Klong.k2 小时前
关于sqlite
数据库·sqlite
DBA圈小圈3 小时前
【KingbaseES】V8R6查询数据库大小
数据库·database