跳至内容

连接池

建立连接通常意味着打开一个 TCP 连接或套接字。套接字一次处理一条语句。如果一个程序需要同时执行许多查询,或者如果它处理并发请求以使用数据库,则它将需要多个活动连接。

由于数据库是使用它们的应用程序的独立服务,因此连接可能会断开,服务可能会重新启动,以及其他程序可能不想关心的问题。

为了解决这些问题,连接池通常是一个不错的解决方案。

当使用 crystal-db 打开数据库时,已经有一个连接池在工作。DB.open 返回一个 DB::Database 对象,它管理整个连接池,而不仅仅是一个单一的连接。

DB.open("mysql://root@localhost/test") do |db|
  # db is a DB::Database
end

当使用 db.querydb.execdb.scalar 等执行语句时,算法如下:

  1. 在池中查找可用的连接。
    1. 如果需要,创建连接。
    2. 如果池不允许创建新的连接,则等待连接可用。
      1. 但是如果等待时间过长,则应中止等待。
  2. 从池中检出该连接。
  3. 执行 SQL 命令。
  4. 如果没有生成 DB::ResultSet,则将连接返回到池中。否则,当 ResultSet 关闭时,连接将被返回到池中。
  5. 返回语句结果。

如果无法创建连接,或者在执行语句时连接丢失,则重复上述过程。

重试逻辑仅在通过 DB::Database 发送语句时才会发生。如果通过 DB::ConnectionDB::Transaction 发送语句,则不会执行重试,因为代码将说明期望使用特定连接对象。

配置

可以通过一组参数配置池的行为,这些参数可以作为连接 URI 中的查询字符串出现。

名称 默认值
initial_pool_size 1
max_pool_size 0(无限制)
max_idle_pool_size 1
checkout_timeout 5.0(秒)
retry_attempts 1
retry_delay 1.0(秒)

当打开 DB::Database 时,将创建 initial_pool_size 个连接。池永远不会持有超过 max_pool_size 个连接。当将连接返回/释放到池中时,如果池中已有 max_idle_pool_size 个空闲连接,则该连接将被关闭。

如果达到了 max_pool_size 并且需要一个连接,则最多等待 checkout_timeout 秒,以使现有的连接可用。

如果连接丢失或无法建立,则最多重试 retry_attempts 次,每次尝试之间等待 retry_delay 秒。

示例

以下程序将从 MySQL 中打印当前时间,但是如果连接丢失或整个服务器停机几秒钟,程序仍然可以运行,不会引发异常。

sample.cr
require "mysql"

DB.open "mysql://root@localhost?retry_attempts=8&retry_delay=3" do |db|
  loop do
    pp db.scalar("SELECT NOW()")
    sleep 0.5
  end
end
$ crystal sample.cr
db.scalar("SELECT NOW()") # => 2016-12-16 16:36:57
db.scalar("SELECT NOW()") # => 2016-12-16 16:36:57
db.scalar("SELECT NOW()") # => 2016-12-16 16:36:58
db.scalar("SELECT NOW()") # => 2016-12-16 16:36:58
db.scalar("SELECT NOW()") # => 2016-12-16 16:36:59
db.scalar("SELECT NOW()") # => 2016-12-16 16:36:59
# stop mysql server for some seconds
db.scalar("SELECT NOW()") # => 2016-12-16 16:37:06
db.scalar("SELECT NOW()") # => 2016-12-16 16:37:06
db.scalar("SELECT NOW()") # => 2016-12-16 16:37:07