HarmonyOS鸿蒙Next中多线程写同一个数据库报错

HarmonyOS鸿蒙Next中多线程写同一个数据库报错 主线程和Worker线程打开同一个数据库,同时写入数据,有时会报错:

Error:SQLite:The database file is locked.

这种场景有什么好办法解决吗?

7 回复

多线程如何操作数据库请参考如下:

【背景知识】

  • 关系型数据库(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线程。

    使用TaskPool进行频繁数据库操作

    使用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间共享的数据库连接进行串行化访问,而不是每个线程各自打开一个连接。

具体可以通过以下两种方式实现:

  1. 使用“多线程模式”打开数据库连接 在初始化数据库时,通过设置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);
    
  2. 设计一个中心化的数据访问层(推荐)

    • 在主线程(或一个专门的Worker线程)中唯一地打开并持有数据库连接(RdbStore实例)。
    • 主线程与Worker线程之间通过消息通信(例如使用WorkerPortEmitter)来发送数据库操作请求(如插入、更新命令)。
    • 所有实际的数据库写入操作,都交由这个唯一的持有者来串行执行。这自然避免了并发写入冲突。
    • 这种方式结构清晰,数据一致性最容易控制。

总结: 根本原因是多个独立的数据库连接并发写入。解决方案的核心是确保整个应用进程内,对同一个数据库的写入操作通过同一个连接串行进行。采用中心化访问层是更稳健的架构选择。

回到顶部