SQLite并发访问

aamkag61  于 2023-02-19  发布在  SQLite
关注(0)|答案(8)|浏览(376)

SQLite3是否安全地处理多个进程从同一个DB阅读/写的并发访问?是否有任何平台例外?

3npbholx

3npbholx1#

如果大多数并发访问是读操作(例如SELECT),SQLite可以很好地处理它们。但是如果你开始并发写操作,锁争用可能会成为一个问题。这在很大程度上取决于你的文件系统有多快,因为SQLite引擎本身非常快,并且有许多聪明的优化来最小化争用。Especially SQLite 3.
对于大多数台式机/笔记本电脑/平板电脑/手机应用程序,SQLite足够快,因为没有足够的并发性(Firefox广泛使用SQLite作为书签、历史记录等)。
对于服务器应用程序,有人说在典型的场景中(例如博客、论坛),SQLite数据库可以完美地处理每天少于10万的页面浏览量,我还没有看到任何相反的证据。事实上,在现代磁盘和处理器的支持下,95%的网站和Web服务都可以很好地与SQLite一起工作。
如果你想要真正快速的读/写访问,使用in-memory SQLite database。RAM比磁盘快几个数量级。

8ulbf1ek

8ulbf1ek2#

是的。让我们找出原因

SQLite为transactional

SQLite中单个事务内的所有更改要么完全发生,要么根本不发生
这种ACID支持以及并发读/写以两种方式提供-使用所谓的日志记录(我们称之为“* 旧方式 ”)或提前写入日志记录(我们称之为“ 新方式 *”)

日志记录(旧方法)

