flask_sqlalchemy的session线程安全源码解读
flask_sqlalchemy是如何在多线程中对数据库操作不相互影响
数据库操作隔离
结论:使用scoped_session实现数据库操作隔离
flask的api.route()接收一个请求,就会创建一个新的线程去处理,请求之间不相互影响
flask_sqlalchemy是如何使用db.session使多个请求中保函的改变同一个表的sql操作不相互影响的
在flask_sqlalchemy.SQLAlchemy类中关于session的定义:
# Which stack should we use? _app_ctx_stack is new in 0.9
connection_stack = _app_ctx_stack or _request_ctx_stack
def __init__(self, app=None,
use_native_unicode=True,
session_options=None):
session_options.setdefault(
'scopefunc', connection_stack.__ident_func__
)
self.session = self.create_scoped_session(session_options)
def create_scoped_session(self, options=None):
"""Helper factory method that creates a scoped session."""
if options is None:
options = {}
scopefunc=options.pop('scopefunc', None)
return orm.scoped_session(
partial(_SignallingSession, self, **options), scopefunc=scopefunc
)
self.session = self.create_scoped_session(session_options) 以及最后返回的
return orm.scoped_session(self.create_session(options), scopefunc=scopefunc)
可以看到使用的是sqlalchemy.orm.scoped_session
线程安全:scoped_session
结论:scoped_session会为每一个请求创建独立的session, 由线程id或者_app_ctx_stack.__ident_func__为标记
sqlalchemy的session对象
from sqlalchemy.orm import sessionmaker
session = sessionmaker()
一般我们会通过sessionmaker()这个工厂函数创建session,但这个session并不能用在多线程中,为了支持多线程
操作,sqlalchemy提供了scoped_session,通过名字反映出scoped_session是通过某个作用域实现的
所以在多线程中一般都是如下使用session
from sqlalchemy.orm import scoped_session, sessionmaker
session = scoped_session(sessionmaker())
我们来看看scoped_session是如何提供多线程环境支持的
class scoped_session(object):
def __init__(self, session_factory, scopefunc=None): self.session_factory = session_factory
if scopefunc:
self.registry = ScopedRegistry(session_factory, scopefunc)
else:
self.registry = ThreadLocalRegistry(session_factory)
__init__中,session_factory是创建session的工厂函数,而sessionmaker就是一工厂函数(其实是定义了__call__的
函数)而scopefunc就是能产生某个作用域的函数,如果不提供将使用ThreadLocalRegistry
class ThreadLocalRegistry(ScopedRegistry):
def __init__(self, createfunc):
self.createfunc = createfunc
self.registry = threading.local() def __call__(self):
try:
return self.registry.value
except AttributeError:
val = self.registry.v
从上面__call__可以看出,每次都会创建新的session,并发在线程本地变量中,你可能会好奇__call__是在哪里调用的?
def instrument(name):
def do(self, *args, **kwargs):
return getattr(self.registry(), name)(*args, **kwargs)
return do
for meth in Session.public_methods:
setattr(scoped_session, meth, instrument(meth))
正如我们所看到的,当我们调用session.query将会调用 getattr(self.registry(), 'query'),self.registry()就是
调用__call__的时机,但是在flask_sqlalchemy中并没有使用ThreadLocalRegistry,创建scoped_session过程如下
# Which stack should we use? _app_ctx_stack is new in 0.9
connection_stack = _app_ctx_stack or _request_ctx_stack def __init__(self, app=None,
use_native_unicode=True,
session_options=None):
session_options.setdefault(
'scopefunc', connection_stack.__ident_func__
)
self.session = self.create_scoped_session(session_options) def create_scoped_session(self, options=None):
"""Helper factory method that creates a scoped session."""
if options is None:
options = {}
scopefunc=options.pop('scopefunc', None)
return orm.scoped_session(
partial(_SignallingSession, self, **options), scopefunc=scopefunc
)
我们看到scopefunc被设置为connection_stack.__ident_func__,而connection_stack就是flask中app上下文,
__ident_func__其实就是在多线程中就是thrading.get_ident,也就是线程id
我们看看ScopedRegistry是如何通过_操作的
class ScopedRegistry(object):
def __init__(self, createfunc, scopefunc):
self.createfunc = createfunc
self.scopefunc = scopefunc
self.registry = {} def __call__(self):
key = self.scopefunc()
try:
return self.registry[key]
except KeyError:
return self.registry.setdefault(key, self.createfunc())
代码也很简单,其实也就是根据线程id创建对应的session对象,到这里我们基本已经了解了flask_sqlalchemy的线程安全原理。
1.flask_sqlalchemy能否使用ThreadLocalRegistry?
大部分情况都是可以的,但如果wsgi对多并发使用的是greenlet的模式就不适用了
2.上面create_scoped_session中partial是干嘛的?
前面我们说过scoped_session的session_factory是可调用对象,但_SignallingSession类并没有定义__call__,所以通过partial支持
这里说一下对db.relationship lazy的理解,看如下代码
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role', lazy='dynamic') class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
假设role是已经获取的一个Role的实例
lazy:dynamic => role.users不会返回User的列表, 返回的是sqlalchemy.orm.dynamic.AppenderBaseQuery对象
当执行role.users.all()是才会真正执行sql,这样的好处就是可以继续过滤
lazy:select => role.users直接返回User实例的列表,也就是直接执行sql
注意:db.session.commit只有在对象有变化时才会真的执行update
两个比较重要的配置
app.config['SQLALCHEMY_ECHO'] = True =》配置输出sql语句
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True =》每次request自动提交db.session.commit()
这是通过app.teardown_appcontext注册实现
# 绑定app然后初始化sql配置
if app is not None:
self.init_app(app) # 使用钩子,当请求结束后若没有配置自动提交,则移除此session
@teardown
def shutdown_session(response_or_exc):
if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
if response_or_exc is None:
self.session.commit()
self.session.remove()
return response_or_exc # sqlalchemy.orm.scoping.scoped_session
# sqlalchemy.util._collections.ScopedRegistry 定义
def clear(self):
#Clear the current scope, if any.
try:
del self.registry[self.scopefunc()]
except KeyError:
pass
response_or_exc为异常值,默认为sys.exc_info()[1]
上面self.session.remove()表示每次请求后都会销毁self.session,不然会导致存放session的字段太大。
https://blog.csdn.net/luffyser/article/details/89380186
flask_sqlalchemy的session线程安全源码解读的更多相关文章
- 线程本地变量ThreadLocal源码解读
一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ...
- 从koa-session源码解读session本质
前言 Session,又称为"会话控制",存储特定用户会话所需的属性及配置信息.存于服务器,在整个用户会话中一直存在. 然而: session 到底是什么? session 是存在 ...
- 线程池ThreadPoolExecutor源码解读研究(JDK1.8)
一.什么是线程池 为什么要使用线程池?在多线程并发开发中,线程的数量较多,且每个线程执行一定的时间后就结束了,下一个线程任务到来还需要重新创建线程,这样线程数量特别庞大的时候,频繁的创建线程和销毁线程 ...
- SDWebImage源码解读之SDWebImageDownloaderOperation
第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...
- AFNetworking 3.0 源码解读 总结(干货)(下)
承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...
- Alamofire源码解读系列(一)之概述和使用
尽管Alamofire的github文档已经做了很详细的说明,我还是想重新梳理一遍它的各种用法,以及这些方法的一些设计思想 前言 因为之前写过一个AFNetworking的源码解读,所以就已经比较了解 ...
- Alamofire源码解读系列(六)之Task代理(TaskDelegate)
本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...
- Flask(4)- flask请求上下文源码解读、http聊天室单聊/群聊(基于gevent-websocket)
一.flask请求上下文源码解读 通过上篇源码分析,我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__call__方法返回了app的wsgi_app(en ...
- flask的请求上下文源码解读
一.flask请求上下文源码解读 通过上篇源码分析( ---Flask中的CBV和上下文管理--- ),我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__ ...
随机推荐
- fragment初步认识
- Git 简要教程
Git是一个管理系统,管理版本,管理内容(CMS),管理工作等. Git主要还是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目. 工作流程是这样的: 克隆 Git 资源作为工作目录 ...
- IMP-00058: ORACLE error 1882 encountered
问题现象: IMP: ORACLE error encountered ORA: timezone region not found ORA: at "SYS.DBMS_EXPORT_EXT ...
- Python爬虫进阶之Scrapy框架安装配置
Python爬虫进阶之Scrapy框架安装配置 初级的爬虫我们利用urllib和urllib2库以及正则表达式就可以完成了,不过还有更加强大的工具,爬虫框架Scrapy,这安装过程也是煞费苦心哪,在此 ...
- GitHub 上最热的10款国产开源软件
衡量一个开源产品好不好,看看产品在 GitHub 的 Star 数量就知道了.由此可见,GitHub 已经沦落为开源产品的“大众点评”了.一个开源产品希望快速的被开发者知道.快速的获取反馈,放到 Gi ...
- 需要以管理员的身份运行程序(winform)
1.添加应用程序清单文件(app.manifest) 2.打开app.manifest,将<requestedExecutionLevel level="asInvoker" ...
- KNN分类算法
K邻近算法.K最近邻算法.KNN算法(k-Nearest Neighbour algorithm):是数据挖掘分类技术中最简单的方法之一 KNN的工作原理 所谓K最近邻,就是k个最近的邻居的意思,说的 ...
- linux内核 进程调度
概念: 进程调度决定那个进程投入运行,运行多长时间. 进程调度没有太复杂的原理,最大限度的利用处理器时间的原则是:只要有可执行的程序,那么总会有进程在执行,如果可运行的进程比处理器数目要多,那么注定要 ...
- POJ-2752-Seek the Name(KMP, 循环节)
链接: https://vjudge.net/problem/POJ-2752#author=0 题意: 给定若干只含小写字母的字符串(这些字符串总长≤400000),在每个字符串中求出所有既是前缀又 ...
- luogu 3441 [POI2006]MET-Subway 拓扑排序+思维
Description 给出一棵N个结点的树,选择L条路径,覆盖这些路径上的结点,使得被覆盖到的结点数最多. Input 第一行两个正整数N.L(2 <= N <= 1,000,000, ...