連線池
當我們建立資料庫連線時,通常這代表著一條 TCP 連線或是 Socket 連線的建立。Socket 會一次處理一個資料庫語法。如果我們的程式需要同時執行很多的查詢,或者它需要處理並行化的資料庫請求,在這種情況下,我們會需要使用一條以上的資料庫連線。
資料庫從應用程式中分離後,我們面臨許多的問題,例如:連線可能斷開、應用程式重啟,以及許多我們不希望是我們的程式要處理的問題。
通常來說,連線池能夠乾淨俐落的幫我們解決上述的問題。
當我們使用 crystal-db 開啟資料庫時,其實已經有一個可用的連線池了。DB.open 回傳一個 DB::Database 物件,DB::Database 會管理整個連線池而非僅僅一條連線。
DB.open("mysql://root@localhost/test") do |db|
# db 為 DB::Database 物件
end
當我們執行一些資料庫語法如 db.query、 db.exec、db.scalar 的時候,會執行下面的步驟:
- 從連線池中找到一條可用的連線
- 當有需要的時候,建立一條連線
- 如果連線池不允許建立新的連線,會等待一段時間直到有連線是可用的
- 若等待的時間太長,則會自動退出
- 切換到該條可用的連線上
- 執行 SQL 語法
- 如果沒有返回任何的
DB::ResultSet將連線還回連線池。否則,在 ResultSet 結束前連線不會返回連線池。 - 回傳資料庫語法結果
如果無法建立連線,或是在過程中連線段開了,crystal-db 會重複上述的過程。
只有當我們使用
DB::Database來執行資料庫語句的時候才會重新執行。如果我們是藉由DB::Connection或是DB::Transaction來執行資料庫語句則不會重新執行,因為程式碼會預期 connection 物件應該會被使用。
設定
我們可以藉由連線 URI 中的參數來設定連線池的行為。
| 名稱 | 預設值 |
|---|---|
| initial_pool_size | 1 |
| max_pool_size | 0 (unlimited) |
| max_idle_pool_size | 1 |
| checkout_timeout | 5.0 (seconds) |
| retry_attempts | 1 |
| retry_delay | 1.0 (seconds) |
當 DB::Database 被建立時,會以 initial_pool_size 決定初始的連線數建立連線。而連線池連線數的上限不會超過 max_pool_size 的值。當連線池中的閒置連線達到 max_idle_pool_size 設置的數量時,返回至連線池的連線會被關閉。
當連線數已經達到最大,而有需要新的連線的時候,會以 checkout_timeout 設置的秒數作為等待可用連線的時間。
當連線斷開,或是無法建立時,會以 retry_attempts 做為最多重新嘗試的次數,而我們可以使用 retry_delay 來設置每次重新嘗試連線的間隔時間。
範例
下面的範例程式會印出從 MySQL 拿到的時間,如果連線斷開或是 MySQL 伺服器暫時不能使用,程式仍會繼續執行,不發生例外。
# file: 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
# 停止 MySQL 伺服器幾秒鐘
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