PAYATHON 2026

Expo SQLite 在 Fast Refresh 或完整重载后报 database is locked,如何排查?

数据小许

单例写法本身不能证明存在问题,现有报错也不足以认定是旧连接没有关闭。database is locked 表明当前数据库操作遇到了锁冲突;连接保持打开,并不等于它一直持有阻止其他操作的锁。

需要先查清重载前后有哪些数据库操作仍在执行,以及重载后是否重复启动了写入、事务或初始化任务。仅凭提供的代码,无法确定具体根因,也无法确认这是 Expo SQLite 的缺陷。

单例为什么没有解决重载后的问题?

SQLiteConnectorV2.instance 只能约束当前模块实例中的连接创建。Fast Refresh 可能重新执行模块,完整重载则会重建 JavaScript 运行环境,因此不能把这个静态字段当成跨重载的连接管理机制。

globalThis 可以在同一个 JavaScript 运行环境中保存引用,但不能跨越运行环境的销毁与重建。观察到它被重置,既不能证明原生连接已经释放,也不能证明原生连接仍然存在。

SQLite 允许多个连接访问同一个数据库。排查时应关注连接上的操作,例如未结束的写事务、尚未完成的语句,以及多个连接同时写入。

先定位重载后的第一条失败操作

建议给连接创建、数据库初始化和 Repository 的主要入口添加日志,记录操作的开始、结束和异常。下面的包装函数可以记录异步操作的执行区间:

let operationId = 0;

async function traceDbOperation<T>(
  label: string,
  operation: () => Promise<T>,
): Promise<T> {
  const id = ++operationId;
  console.log(`[db:${id}] start ${label}`);

  try {
    const result = await operation();
    console.log(`[db:${id}] done ${label}`);
    return result;
  } catch (error) {
    console.error(`[db:${id}] failed ${label}`, error);
    throw error;
  }
}

将需要检查的 Repository 调用传给这个函数即可。它只能用来观察执行顺序,不能消除锁竞争,也不能恢复重载前已经丢失的 JavaScript 日志状态。

同时,在构造函数调用 SQLite.openDatabaseSync(DB_NAME) 前后记录日志,检查以下现象:

  • 同一次运行期间,连接是否被创建了多次。
  • 重载后最先执行的是查询、建表、迁移,还是业务写入。
  • 刷新前是否存在只有开始日志、没有结束日志的操作。
  • 初始化任务是否由多个组件或 Repository 重复触发。

连接创建次数只能帮助缩小范围,不能直接说明哪个连接持有锁。

优先检查事务与后台任务

如果项目存在显式事务,检查每条执行路径是否都会提交或回滚,尤其是抛出异常、组件卸载和取消操作时的路径。异步数据库调用应按业务顺序正确 await,避免初始化尚未结束,Repository 就开始访问数据库。

事务内尽量只执行必要的数据库操作。网络请求、定时等待或用户交互会延长事务时间,使其他连接更容易遇到锁冲突。

如果使用了手动创建的 prepared statement,也要按所用版本的 API 正确结束执行并释放资源。仅从 NativeStatement.runSync 这个错误位置,无法判断是否遗漏了语句清理。

订阅、定时器和后台写入也需要检查。Fast Refresh 后如果重复注册任务,同一份业务逻辑可能同时发起多次写入。组件清理时应停止后续任务,并按应用自身的任务管理方式处理已开始的操作。

用最小复现区分问题来源

可以按下面的顺序逐步验证,每次只增加一个因素:

  1. 仅使用 Expo SQLite 打开同一个数据库并执行简单查询,然后触发刷新。
  2. 加入简单写入,观察是否只有写入期间刷新才会失败。
  3. 加入 Drizzle,检查行为是否发生变化。
  4. 逐步恢复迁移、Repository、订阅及后台任务。

如果项目在启动时执行建表或迁移,应保证同一次运行期间只有一个初始化任务,让业务访问等待该任务完成。这样可以减少初始化并发,但不能处理重载前遗留的原生资源问题。

如果不使用 Drizzle、没有业务并发的最小示例仍能稳定复现,再结合 Expo SDK、expo-sqlite、Drizzle 的版本,以及平台和刷新类型,进一步检查对应版本的实现或已知问题。当前材料没有提供这些信息,不能给出确定的版本修复结论。

是否应该主动关闭连接?

长期保留一个受控连接是合理做法。需要明确释放资源时,应先停止新的数据库请求,等待正在执行的操作结束,再关闭底层连接,并避免 Repository 继续使用旧的 Drizzle 实例。

不要把普通组件卸载直接等同于整个数据库生命周期结束。多个组件共享同一连接时,如果其中一个组件卸载就关闭连接,可能影响其他调用。也不能依赖 JavaScript 的清理逻辑在所有完整重载场景中执行。

busy_timeout 可以缓解短暂的锁竞争,WAL 可以改善部分读写并发场景,但二者都不能修复未结束的事务;WAL 也不会允许多个写事务同时执行。因此,应先确认锁竞争的来源,再判断这些配置是否适用。

OP SQLite 没有出现同样的错误,可以作为对照。不过,不同实现的调度和资源清理行为可能不同,这一现象还不足以证明 Expo SQLite 存在特定缺陷,或断定锁发生在 Expo 的内部对象上。

备注:内容仅供参考。