在这种模式下,SQLite使用 * ase-LEVEL * locking,这是理解的关键点。
这意味着每当它需要读/写某个东西时,它首先获得一个对 * 整个 * 数据库文件的锁。多个读取器可以共存并并行读取某个东西。
在写入过程中,它确保获得一个排他锁,并且 * 没有其他 * 进程同时进行阅读/写,因此写入是安全的。
(This称为multiple-readers-single-writer or MSRW lock
这就是为什么他们说SQlite实现serializable事务

麻烦

由于每次都需要锁定整个数据库,并且每个人都在等待处理写入的进程,因此并发性受到影响,并且这种并发写入/读取的性能相当低

回滚/停机

在写入数据库文件之前,SQLite会先把要更改的数据块保存在一个临时文件中,如果在写入数据库文件的过程中发生崩溃,它会拾取这个临时文件并恢复其中的更改

预写日志记录或WAL(新方式)

在这种情况下,所有的写入都被附加到一个临时文件(write-ahead log)中,该文件定期与原始数据库合并。当SQLite搜索某个内容时,它会首先检查该临时文件,如果没有找到任何内容,则继续使用主数据库文件。
因此,读者不与作家竞争,性能比旧的方式好得多。

警告

SQlite严重依赖于底层文件系统锁定功能,因此应谨慎使用,此处提供了更多详细信息
您还可能遇到数据库已锁定错误,尤其是在日志模式下,因此在设计应用程序时需要考虑此错误

0x6upsns

0x6upsns3#

是的,SQLite处理并发性很好,但是从性能的Angular 来看,它不是最好的。据我所知,没有例外。详细信息在SQLite的网站上:https://www.sqlite.org/lockingv3.html
这句话很有趣:分页器模块可确保所有更改同时发生、所有更改都发生或都不发生、两个或多个进程不会尝试以不兼容的方式同时访问数据库

ezykj2lf

ezykj2lf4#

似乎没有人提到WAL(Write Ahead Log)模式。确保事务被正确组织,并且在WAL模式下,当人们在更新的同时阅读东西时,没有必要保持数据库锁定。
唯一的问题是在某个时候WAL需要被重新合并到主数据库中,并且它会在最后一个连接关闭时执行此操作。对于一个非常忙碌的站点,您可能会发现关闭所有连接需要几秒钟的时间,但是每天100K的点击应该不是问题。

xkrw2x1b

xkrw2x1b5#

2019年,有两个新的并发写入选项尚未发布,但在不同的分支机构提供。
"PRAGMA journal_mode = wal2"
与常规的“wal”模式相比,这种日志模式的优点在于,写入程序可以在一个wal文件执行检查点操作时继续写入另一个wal文件。
BEGIN CONCURRENT-链接到详细文档
如果数据库处于“wal”或“wal 2”模式,则开始CONCURRENT增强功能允许多个写入程序同时处理写入事务,尽管系统仍然序列化COMMIT命令。
使用“开始CONCURRENT”打开写事务处理时,实际锁定数据库的操作将推迟到执行COMMIT之后。这意味着使用BEGIN CONCURRENT启动的任意数量的事务处理都可以并发执行。系统使用乐观页级锁定来防止提交冲突的并发事务处理。
它们一起存在于begin-concurrent-wal2中,或者各自存在于单独的branch中。

bjp0bcyl

bjp0bcyl6#

SQLite在数据库级别有一个readers-writer lock,多个连接(可能由不同的进程拥有)可以同时从同一个数据库读取数据,但只有一个可以写入数据库。
SQLite支持无限数量的同时读取器,但它在任何时刻只会允许一个写入器。对于许多情况来说,这不是问题。写入器排队。每个应用程序快速地完成其数据库工作并继续前进,没有锁持续超过几十毫秒。但有一些应用程序需要更多的并发性,并且那些应用可能需要寻求不同的解决方案。
读取器-写入器锁支持独立的事务处理,并且在数据库级别使用排他锁和共享锁实现。
在连接对数据库执行写操作之前,必须获取排它锁。获取排它锁之后,来自其他连接的读写操作都将被阻止,直到再次释放锁为止。

并发写入情况下的实施详细信息

SQLite有一个锁表,它有助于在写操作期间尽可能晚地锁定数据库,以确保最大的并发性。
初始状态为UNLOCKED,在此状态下,连接尚未访问数据库。当进程连接到数据库时,即使已使用开始启动事务处理,连接仍处于UNLOCKED状态。
在UNLOCKED状态之后,下一个状态是SHARED状态。(不是写入)数据,连接必须首先通过获取SHARED锁进入SHARED状态。多个连接可以同时获取和维护SHARED锁,因此多个连接可以同时从同一数据库读取数据。但是,只要只有一个SHARED锁未释放,没有连接可以成功地完成对数据库的写入。
如果连接要写入数据库,它必须首先获得RESERVED锁。
一次只能有一个RESERVED锁处于活动状态,但多个SHARED锁可以与一个RESERVED锁共存。RESERVED与PENDING的不同之处在于,当存在RESERVED锁时,可以获取新的SHARED锁。--File Locking And Concurrency In SQLite Version 3 @ SQLite.org
一旦连接获得RESERVED锁,它就可以开始处理数据库修改操作,尽管这些修改只能在缓冲区中完成,而不能实际写入磁盘。对读出内容所做的修改保存在内存缓冲区中。当连接要提交修改时(或事务),则必须将RESERVED锁升级为EXCLUSIVE锁。为了获得该锁,必须首先将该锁解除为PENDING锁。
PENDING锁意味着持有锁的进程希望尽快写入数据库,并且只是等待所有当前SHARED锁清除,以便它可以获得EXCLUSIVE锁。如果PENDING锁处于活动状态,则不允许对数据库使用新的SHARED锁,但允许继续使用现有SHARED锁。
要写入数据库文件,需要EXCLUSIVE锁。文件上只允许有一个EXCLUSIVE锁,任何类型的其他锁都不允许与EXCLUSIVE锁共存。为了最大限度地提高并发性,SQLite会尽量减少持有EXCLUSIVE锁的时间。--File Locking And Concurrency In SQLite Version 3 @ SQLite.org
所以你可能会说SQLite安全地处理了多个进程对同一个DB的并发访问,仅仅是因为它不支持它!当第二个写入器达到重试限制时,你会得到SQLITE_BUSYSQLITE_LOCKED

drkbr07n

drkbr07n7#

这个线程是旧的,但我认为这将是很好的分享我的测试结果做了sqlite:我运行了2个python程序示例(不同进程相同程序),在事务中执行语句SELECT和UPDATE SQL命令,并将EXCLUSIVE锁和超时设置为10秒以获取锁,结果令人沮丧。每个示例在10000步循环中执行:

  • 使用排他锁连接到数据库
  • 选择一行以读取计数器
  • 用等于计数器加1的新值更新该行
  • 关闭与数据库的连接

即使sqlite在事务上授予了排他锁,实际执行的循环总数不等于20 000,但更少(两个进程在单个计数器上的迭代总数)。Python程序几乎没有抛出任何异常(在选择20次执行期间仅一次)测试时的. sqlite修订版本为3.6.20,python v3.3 CentOS 6.5。在我看来,最好为这种工作找到更可靠的产品,或者将对sqlite的写入限制为单个唯一的进程/线程。

lawou6xi

lawou6xi8#

当你指定db的名字时,甚至在内存db中,如果你有并发访问(特别是写),你会得到这个名字,这是很自然的。
在我的例子中,我使用Sqlite进行测试,这是因为在同一个解决方案中有几个测试。
您可以进行两项改进:
1.创建db.Database.EnsureDeletedAsync();前删除
1.使用一个空字符串进行连接,在这种情况下,它会在每次调用时创建一个随机名称:

{
  "ConnectionStrings": {
    "ConnectionType": "sqlite",
    "ConnectionString": ""
  }
}

相关问题