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_sessionremove()方法会调用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或者内存泄漏的可能。

参考