【问题标题】:How do I prevent SQLite database locks?如何防止 SQLite 数据库锁定?
【发布时间】:2013-06-27 16:29:01
【问题描述】:

从 sqlite FAQ 我知道:

多个进程可以同时打开同一个数据库。 多个进程可以同时执行 SELECT。但只有 一个进程可以随时更改数据库 然而,时间。

所以,据我所知,我可以: 1)从多个线程读取数据库(SELECT) 2)从多线程读取数据库(SELECT)并从单线程写入(CREATE,INSERT,DELETE)

但是,我读到了 Write-Ahead Logging,它提供了更多的并发性,因为 读取器不会阻止写入器,而写入器不会阻止读取器。读写可以同时进行。

最后,当我找到it 时,我完全糊涂了:

以下是导致 SQLITE_LOCKED 错误的其他原因:

  • 在执行 SELECT 语句时尝试创建或删除表或索引 仍悬而未决。
  • 当 SELECT 在同一个表上处于活动状态时尝试写入该表。
  • 试图在同一个表中同时执行两个 SELECT 多线程应用程序,如果没有设置 sqlite 这样做。
  • fcntl(3,F_SETLK 调用 DB 文件失败。这可能是由 NFS 锁定引起的 问题,例如。此问题的一种解决方案是将数据库移走, 并将其复制回来,使其具有新的 Inode 值

所以,我想为自己澄清一下,什么时候应该避免使用锁?我可以从两个不同的线程同时读写吗?谢谢。

【问题讨论】:

  • 我已经多次围绕这个话题。如果您使用事务,唯一安全的做法是在整个事务中序列化数据库。

标签: sqlite


【解决方案1】:

对于那些使用Android API的人:

SQLite 中的锁定是在文件级别完成的,这保证了锁定 来自不同线程和连接的变化。从而多 线程可以读取数据库,但只能写入。

更多关于锁定 SQLite 的信息可以在SQLite 文档中阅读,但我们最感兴趣的是 OS Android 提供的 API。

可以从单个和多个数据库连接中使用两个并发线程进行写入。由于只有一个线程可以写入数据库,因此有两种变体:

  1. 如果您从一个连接的两个线程写入,则一个线程将 等待另一个完成写作。
  2. 如果您从不同连接的两个线程写入,则会出现错误 will be – 您的所有数据都不会写入数据库,并且 应用程序将被中断 SQLiteDatabaseLockedException。很明显, 应用程序应始终只有一份 SQLiteOpenHelper(只是一个打开的连接)否则 SQLiteDatabaseLockedException 随时可能发生。

单个 SQLiteOpenHelper 的不同连接

每个人都知道 SQLiteOpenHelper 有 2 个方法提供对数据库 getReadableDatabase()getWritableDatabase() 的访问,分别读取和写入数据。然而,在大多数情况下,只有一个真正的联系。而且它是同一个对象:

SQLiteOpenHelper.getReadableDatabase()==SQLiteOpenHelper.getWritableDatabase()

这意味着读取数据的方法的使用没有区别。然而,还有另一个更重要的未记录问题——在 SQLiteDatabase 类内部有自己的锁——变量 mLock。在对象 SQLiteDatabase 级别进行写入锁定,并且由于只有一个 SQLiteDatabase 副本用于读取和写入,因此数据读取也被阻止。在事务中写入大量数据时,它会更加突出。

让我们考虑这样一个应用程序的示例,它应该在首次启动时在后台下载大量数据(大约 7000 行包含 BLOB)并保存它到数据库。如果数据保存在事务中,则保存大约需要。 45 秒,但用户无法使用该应用程序,因为任何阅读查询都被阻止。如果数据以小部分保存,则更新过程会拖出相当长的时间(10-15 分钟),但用户可以使用该应用程序而没有任何限制和不便。 “双刃剑”——快速或方便。

Google 已经修复了与 SQLiteDatabase 功能相关的部分问题,添加了以下方法:

beginTransactionNonExclusive() – 在“即时模式”下创建交易。

yieldIfContendedSafely() – 临时占用事务以允许其他线程完成任务。

isDatabaseIntegrityOk() - 检查数据库完整性

更多详情请阅读documentation

但是,对于旧版本的 Android,此功能也是必需的。

解决方案

应关闭第一个锁定并允许在任何情况下读取数据。

SQLiteDatabase.setLockingEnabled(false);

使用内部查询锁定取消 - 在 java 类的逻辑级别(与 SQLite 中的锁定无关)

SQLiteDatabase.execSQL(“PRAGMA read_uncommitted = true;”);

允许从缓存中读取数据。实际上,更改了隔离级别。应为每个连接重新设置此参数。如果有多个连接,那么它只影响调用此命令的连接。

SQLiteDatabase.execSQL(“PRAGMA 同步=OFF”);

将写入方式更改为数据库——无需“同步”。激活此选项时,如果系统意外故障或电源关闭,数据库可能会损坏。但是根据 SQLite 文档,如果未激活该选项,某些操作的执行速度会快 50 倍。

不幸的是,Android 并不支持所有 PRAGMA,例如不支持“PRAGMAlocking_mode = NORMAL”和“PRAGMA journal_mode = OFF”和其他一些。在尝试调用 PRAGMA 数据时,应用程序失败。

在方法 setLockingEnabled 的文档中,据说只有在您确定所有与数据库的工作都是从单个线程完成的情况下,才建议使用此方法。我们应该保证一次只进行一笔交易。此外,应使用即时事务而不是默认事务(排他事务)。在旧版本的 Android(低于 API 11)中,没有通过 java 包装器创建即时事务的选项,但是 SQLite 支持此功能。要在立即模式下初始化事务,应直接对数据库执行以下 SQLite 查询,例如通过方法 execSQL:

SQLiteDatabase.execSQL(“开始立即事务”);

由于事务是由直接查询初始化的,所以它应该以同样的方式完成:

SQLiteDatabase.execSQL(“提交事务”);

那么 TransactionManager 是唯一需要实现的东西,它将启动和完成所需类型的事务。 TransactionManager 的目的是确保所有更改查询(插入、更新、删除、DDL 查询)都来自同一个线程。

希望这对未来的访客有所帮助!!!

【讨论】:

【解决方案2】:

不特定于 SQLite:

1) 编写代码以优雅地处理在应用程序级别遇到锁定冲突的情况;即使您编写了代码,这也是“不可能的”。使用事务重试(即:SQLITE_LOCKED 可能是您解释为“重试”或“等待并重试”的众多代码之一),并将其与应用程序级代码协调。如果您考虑一下,获得 SQLITE_LOCKED 比简单地因为它被锁定而挂起尝试要好 - 因为您可以去做其他事情。

