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线程安全源码解读的更多相关文章

  1. 线程本地变量ThreadLocal源码解读

      一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ...

  2. 从koa-session源码解读session本质

    前言 Session,又称为"会话控制",存储特定用户会话所需的属性及配置信息.存于服务器,在整个用户会话中一直存在. 然而: session 到底是什么? session 是存在 ...

  3. 线程池ThreadPoolExecutor源码解读研究(JDK1.8)

    一.什么是线程池 为什么要使用线程池?在多线程并发开发中,线程的数量较多,且每个线程执行一定的时间后就结束了,下一个线程任务到来还需要重新创建线程,这样线程数量特别庞大的时候,频繁的创建线程和销毁线程 ...

  4. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  5. AFNetworking 3.0 源码解读 总结(干货)(下)

    承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...

  6. Alamofire源码解读系列(一)之概述和使用

    尽管Alamofire的github文档已经做了很详细的说明,我还是想重新梳理一遍它的各种用法,以及这些方法的一些设计思想 前言 因为之前写过一个AFNetworking的源码解读,所以就已经比较了解 ...

  7. Alamofire源码解读系列(六)之Task代理(TaskDelegate)

    本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...

  8. Flask(4)- flask请求上下文源码解读、http聊天室单聊/群聊(基于gevent-websocket)

    一.flask请求上下文源码解读 通过上篇源码分析,我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__call__方法返回了app的wsgi_app(en ...

  9. flask的请求上下文源码解读

    一.flask请求上下文源码解读 通过上篇源码分析( ---Flask中的CBV和上下文管理--- ),我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__ ...

随机推荐

  1. 基于partition的递归

    partition算法可以应用在快速排序算法中,也可以应用到 Selection algorithm(在无序数组中寻找第K大的值) Partition 实现 快速排序中用到的 partition 算法 ...

  2. POJ 1741 单次询问树上距离<=K的点对数 点分治

    #include<cstdio> #include<cstring> #include<algorithm> using namespace std; ; ; ], ...

  3. hashmap C++实现分析及std::unordered_map拓展

    今天想到哈希函数,好像解决冲突的只了解了一种链地址法而且也很模糊,就查了些资料复习一下 1.哈希Hash 就是把任意长度的输入,通过哈希算法,变换成固定长度的输出(通常是整型),该输出就是哈希值. 这 ...

  4. pycharm图像不能显示,之前是可以显示的。显示一两次突然不显示了

    网上说是什么包问题的就说了.我遇到一个非常奇葩的问题 因为你的设置可能是这样 每次都在窗口右侧的工具栏那边显示.可能突然心情不佳就不显示了.然后你再把勾去掉即可.你要喜欢再点上也行.

  5. HADOOP HA 报错 - 所有 namenode 都是standby --集群报错: Operation category READ is not supported in state standby

    报错: 经过查看集群的jps如下: ==================== hadoop01 jps =================== FsShell ResourceManager Name ...

  6. 微信公众号开发不能使用session原因

    今天做微信公众号开发整合功能的时候,使用session保存记录.用postman测试好使,但是一旦用手机就不好使.上网查了好久才明白,微信开发是不能用session的.具体原因如下:因为微信的所有请求 ...

  7. httpclient个人理解

    httpclient:模拟浏览器发送请求,服务器会响应数据,用心区域网内 不同系统间的请求调用 依赖  httpclient.jar和httpcore.jar需要同时纯在 <dependency ...

  8. react-native连接夜神模拟器

    配置好adb的环境变量 新建 ADB_MY_HOME C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools 然后加入path ...

  9. MessagePack Jackson 数据大小

    我们在使用 MessagePack 对 List 对象数据进行序列化的时候,发现序列化以后的二进制数组数据偏大的情况. 请注意,不是所有的 List 对象都会出现这种情况,这个根据你 List 对象中 ...

  10. 学习日记6、easyui datagrid 新增一行,编辑行,结束编辑和删除行操作记录

    1.新增一行并进入编辑状态 var index=$('#Numbers').datagrid('appendRow', { CardInformation: '开户行', CardNumber: '银 ...