2024-12-14 08:00:07

SQLite并发访问

SQLite3是否安全处理多个进程的并发访问 读/写从相同的DB?有什么平台例外吗?


是的,SQLite处理并发性很好,但从性能角度来看它并不是最好的。据我所知,没有例外。详情见SQLite的网站:https://www.sqlite.org/lockingv3.html

这句话很有趣:“寻呼机模块确保所有更改都同时发生,要么所有更改都发生,要么都不发生,两个或多个进程不会试图同时以不兼容的方式访问数据库。”


如果大多数并发访问是读(例如SELECT), SQLite可以很好地处理它们。但是如果开始并发地编写,锁争用就会成为一个问题。这在很大程度上取决于您的文件系统有多快,因为SQLite引擎本身非常快,并且有许多巧妙的优化来最小化争用。尤其是SQLite 3。

对于大多数台式机/笔记本电脑/平板电脑/手机应用程序,SQLite已经足够快了,因为没有足够的并发性。(Firefox在书签、历史记录等方面广泛使用SQLite)

对于服务器应用程序,前段时间有人说过,在典型的场景下(例如博客、论坛),SQLite数据库可以完美地处理每天少于10万的页面浏览量,我还没有看到任何相反的证据。事实上,使用现代磁盘和处理器,95%的网站和web服务都可以很好地使用SQLite。

如果你想要快速的读/写访问,请使用内存中的SQLite数据库。内存比磁盘快几个数量级。


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

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

Even if sqlite granted exclusive lock on transaction, the total number of really executed cycles were not equal to 20 000 but less (total number of iterations over single counter counted for both processes). Python program almost did not throw any single exception (only once during select for 20 executions). sqlite revision at moment of test was 3.6.20 and python v3.3 CentOS 6.5. In mine opinion it is better to find more reliable product for this kind of job or restrict writes to sqlite to single unique process/thread.


似乎没有人提到WAL(预写日志)模式。确保事务被正确组织,并设置了WAL模式,当人们在更新时读取数据时,没有必要保持数据库锁定。

唯一的问题是,在某些情况下,WAL需要重新合并到主数据库中,并且在到数据库的最后一个连接关闭时这样做。对于一个非常繁忙的网站,你可能会发现需要几秒钟才能关闭所有连接,但每天10万次点击应该不是问题。


是的。 让我们来看看为什么

SQLite是事务性的

SQLite中单个事务中的所有更改都可能发生 完全或根本没有

这种ACID支持以及并发读/写是通过两种方式提供的——使用所谓的日志记录(我们称之为“旧方式”)或预写日志记录(我们称之为“新方式”)

日志(旧方式)

在这种模式下,SQLite使用数据库级锁定。 这是需要理解的关键。

这意味着当它需要读/写一些东西时,它首先在整个数据库文件上获得一个锁。 多个阅读器可以共存并并行阅读

在写入过程中,它确保获得一个排他锁,并且没有其他进程同时读写,因此写入是安全的。

这就是为什么这里说SQlite实现了可序列化的事务

麻烦

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

回滚/中断

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

预写测井或WAL(新方法)

在这种情况下,所有写操作都被追加到一个临时文件(预写日志),并且该文件定期与原始数据库合并。 当SQLite搜索某些东西时,它会首先检查这个临时文件,如果没有发现,则继续检查主数据库文件。

因此,读者不会与作者竞争,与旧的方式相比,表现要好得多。

警告

SQlite在很大程度上依赖于底层的文件系统锁定功能,因此应该谨慎使用,更多细节请点击这里

你也可能会遇到数据库锁定错误,特别是在日志模式下,所以你的应用程序需要在设计时考虑到这个错误


SQLite在数据库级上有一个读写锁。多个连接(可能属于不同的进程)可以同时从同一个数据库读取数据,但只有一个连接可以写入数据库。

