SQLite3::BusyException

38

我目前正在使用SQLite3运行一个Rails网站。

大约每500个请求左右,我会收到一个

ActiveRecord::StatementInvalid(SQLite3::BusyException:数据库被锁定...

有什么方法可以修复这个问题,对我的代码干预最小?

目前我正在使用SQLLite,因为可以将数据库存储在源代码控制中,这使得备份变得自然,并且可以快速推送更改。然而,显然它并没有真正设置为并发访问。明天早上我会迁移到MySQL。


我打赌你的生产环境主机使用NFS作为应用程序用户的主目录,对吧? - ybakos
16个回答

58

您提到这是一个Rails网站。Rails允许您在database.yml配置文件中设置SQLite重试超时时间:

production:
  adapter: sqlite3
  database: db/mysite_prod.sqlite3
  timeout: 10000

超时值以毫秒为单位指定。将其增加到10或15秒应该会减少日志中看到的BusyExceptions数量。

不过,这只是一个临时解决方案。如果您的网站需要真正的并发性,则必须迁移到另一个数据库引擎。


2
这将为您在数据库连接上调用sqlite3_busy_timeout。 - docwhat
1
确保在进行此更改后重新启动您的Rails应用程序。 在我这样做之前没有起作用。 :) - Kyle Carlson
它在某些情况下似乎是有效的,如果数据库很忙并且我打开另一个控制台尝试写入,它将会挂起一段时间。但是,如果我打开两个控制台并让它们都在循环中写入,其中一个将立即死亡而不尊重超时。有什么想法吗? - Roman Gaufman

9

默认情况下,如果数据库被锁定,sqlite会立即返回一个被阻止的繁忙错误。您可以要求它在放弃之前等待并尝试一段时间。这通常可以解决问题,除非您有成千上万个线程访问数据库,那么我同意sqlite可能不合适。

    // 将SQLite设置为等待并重试,最多100毫秒,如果数据库被锁定
    sqlite3_busy_timeout( db, 100 );

2
你应该把sqlite3_busy_timeout放在哪里? - Sam
放置位置并不重要。在打开数据库之后,执行被阻塞的请求之前的任何位置都可以。为了方便起见,我将其放置在打开数据库之后的第一时间。 - ravenspoint
13
请按照Rifkin Habsburg所提到的方式修改database.yml配置文件。 - docwhat

3

仅供记录。我们在一个使用Rails 2.3.8的应用程序中发现,Rails忽略了Rifkin Habsburg建议的“timeout”选项。

经过更多的调查,我们发现Rails dev中可能存在相关的错误:http://dev.rubyonrails.org/ticket/8811。在进一步的调查后,我们找到了解决方案(在Rails 2.3.8中测试通过):

编辑此ActiveRecord文件:activerecord-2.3.8/lib/active_record/connection_adapters/sqlite_adapter.rb

替换为以下内容:

  def begin_db_transaction #:nodoc:
    catch_schema_changes { @connection.transaction }
  end

使用

  def begin_db_transaction #:nodoc:
    catch_schema_changes { @connection.transaction(:immediate) }
  end

就这些了!我们没有注意到性能下降,现在应用程序支持更多的请求而不会出错(它会等待超时)。 Sqlite很好用!


感谢Ignacio。对于AR 3.0.9,只需注意方法略有不同,但仍然将transaction()更改为transaction(:immediate)。我想知道为什么这不是AR代码库中明确的说明? - ybakos

3

所有这些事情都是真的,但它没有回答问题,也就是:为什么我的Rails应用程序在生产中偶尔会引发一个SQLite3::BusyException异常?

@Shalmanese: 生产托管环境是怎样的?它是在共享主机上吗?包含sqlite数据库的目录是否在NFS共享上?(很可能在共享主机上)。

这个问题很可能与使用NFS共享和SQLite缺乏并发性的文件锁定现象有关。


3
如果你遇到了这个问题,但是“增加超时时间”并没有改变任何东西,那么你可能还有另一个事务并发问题,总结如下:
  1. 开始一个事务(获取一个SHARED锁)
  2. 从数据库中读取一些数据(我们仍在使用SHARED锁)
  3. 同时,另一个进程启动一个事务并写入数据(获取RESERVED锁)。
  4. 然后你尝试写入,现在你正在尝试请求RESERVED锁
  5. SQLite立即引发SQLITE_BUSY异常(独立于超时时间),因为在它获得RESERVED锁之前,你之前的读取可能已不再准确。
修复此问题的一种方法是将active_record sqlite适配器修补程序打补丁,以通过将:immediate选项填充到驱动程序中直接获取RESERVED锁。 这会稍微降低性能,但至少所有事务都将遵守您的超时时间并依次发生。以下是使用prepend(Ruby 2.0+)的方法,请将其放入初始化程序中:
module SqliteTransactionFix
  def begin_db_transaction
    log('begin immediate transaction', nil) { @connection.transaction(:immediate) }
  end
end

module ActiveRecord
  module ConnectionAdapters
    class SQLiteAdapter < AbstractAdapter
      prepend SqliteTransactionFix
    end
  end
end

点击此处了解更多信息。该链接涉及IT技术,讨论了在某些情况下设置sqlite3_busy_timeout后仍会立即引发sqlite3busyexceptions的问题。


这在2021年仍然是一个问题 :( - 请问你有一个适用于Rails6的最新示例吗? - Hackeron
@Hackeron,很遗憾我没有用过sqlite,自那以后也没怎么用过。 - Adrien Rey-Jarthon
我们放弃了尝试让它工作并转向使用MariaDB。 - Hackeron

2
bundle exec rake db:reset

它对我有用,它将重置并显示待迁移状态。

1
大多数答案都是针对Rails而不是纯Ruby的,而OP的问题是关于Rails的,这很好。 :)
所以我只想在这里留下这个解决方案,以防任何使用原始Ruby的用户遇到此问题,并且没有使用yml配置。
在实例化连接后,您可以像这样设置它:
db = SQLite3::Database.new "#{path_to_your_db}/your_file.db"
db.busy_timeout=(15000) # in ms, meaning it will retry for 15 seconds before it raises an exception.
#This can be any number you want. Default value is 0.

1

我在使用rake db:migrate时遇到了类似的问题。问题出在我的工作目录在SMB共享上。 我通过将文件夹复制到本地机器上来解决了这个问题。


1

Sqlite可以让其他进程等待当前进程完成。

当我知道可能有多个进程尝试访问Sqlite数据库时,我使用这行代码进行连接:

conn = sqlite3.connect('filename', isolation_level = 'exclusive')

根据Python Sqlite文档的说明:

您可以通过connect()调用的isolation_level参数或连接的isolation_level属性来控制pysqlite隐式执行哪种类型的BEGIN语句(或根本不执行)。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接