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

相关推荐
youyin5 天前
HarmonyOS ArkUI 组件与自定义组件零基础:从搭页面到组件化开发
华为·harmonyos
这个DBA有点耶5 天前
MVCC深入:Read View、版本链与快照读——InnoDB并发控制的内核
数据库·mysql·架构
DBA_G5 天前
从地面到云霄:GBase数据库在民航三大场景的落地实践
数据库
自由能燃气设备5 天前
商用全预混低氮冷凝锅炉免费方案vs付费方案对比+选型避坑指南
大数据·数据库·人工智能
科创致远5 天前
科创致远 ESOP 系统核心效能与实战价值展示
大数据·数据库·人工智能·精益工程
2601_962218615 天前
万象生鲜系统称重自动多退少补算法解决生鲜非标品痛点
大数据·数据库·人工智能·python·算法
慧都小项5 天前
当边缘AI上产线:QtitanDocking 如何让工业 HMI 实现多视图协同
人工智能·qt·ui·边缘计算·qt6.3
张洛闻Eren5 天前
k8s云原生【第十课】:水平 Pod 自动扩缩容
运维·数据库·云原生·kubernetes·github
于平安5 天前
MySQL-触发器
数据库·mysql
白远山5 天前
上海24小时自助健身房解决方案实战指南与经验分享
java·数据库·架构·需求分析