2) 获取锁。但是,如果您需要获得多个,则必须小心。对于应用程序级别的每个事务,以一致的(即:字母顺序?)顺序获取您需要的所有资源(锁),以防止在数据库中获取锁时出现死锁。如果数据库能够可靠快速地检测到死锁并抛出异常,有时您可以忽略这一点;在其他系统中,它可能会在没有检测到死锁的情况下挂起——因此绝对有必要努力正确地获取锁。

除了与锁定有关的生活事实之外,您应该尝试从一开始就设计具有并发合并和回滚的数据和内存结构。如果您可以设计数据,使数据竞争的结果对所有订单都产生良好的结果,那么您就不必在这种情况下处理锁。一个很好的例子是在不知道其当前值的情况下增加一个计数器,而不是读取该值并提交一个新值进行更新。附加到一个集合也是类似的(即:添加一行,这样行插入发生的顺序无关紧要)。

一个好的系统应该以事务方式从一个有效状态转移到下一个状态,您可以将异常(即使在内存中的代码中)视为中止转移到下一个状态的尝试;可选择忽略或重试。

【讨论】:

    【解决方案3】:

    您可以使用多线程。您链接的页面列出了您在同一线程中循环查看 SELECT 的结果(即您的选择处于活动状态/待定)时不能执行的操作。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-14
      • 2015-10-11
      相关资源
      最近更新 更多