SQLAlchemy 的 scoped_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_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 或者内存泄漏的可能。

参考

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值