15

从 sqlite FAQ 我知道:

多个进程可以同时打开同一个数据库。多个进程可以同时执行 SELECT。但是,任何时候只有一个进程可以对数据库进行更改。

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

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

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

以下是获得 SQLITE_LOCKED 错误的其他原因:

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

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

4

3 回答 3

10

对于那些使用Android API的人:

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

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

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

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

单个 SQLiteOpenHelper 的不同连接

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

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

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

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

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

beginTransactionNonExclusive() – 在“即时模式”下创建一个事务。

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

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

请阅读文档中的更多详细信息。

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

解决方案

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

SQLiteDatabase.setLockingEnabled(false);

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

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

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

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

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

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

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

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

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

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

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

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

于 2016-08-29T20:27:36.723 回答
5

不特定于 SQLite:

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

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

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

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

于 2013-06-27T17:39:42.873 回答
1

你对多线程没问题。SELECT您链接的页面列出了您在同一线程中循环(即您的选择是活动/待定)的结果时不能做的事情。

于 2013-06-27T16:38:56.717 回答