a3.sqlalchemy-scoped_session

scoped_session防止內存泄漏,保護性session的創建。

SQLAlchemy 的 scoped_session 是啥玩意

通常我們用 SQLAlchemy 寫數據的時候要創建 Session 對象來維護數據庫會話,用完了再關掉。但是聽說還有個叫scoped_session的玩意,這是做啥用的?

這東西其實與 web 應用有一些關係。我們在使用 Django 的 ORM 的時候怎麼沒見到需要創建個 session 呢?因爲它已經悄悄幫你實現好了維護 session 的邏輯,自動進行創建和銷燬(多麼偉大,多麼 friendly 啊),而當我們用 Flask 之類裸奔極客模式的 web 框架時就沒有這樣的好事了,只能自己搞定。

Session 的生命週期

首先我們需要知道一個 sqlalchemy session 的生命週期是怎樣的。我們的 web 應用會同時服務多個用戶,因此不同的請求要有不同的 session,不然就會翻車。

session 會在一次請求進來的時候創建出來

session = Session()

在整個請求處理過程中被使用

session.add(some_obj)
session.commit()

在請求處理完畢後被關閉銷燬。

session.close()

當然上面的代碼只是簡略的情形,通常還需要包括 try-except 來處理 session 需要 rollback 之類的情況。

scoped_session 與 registry 模式

scoped_session就是用在 web 應用這種處理請求的場景下,協助進行 session 維護的。

from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker

session_factory = sessionmaker(bind=some_engine)
ScopedSession = scoped_session(session_factory)

通常我們在初始化 web 應用時通過scoped_session函數對原始的Session工廠進行處理,返回出一個ScopedSession工廠,在每個請求來的時候就可以通過這個工廠獲得一個 scoped_session 對象。

這實際上實現了一種 registry 模式,它有個中心化的 registry 來保存已經創建的 session,並在你調用ScopedSession工廠的時候,在 registry 裏面找找是不是之前已經爲你創建過 session 了,如果有,就直接把這個 session 返回給你,如果沒有,就創建一個新的 session,並註冊到 registry 中以便你下次來要的時候給你。

some_session = ScopedSession()
some_other_session = ScopedSession()
>>> some_session is some_other_session
True

>>> some_other_session.remove()

>>> new_session = ScopedSession()
>>> new_session is some_session
False

調用scoped_session的remove()方法會調用ScopedSession.close()關閉 session,釋放連接資源、把數據庫 transaction 狀態恢復到初始狀態等,最後銷燬 session 本身,下回再調用ScopedSession的時候,又會重新創建一個 session 出來。

這樣,我們在整個請求的任意環節,都可以開開心心地隨時通過工廠來獲取一個 “新的”session 對象,而只要在請求處理完的時候移除這個 session 就可以了。如果不用它,那麼你在每個需要讀寫數據庫的地方,都要小心翼翼地創建個 session 出來,並記得把它們關掉,不然就造成了資源泄漏

Thread-Local Storage

上面說到 scoped_session 類似單例模式,我們看似創建了新的 session,實際上拿到的可能是之前創建出來的 session。但我們 web 應用通常要同時處理多個請求,我的請求有沒有可能不小心拿到別人創建的 session 對象呢?

這是一個好問題,然而正直的 SQLAlchemy 不會讓不法分子得逞的

儘管 Python 有着神奇的 GIL,沒法真正的並行地跑線程,但至少還是有線程的概念的,對於不同的請求進來的時候我們通常會在不同的線程中進行處理。Python 裏有個概念叫 thread local storage(TLS),即線程本地存儲,它可以作爲全局變量一樣使用,但這個數據只在這個線程中有效,與其他的線程是隔離的。

import threading
mydata = threading.local()
mydata.x = 1

等等,全局變量?這不正好和剛纔 registry 模式的思想差不多嘛。和我們所希望的一樣,scoped_session 也確實使用了 tls 作爲 session 的存儲方式,一個線程只能拿到自己創建出來的 session,保證了不同線程不會亂入別人的 session。

使用 tls 還有另外一個好處,由於 session 是跟着線程走的,就算你沒有調用remove()親手幹掉 session,也會由於線程結束,session 也跟着被一起回收掉,不至於泄漏。(但仍建議在必要的時候對資源進行顯式的回收)

還有一個隱蔽的問題,如果我們用了 gevent 來處理併發而不是用多線程,會翻車嗎?答案是不會。++gevent 在monkey.patch_all()的時候,已經悄悄把這個 threading 相關的東西悄悄替換成自己的一套了++,thread-local 的東西已經變成了 greenlet-local,不同協程間仍是隔離的,一般不會有問題

要不要用 scoped_session

對於 Flask 框架,強烈不建議自己維護 session,就算我們已經有了 scoped_session,但這玩意仍舊不是那麼好用,有很多細節需要處理。Flask 有個名爲 Flask-SQLAlchemy 的擴展,它已經把 scoped_session 的這一套在內部幫你配置好了,你只需要正常使用它的db.session,無需關心 session 是怎麼來的,又是怎麼沒的

++但如果用了個別的框架,而它又沒有好用的自帶 ORM(即除了 Django 之外其他框架),或者是在非 web 應用裏使用,這時就應該使用 SQLALchemy 的 scoped_session,來減少一些 bug 或者內存泄漏的可能++

參考:

Contextual/Thread-local Sessions – SQLAlchemy Documentation

threading — Thread-based parallelism – Python Documentation

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章