SQLite支持无限数量的同时读取器,但在任何时刻只允许一个写入器。在许多情况下,这不是问题。写入器排队。每个应用程序都能快速完成数据库工作,然后继续前进,任何锁的持续时间都不会超过几十毫秒。但是有一些应用程序需要更多的并发性,这些应用程序可能需要寻求不同的解决方案。-适当使用SQLite @ SQLite.org

读写锁支持独立的事务处理,并且在数据库级别上使用排他性和共享锁实现。

在连接对数据库执行写操作之前,必须获得排他锁。获得独占锁后,来自其他连接的读写操作都将被阻塞,直到再次释放锁。

并发写入情况的实现细节

SQLite有一个锁表,可以帮助在写操作期间尽可能晚地锁定数据库,以确保最大的并发性。

初始状态是UNLOCKED,在这种状态下,连接还没有访问数据库。当一个进程连接到数据库时,即使一个事务已经用BEGIN启动,连接仍然处于解锁状态。

在“UNLOCKED”状态之后,下一个状态是“SHARED”状态。为了能够从数据库读取(而不是写入)数据,连接必须首先通过获得SHARED锁进入SHARED状态。 多个连接可以同时获取和维护SHARED锁,因此多个连接可以同时从同一个数据库读取数据。但是,只要只有一个SHARED锁未释放,就没有连接可以成功地完成对数据库的写入。

如果连接想要写入数据库,它必须首先获得一个保留锁。

一次只能激活一个保留锁,但是多个SHARED锁可以与一个保留锁共存。RESERVED与PENDING的区别在于,当有保留锁时,可以获得新的SHARED锁。——SQLite版本3中的文件锁定和并发性

一旦连接获得保留锁,它就可以开始处理数据库修改操作,尽管这些修改只能在缓冲区中完成,而不能实际写入磁盘。对读出内容所做的修改保存在内存缓冲区中。 当连接想要提交修改(或事务)时,有必要将保留锁升级为EXCLUSIVE锁。为了获得锁,必须首先将锁提升到PENDING锁。

A PENDING lock means that the process holding the lock wants to write to the database as soon as possible and is just waiting on all current SHARED locks to clear so that it can get an EXCLUSIVE lock. No new SHARED locks are permitted against the database if a PENDING lock is active, though existing SHARED locks are allowed to continue. An EXCLUSIVE lock is needed in order to write to the database file. Only one EXCLUSIVE lock is allowed on the file and no other locks of any kind are allowed to coexist with an EXCLUSIVE lock. In order to maximize concurrency, SQLite works to minimize the amount of time that EXCLUSIVE locks are held. -- File Locking And Concurrency In SQLite Version 3 @ SQLite.org

所以你可能会说SQLite可以安全地处理多个进程写入同一个DB的并发访问,因为SQLite不支持!当第二个写入器达到重试限制时,您将获得SQLITE_BUSY或SQLITE_LOCKED。


2019年,有两个新的并发写选项尚未发布,但在不同的分支中可用。

"PRAGMA journal_mode = wal2"

与常规的“wal”模式相比,这种日志模式的优点是,写入者可以继续写入一个wal文件,而另一个则是检查点。

BEGIN CONCURRENT -链接到详细的文档

The BEGIN CONCURRENT enhancement allows multiple writers to process write transactions simultanously if the database is in "wal" or "wal2" mode, although the system still serializes COMMIT commands. When a write-transaction is opened with "BEGIN CONCURRENT", actually locking the database is deferred until a COMMIT is executed. This means that any number of transactions started with BEGIN CONCURRENT may proceed concurrently. The system uses optimistic page-level-locking to prevent conflicting concurrent transactions from being committed.

它们一起出现在begin-concurrent-wal2中,或者各自出现在一个独立的分支中。


当您为db指定名称时,如果您有并发访问(特别是写),甚至在内存db中指定名称时,很自然会得到这个结果。 在我的例子中,我使用Sqlite进行测试,这是因为在同一个解决方案中有几个测试。 你可以有两个改进:

在创建db.Database.EnsureDeletedAsync(); 使用空字符串进行连接,在这种情况下,每次调用都会创建一个随机名称:

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