最近總會(huì)遇到 MySQL server has gone away 的報(bào)錯(cuò),然后就看了一下django數(shù)據(jù)庫(kù)連接這一塊。
django數(shù)據(jù)庫(kù)連接
ORM中數(shù)據(jù)庫(kù)連接用到的 connections ,從 django.db 模塊引入,屬于 ConnectionHandler 對(duì)象。
# django.db.__init__.py
# django ORM中用到的數(shù)據(jù)庫(kù)連接來(lái)源
connections = ConnectionHandler()
# 請(qǐng)求開(kāi)始之前重置所有連接def reset_queries(**kwargs):
for conn in connections.all():
conn.queries_log.clear()
signals.request_started.connect(reset_queries)
# 請(qǐng)求開(kāi)始結(jié)束之前遍歷所有已存在連接,關(guān)閉不可用的連接def close_old_connections(**kwargs):
for conn in connections.all():
conn.close_if_unusable_or_obsolete()
signals.request_started.connect(close_old_connections)
signals.request_finished.connect(close_old_connections)
我理解的 ConnectionHandler 類是一個(gè)數(shù)據(jù)庫(kù)連接管理器,負(fù)責(zé)根據(jù)不同數(shù)據(jù)庫(kù)后端創(chuàng)建數(shù)據(jù)庫(kù)連接,保存連接,給應(yīng)用方提供連接,以及關(guān)閉所有連接。 這里通過(guò)django信號(hào)的方式,在請(qǐng)求開(kāi)始之前以及請(qǐng)求結(jié)束之后關(guān)閉失效數(shù)據(jù)庫(kù)連接。
# django.db.utils.py
class ConnectionHandler(object):
def __init__(self, databases=None):
# 獲取數(shù)據(jù)庫(kù)配置
self._databases = databases
# 從當(dāng)前線程變量獲取所有數(shù)據(jù)庫(kù)連接
self._connections = local()
# 獲取數(shù)據(jù)庫(kù)連接關(guān)鍵邏輯
def __getitem__(self, alias):
# 首先直接從當(dāng)前線程變量獲取
if hasattr(self._connections, alias):
return getattr(self._connections, alias)
# 重新建立數(shù)據(jù)庫(kù)連接并寫(xiě)入當(dāng)前線程變量
self.ensure_defaults(alias)
self.prepare_test_settings(alias)
db = self.databases[alias]
backend = load_backend(db['ENGINE'])
# django.db.backends.mysql.base.DatabaseWrapper
conn = backend.DatabaseWrapper(db, alias)
setattr(self._connections, alias, conn)
return conn
ConnectionHandler 中 _connections 表示當(dāng)前數(shù)據(jù)庫(kù)連接集合,是一個(gè) ThreadLocal 對(duì)象,是和線程綁定在一起的。在整個(gè)線程生命周期內(nèi), _connections 屬于全局變量,但是當(dāng)線程一旦關(guān)閉, _connections 也消失了。
關(guān)鍵邏輯在于 __getitem__ 方法,當(dāng)通過(guò)別名獲取數(shù)據(jù)庫(kù)連接時(shí),首先從當(dāng)前線程變量中獲取連接,獲取不到就根據(jù)別名創(chuàng)建新的數(shù)據(jù)庫(kù)連接,并將連接寫(xiě)入 ThreadLocal 。
通過(guò)CONN_MAX_AGE設(shè)置連接存活時(shí)間
django 1.6 開(kāi)始支持持久數(shù)據(jù)庫(kù)連接,通過(guò)參數(shù) CONN_MAX_AGE 設(shè)置每個(gè)連接的最大存活時(shí)間。默認(rèn)值是0,設(shè)置為None表示無(wú)限制的持久連接。
# django.db.backends.base.base.py
class BaseDatabaseWrapper(object):
def connect(self):
self.in_atomic_block = False
self.savepoint_ids = []
self.needs_rollback = False
# 根據(jù)CONN_MAX_AGE參數(shù)設(shè)置連接的關(guān)閉時(shí)間
max_age = self.settings_dict['CONN_MAX_AGE']
self.close_at = None if max_age is None else time.time() + max_age
... ...
def close_if_unusable_or_obsolete(self):
if self.connection is not None:
if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']:
self.close()
return
# 發(fā)生異常,檢查連接是否可用,不可用關(guān)閉連接
if self.errors_occurred:
if self.is_usable():
self.errors_occurred = False
else:
self.close()
return
# 設(shè)置了超時(shí)時(shí)間,并且連接超時(shí),關(guān)閉連接
if self.close_at is not None and time.time() >= self.close_at:
self.close()
return
數(shù)據(jù)庫(kù)連接在建立的時(shí)候會(huì)根據(jù) CONN_MAX_AGE 參數(shù)設(shè)置連接的 close_at 屬性,表示連接失效時(shí)間。再看上面:point_up_2: django.db.__init__.py 的代碼,通過(guò)信號(hào)方式,每次請(qǐng)求開(kāi)始以及結(jié)束的時(shí)候,會(huì)調(diào)用 close_if_unusable_or_obsolete 方法,判斷當(dāng)連接超時(shí)或者處在不可恢復(fù)狀態(tài)時(shí)則關(guān)閉連接。
總結(jié)
1. django的數(shù)據(jù)庫(kù)連接是保存到線程變量的數(shù)據(jù)庫(kù)連接是全局的,但只存在于當(dāng)前線程中,如果線程關(guān)閉,數(shù)據(jù)庫(kù)連接也不存在了。
2. 可以通過(guò)CONN_MAX_AGE參數(shù)配置數(shù)據(jù)庫(kù)連接的存活時(shí)間即使設(shè)置了CONN_MAX_AGE參數(shù),也是在線程依然存活的情況下,數(shù)據(jù)庫(kù)連接能夠存活的時(shí)間。
需要注意的兩點(diǎn)是:
·CONN_MAX_AGE 應(yīng)該小于數(shù)據(jù)庫(kù)本身的最大連接時(shí)間 wait_timeout ,否則應(yīng)用程序可能會(huì)獲取到連接超時(shí)的數(shù)據(jù)庫(kù)連接,這時(shí)會(huì)出現(xiàn) MySQL server has gone away 的報(bào)錯(cuò)。
·如果部署方式采用多線程,最大線程數(shù)不能大于最大數(shù)據(jù)庫(kù)連接數(shù)。另外,開(kāi)發(fā)模式下(runserver),由于每條請(qǐng)求都是創(chuàng)建一個(gè)新的 Thread ,就不要使用 CONN_MAX_AGE 參數(shù)了,這樣在老的請(qǐng)求線程中保存的數(shù)據(jù)庫(kù)連接根本不能復(fù)用。
來(lái)源:rainybowe