根据documentation关于延期交易:
The default transaction behavior is deferred. (...) The first read operation against a database creates a SHARED lock and the first write operation creates a RESERVED lock.
同时根据documentation关于锁:
Any number of processes can hold SHARED locks at the same time (...) Only a single RESERVED lock may be active at one time, though multiple SHARED locks can coexist with a single RESERVED lock
这听起来像是具有任意读取器到写入器升级机制的多个读取器/单个写入器锁,已知这会导致死锁危险:
- A 开始交易
- B 开始交易
- A 获取 SHARED 锁并读取内容
- B 获取共享锁并读取内容
- A 获取 RESERVED 锁并准备写入内容。只要有其他共享锁,它就无法写入,因此会阻塞。
- B 希望写入,因此尝试获取 RESERVED 锁。已经有另一个 RESERVED 锁,因此它会阻塞,直到它被释放,但仍持有 SHARED 锁。
- 陷入僵局。
那么 SQLite 如何解决这个问题呢?我想到了两种可能的解决方案,但它们似乎都打破了交易的整个理念:
- 潜在的写入者在获取 RESERVED 之前释放 SHARED 锁。这会破坏读取和写入之间的原子性。
- B 在尝试获取 RESERVED 锁时不会阻塞,但会出错。这意味着所有读取都需要重复,并使 API 使用变得非常复杂。
我错过了什么吗? SQLite 如何处理这个问题?为什么这种看似危险的交易类型会成为默认交易类型?
最佳答案
通过简单的尝试和错误,我发现他们走了错误路线。
在给定的场景中,当 B 尝试获取 RESERVED 时,它将首先等待 PRAGMA busy_timeout
毫秒。然后会报错误:数据库已锁定
。该事务仍将处于事件状态,因此可以立即重试。
如果A随后尝试COMMIT
(或者如果它耗尽了内存缓存),它将获取PENDING锁(防止额外的SHARED锁),然后等待EXCLUSIVE。如果在PRAGMA busy_timeout
毫秒后仍有一些共享锁存在,则会报告错误:数据库已锁定。该事务仍将处于事件状态,因此可以立即重试。
也就是说,使用的死锁预防机制是超时。不过,它确实需要 API 用户配合,回滚并重试。
作为指导:
- 当您只想阅读时,只需使用
BEGIN TRANSACTION
(或显式使用BEGIN DEFERRED TRANSACTION
)。写入可能会失败,迫使您回滚并再次重试整个事务。 - 当您希望在某个时刻进行写入时,请使用
BEGIN IMMEDIATE TRANSACTION
。这将阻止所有其他作家和所有其他直接的潜在作家。 BEGIN EXCLUSIVE TRANSACTION
将立即阻塞,直到释放所有其他锁。我不知道为什么有人会想要这个。可能是为一些数据到达后需要尽快写入磁盘做好准备? 编辑:这似乎是防止开始事务后任意点超时的唯一方法。
关于sqlite - SQLite 如何通过延迟事务防止死锁?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55831645/