HarmonyOS鸿蒙Next中多线程写同一个数据库报错
HarmonyOS鸿蒙Next中多线程写同一个数据库报错 主线程和Worker线程打开同一个数据库,同时写入数据,有时会报错:
Error:SQLite:The database file is locked.
这种场景有什么好办法解决吗?
多线程如何操作数据库请参考如下:
【背景知识】
- 关系型数据库(Relational Database,RDB)是一种基于关系模型来管理数据的数据库。关系型数据库基于SQLite组件提供了一套完整的对本地数据库进行管理的机制,对外提供了一系列的增、删、改、查等接口,也可以直接运行用户输入的SQL语句来满足复杂的场景需要。
- 共享关系型数据库(sendableRelationalStore)该模块针对关系型数据库(Relational Database,RDB)提供了sendable支持。支持从查询结果集中获取sendable类型ValuesBucket用于并发实例间传递。
- Worker是与主线程并行的独立线程。主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与宿主线程分离,在后台线程中运行一个脚本处理耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞宿主线程的运行。
- TaskPool为应用程序提供多线程环境,降低资源消耗、提高系统性能,无需管理线程生命周期。TaskPool支持开发者在宿主线程提交任务到任务队列,系统选择合适的工作线程执行任务,再将结果返回给宿主线程。接口易用,支持任务执行、取消和指定优先级,同时通过系统统一线程管理,结合动态调度及负载均衡算法,可以节约系统资源。
【解决方案】
-
场景一:在主线程中,对简单少量数据进行数据库操作。 对于简单的数据,并且没有频繁操作,不会阻塞主线程,可以直接在主线程操作数据库。代码示例如下:
import { relationalStore } from '@kit.ArkData'; async function create(context: Context) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 创建表 const CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS test (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "name TEXT NOT NULL, " + "age INTEGER, " + "salary REAL, " + "blobType BLOB)"; await store.executeSql(CREATE_TABLE_SQL); console.info(`Create table test successfully!`); } async function insert(context: Context, valueBucketArray: Array<relationalStore.ValuesBucket>) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 数据插入 await store.batchInsert("test", valueBucketArray as Object as Array<relationalStore.ValuesBucket>); } async function query(context: Context): Promise<Array<relationalStore.ValuesBucket>> { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 获取结果集 let predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates("test"); let resultSet = await store.query(predicates); // 查询所有数据 console.info(`Query data successfully! row count:${resultSet.rowCount}`); let index = 0; let result = new Array<relationalStore.ValuesBucket>(resultSet.rowCount) resultSet.goToFirstRow() do { result[index++] = resultSet.getRow() } while (resultSet.goToNextRow()); resultSet.close(); return result } async function clear(context: Context) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name await relationalStore.deleteRdbStore(context, CONFIG); console.info(`Delete Store.db successfully!`); } @Entry @Component struct RdbPage { @State message: string = 'Hello World'; build() { RelativeContainer() { Text(this.message) .id('RdbPageHelloWorld') .fontSize($r('app.float.page_text_font_size')) .fontWeight(FontWeight.Bold) .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center }, middle: { anchor: '__container__', align: HorizontalAlign.Center } }) .onClick(async () => { let context = getContext(this); let v: relationalStore.ValuesBucket = { id: 0, name: "zhangsan", age: 20, salary: 5000 }; await create(context) await insert(context, [v]) let index = 0 let ret = await query(context) as Array<relationalStore.ValuesBucket> for (let v of ret) { console.info(`Row[${index}].id = ${v.id}`) console.info(`Row[${index}].name = ${v.name}`) console.info(`Row[${index}].age = ${v.age}`) console.info(`Row[${index}].salary = ${v.salary}`) index++ } await clear(context) }) } .height('100%') .width('100%') } } -
场景二:在子线程中,对批量数据进行频繁数据库操作。 对于需要频繁数据库操作的场景,由于读写数据库存在耗时,因此推荐在子线程中操作,避免阻塞UI线程。
使用Worker进行频繁数据库操作。
第一步,创建Worker,在Worker中进行数据库的创建、插入、查询和清除等操作。代码示例如下:
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; import relationalStore from '@ohos.data.relationalStore'; let context: Context; const workerPort: ThreadWorkerGlobalScope = worker.workerPort; export async function create(context: Context) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 创建表 const CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS test (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "name TEXT NOT NULL, " + "age INTEGER, " + "salary REAL, " + "blobType BLOB)"; await store.executeSql(CREATE_TABLE_SQL); console.info(`Create table test successfully!`); } export async function insert(context: Context, valueBucketArray: Array<relationalStore.ValuesBucket>) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 数据插入 await store.batchInsert("test", valueBucketArray as Object as Array<relationalStore.ValuesBucket>); } export async function query(context: Context): Promise<Array<relationalStore.ValuesBucket>> { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 获取结果集 let predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates("test"); let resultSet = await store.query(predicates); // 查询所有数据 console.info(`Query data successfully! row count:${resultSet.rowCount}`); let index = 0; let result = new Array<relationalStore.ValuesBucket>(resultSet.rowCount) resultSet.goToFirstRow() do { result[index++] = resultSet.getRow() } while (resultSet.goToNextRow()); resultSet.close(); return result } export async function clear(context: Context) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name await relationalStore.deleteRdbStore(context, CONFIG); console.info(`Delete Store.db successfully!`); } workerPort.onmessage = async (e: MessageEvents) => { console.info('worker onmessage' + `${e}`) switch (e.data.code) { case 1: // 建表 create(e.data.data); break; case 2: // 添加数据 insert(context, e.data.data) break; case 3: // 查询数据 let ret = await query(e.data.data); // 将处理结果返回主线程 workerPort.postMessage({ code: 103, data: ret }) break; case 4: // 删除 clear(e.data.data) break; case 99: // 传入context context = e.data.data; break; default: } console.info('worker onmessage end') } workerPort.onmessageerror = (e: MessageEvents) => { console.info('worker onmessageerror' + e) } workerPort.onerror = (e: ErrorEvent) => { }第二步,UI主线程调用子任务,完成数据库的增删改查等操作。代码示例如下:
import { MessageEvents, worker } from '@kit.ArkTS'; import { relationalStore } from '@kit.ArkData'; const workerInstance = new worker.ThreadWorker("entry/ets/workers/RdbWorker.ets"); let context = getContext() @Entry @Component struct WorkerRdb { @State message: string = 'Hello World'; aboutToAppear(): void { // context传给worker workerInstance.postMessage({ code: 99, data: context }); workerInstance.onmessage = (e: MessageEvents): void => { console.info('index onmessage' + `${e}`) if (e.data.code == 103) { let ret = e.data.data as Array<relationalStore.ValuesBucket> let index = 0 for (let v of ret) { console.info(`Row[${index}].id = ${v.id}`) console.info(`Row[${index}].name = ${v.name}`) console.info(`Row[${index}].age = ${v.age}`) console.info(`Row[${index}].salary = ${v.salary}`) index++ } } } } build() { Column() { Button('创建数据库').margin({ top: 10 }) .onClick(() => { workerInstance.postMessage({ code: 1, data: context }); }) Button('插入数据').margin({ top: 10 }) .onClick(() => { // 数据准备 const count = 5 let valueBucketArray = new Array<relationalStore.ValuesBucket>(count); for (let i = 0; i < count; i++) { let v: relationalStore.ValuesBucket = { id: i, name: "zhangsan" + i, age: 20, salary: 5000 + 50 * i }; valueBucketArray[i] = v; } workerInstance.postMessage({ code: 2, data: valueBucketArray }); }) Button('查询数据').margin({ top: 10 }) .onClick(() => { workerInstance.postMessage({ code: 3, data: context }); }) Button('清除').margin({ top: 10 }) .onClick(() => { workerInstance.postMessage({ code: 4, data: context }); }) } .height('100%') .width('100%') } } -
场景三:在子线程中,使用Sendable进行大容量数据库操作。 由于数据库数据跨线程传递存在耗时,数据量较大时会占用UI主线程。推荐使用Sendable封装数据库数据,以降低跨线程开销。
使用TaskPool和Sendable进行大容量数据库操作。
使用Worker和Sendable进行大容量数据库操作。
第一步,定义数据库中的数据格式,可以使用Sendable,以减少跨线程操作的耗时。代码示例如下:
// SharedValuesBucket.ets export interface IValueBucket { id: number name: string age: number salary: number } @Sendable export class SharedValuesBucket implements IValueBucket { id: number = 0 name: string = "" age: number = 0 salary: number = 0 constructor(v: IValueBucket) { this.id = v.id; this.name = v.name; this.age = v.age; this.salary = v.salary } }第二步,创建worker,在worker中进行数据库的创建、插入、查询和清除等操作,插入数据和查询返回数据都使用Sendable数据对象。代码示例如下:
// SendableRdbWorker.ets import { collections, ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; import { relationalStore, ValuesBucket } from '@kit.ArkData'; import { IValueBucket, SharedValuesBucket } from '../pages/SharedValuesBucket'; let context: Context; const workerPort: ThreadWorkerGlobalScope = worker.workerPort; export async function create(context: Context) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 创建表 const CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS test (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "name TEXT NOT NULL, " + "age INTEGER, " + "salary REAL, " + "blobType BLOB)"; await store.executeSql(CREATE_TABLE_SQL); console.info(`Create table test successfully!`); } export async function insert(context: Context, valueBucketArray: collections.Array<SharedValuesBucket | undefined>) { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 数据插入 await store.batchInsert("test", valueBucketArray as Object as Array<ValuesBucket>); } export async function query(context: Context): Promise<collections.Array<SharedValuesBucket | undefined>> { const CONFIG: relationalStore.StoreConfig = { name: "Store.db", securityLevel: relationalStore.SecurityLevel.S1, }; // 默认数据库文件路径为context.databaseDir + rdb + StoreConfig.name let store: relationalStore.RdbStore = await relationalStore.getRdbStore(context, CONFIG); console.info(`Create Store.db successfully!`); // 获取结果集 let predicates: relationalStore.RdbPredicates = new relationalStore.RdbPredicates("test"); let resultSet = await store.query(predicates); // 查询所有数据 console.info(`Query data successfully! row count:${resultSet.rowCount}`); let index = 0; let result = collections.Array.create<SharedValues
更多关于HarmonyOS鸿蒙Next中多线程写同一个数据库报错的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
Hello
,
不能加个状态控制让一方读写的时候让另外一方暂停等待嘛?
不太行,主线程写入数据库的时机随机,不太好控制,
试试这样?:主线程和 Worker 线程不直接写数据库,而是将写任务放入一个专门的队列。由一个专用的数据库写入线程从队列中取出任务并执行。
在HarmonyOS鸿蒙Next中,多线程同时写入同一数据库会触发并发冲突,导致报错。鸿蒙Next的数据库访问默认不支持多线程直接并发写操作。
在HarmonyOS Next中,多个线程(包括主线程和Worker线程)直接并发写入同一个SQLite数据库文件,出现database file is locked错误是预期行为。这是因为SQLite的默认并发模型是串行化的,同一时刻只允许一个写入操作。
针对你的场景,推荐的核心解决方案是:使用单例的、线程/Worker间共享的数据库连接进行串行化访问,而不是每个线程各自打开一个连接。
具体可以通过以下两种方式实现:
-
使用“多线程模式”打开数据库连接 在初始化数据库时,通过设置
OpenFlags.READ_WRITE | OpenFlags.CREATE | OpenFlags.SHARED_CACHE | OpenFlags.NO_MUTEX来启用共享缓存和禁用互斥锁(NO_MUTEX),这允许同一个进程内的多个线程使用同一个数据库连接。但请注意,你仍然需要自己协调写入顺序,避免同时写。// 示例代码片段 import relationalStore from '@ohos.data.relationalStore'; let context = ...; // 获取UIAbilityContext const config = { name: 'yourDb.db', securityLevel: relationalStore.SecurityLevel.S1 }; // 关键:使用SHARED_CACHE和NO_MUTEX标志 relationalStore.getRdbStore(context, config, (err, store) => { if (err) { console.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`); return; } // 将此store实例通过线程间通信方式(如Port)共享给Worker线程使用 // Worker线程不应再自己调用getRdbStore }, relationalStore.OpenFlags.READ_WRITE | relationalStore.OpenFlags.CREATE | relationalStore.OpenFlags.SHARED_CACHE | relationalStore.OpenFlags.NO_MUTEX); -
设计一个中心化的数据访问层(推荐)
- 在主线程(或一个专门的Worker线程)中唯一地打开并持有数据库连接(RdbStore实例)。
- 主线程与Worker线程之间通过消息通信(例如使用
WorkerPort或Emitter)来发送数据库操作请求(如插入、更新命令)。 - 所有实际的数据库写入操作,都交由这个唯一的持有者来串行执行。这自然避免了并发写入冲突。
- 这种方式结构清晰,数据一致性最容易控制。
总结: 根本原因是多个独立的数据库连接并发写入。解决方案的核心是确保整个应用进程内,对同一个数据库的写入操作通过同一个连接串行进行。采用中心化访问层是更稳健的架构选择。

