Flask解析(二):Flask-Sqlalchemy与多线程、多进程
原创作者:flowell,转载请标明出处:https://www.cnblogs.com/flowell/p/multiprocessing_flask_sqlalchemy.html
Sqlalchemy
flask-sqlalchemy的session是线程安全的,但在多进程环境下,要确保派生子进程时,父进程不存在任何的数据库连接,可以通过调用db.get_engine(app=app).dispose()来手动销毁已经创建的engine,然后再派生子进程。
最近线上的项目总是会报出数据库连接相关的错误,比如“Command out of Sync”,“Mysql server has gone away”,“Lost databse connection”,“Package sequence out of order”等等,最终解决下来,发现以上错误可以分为两种,一种是和连接丢失有关的,一种是和连接被多个线程(进程)同时使用了有关。
我们项目基于flask,有多线程的场景,也有多进程的场景。orm用的是flask的拓展flask-sqlalchemy。flask-sqlalchemy的使用必须基于flask的app实例,也就是说要在app上下文中才能使用flask-sqlalchemy,所以在某些离线(非web)场景下,我们也用到了原生的Sqlalchemy。
原生的Sqlalchemy的使用方式是
engine = create_engine(db_url)
Session = sessionmaker(bind=engine)
session = Session()
session.query(xxx)
首先要创建一个engine,engine顾名思义就是和数据库连接的引擎。在实际发起查询前,是不会创建任何connection的。创建engine时可以通过指定poolclass参数来指定engine使用的连接池。默认是QueuePool,也可以设置为NullPool(不使用连接池)。为了方便理解,可以把engine视为管理连接池的对象。
sqlalchemy中session和我们平时数据库里说的session是两个不同的概念,在平时数据库中,session的生命周期从连接上数据库开始,到断开和数据库的连接位置。但是sqlalchemy中的session更多的是一种管理连接的对象,它从连接池取出一个连接,使用连接,然后释放连接,而自身也跟随着销毁。sqlalchemy中的Connection对象是管理真正数据库连接的对象,真正的数据库连接在sqlalchemy中是DBAPI。
默认地,如果不传入poolclass,则使用QueuePool(具有一定数量的连接池),如果不指定pool_recycle参数,则默认数据库连接不会刷新。也就是说连接如果不适用,则一直不去刷新它。但是问题来了,在Mysql中,输入“show variables like "%timeout%"; ” ,可以看到有一个waittimeout,还有interacttimeout,默认值为28800(8小时),这两个值代表着,如果8个小时内某个数据库连接都不和mysql联系,那么就会断掉这个连接。所以,8个小时过去了,Mysql把连接断掉了,但是sqlalchemy客户端这边却还保持着这个连接。当某个时候该连接从连接池被取出使用时,就会抛出“Mysql server has gone away”等连接丢失的信息。
解决这个问题的办法很简单,只要传入pool_recycle参数即可。特别地,在flask-sqlalchemy中不会出现这种问题,因为falsk-sqlalchemy拓展自动地帮我们注入了pool_recycle参数,默认为7200秒。
def apply_driver_hacks(self, app, sa_url, options):
"""This method is called before engine creation and used to inject
driver specific hacks into the options. The `options` parameter is
a dictionary of keyword arguments that will then be used to call
the :func:`sqlalchemy.create_engine` function.
The default implementation provides some saner defaults for things
like pool sizes for MySQL and sqlite. Also it injects the setting of
`SQLALCHEMY_NATIVE_UNICODE`.
"""
if sa_url.drivername.startswith('mysql'):
sa_url.query.setdefault('charset', 'utf8')
if sa_url.drivername != 'mysql+gaerdbms':
options.setdefault('pool_size', 10)
options.setdefault('pool_recycle', 7200) # 默认7200秒刷新连接
elif sa_url.drivername == 'sqlite':
pool_size = options.get('pool_size')
detected_in_memory = False
if sa_url.database in (None, '', ':memory:'):
detected_in_memory = True
from sqlalchemy.pool import StaticPool
options['poolclass'] = StaticPool
if 'connect_args' not in options:
options['connect_args'] = {}
options['connect_args']['check_same_thread'] = False # we go to memory and the pool size was explicitly set
# to 0 which is fail. Let the user know that
if pool_size == 0:
raise RuntimeError('SQLite in memory database with an '
'empty queue not possible due to data '
'loss.')
# if pool size is None or explicitly set to 0 we assume the
# user did not want a queue for this sqlite connection and
# hook in the null pool.
elif not pool_size:
from sqlalchemy.pool import NullPool
options['poolclass'] = NullPool # if it's not an in memory database we make the path absolute.
if not detected_in_memory:
sa_url.database = os.path.join(app.root_path, sa_url.database) unu = app.config['SQLALCHEMY_NATIVE_UNICODE']
if unu is None:
unu = self.use_native_unicode
if not unu:
options['use_native_unicode'] = False if app.config['SQLALCHEMY_NATIVE_UNICODE'] is not None:
warnings.warn(
"The 'SQLALCHEMY_NATIVE_UNICODE' config option is deprecated and will be removed in"
" v3.0. Use 'SQLALCHEMY_ENGINE_OPTIONS' instead.",
DeprecationWarning
)
if not self.use_native_unicode:
warnings.warn(
"'use_native_unicode' is deprecated and will be removed in v3.0."
" Use the 'engine_options' parameter instead.",
DeprecationWarning
)
sessionmaker是Session定制方法,我们把engine传入sessionmaker中,就可以得到一个session工厂,通过工厂来生产真正的session对象。但是这种生产出来的session是线程不安全的,sqlalchemy提供了scoped_session来帮助我们生产线程安全的session,原理类似于Local,就是代理session,通过线程的id来找到真正属于本线程的session。
flask-sqlalchemy就是使用了scoped_session来保证线程安全,具体的代码可以在Sqlalchemy中看到,构造session时,使用了scoped_session。
def create_scoped_session(self, options=None):
"""Create a :class:`~sqlalchemy.orm.scoping.scoped_session`
on the factory from :meth:`create_session`.
An extra key ``'scopefunc'`` can be set on the ``options`` dict to
specify a custom scope function. If it's not provided, Flask's app
context stack identity is used. This will ensure that sessions are
created and removed with the request/response cycle, and should be fine
in most cases.
:param options: dict of keyword arguments passed to session class in
``create_session``
""" if options is None:
options = {} scopefunc = options.pop('scopefunc', _app_ctx_stack.__ident_func__)
options.setdefault('query_cls', self.Query)
return orm.scoped_session(
self.create_session(options), scopefunc=scopefunc
) def create_session(self, options):
"""Create the session factory used by :meth:`create_scoped_session`.
The factory **must** return an object that SQLAlchemy recognizes as a session,
or registering session events may raise an exception.
Valid factories include a :class:`~sqlalchemy.orm.session.Session`
class or a :class:`~sqlalchemy.orm.session.sessionmaker`.
The default implementation creates a ``sessionmaker`` for :class:`SignallingSession`.
:param options: dict of keyword arguments passed to session class
""" return orm.sessionmaker(class_=SignallingSession, db=self, **options)
多进程和数据库连接
多进程环境下,要注意和数据库连接相关的操作。
说到多进程,python里最常用的就是multiprocessing。multiprocessing在windows下和linux的表现有所区别,在此只讨论linux下的表现。linux下多进程通过fork()来派生,要理解我下面说的必须先弄懂fork()是什么东西。粗略地说,每个进程都有自己的一个空间,称为进程空间,每个进程的进程空间都是独立的,进程与进程之间互不干扰。fork()的作用,就是将一个进程的进程空间,完完全全地copy一份,copy出来的就是子进程了,所以我们说子进程和父进程有着一模一样的地址空间。地址空间就是进程运行的空间,这空间里会有进程已经打开的文件描述符,文件描述符会间接地指向进程已经打开的文件。也就是说,fork()之后,父进程,子进程会有相同的文件描述符,指向相同的一个文件。为什么?因为文件是存在硬盘里的,fork()时copy的内存中的进程空间,并没有把文件也copy一份。这就导致了,父进程,子进程,同时指向同一个文件,他们任意一个都可以对这个文件进行操作。这和本文说的数据库有啥关系?顺着这个思路想,数据库连接是不是一个TCP连接?TCP连接是不是一个socket?socket在linux下是什么,就是一个文件。所以说,如果父进程在fork()之前打开了数据库连接,那么子进程也会拥有这个打开的连接。
两个进程同时写一个连接会导致数据混乱,所以会出现“Command out of sync”的错误,两个进程同时读一个连接,会导致一个进程读到了,另一个没读到,就是“No result”。一个进程关闭了连接,另一个进程并不知道,它试图去操作连接时,就会出现“Lost database connection”的错误。
在此讨论的场景是,父进程在派生子进程之前,父进程拥有已打开的数据库连接。派生出子进程之后,子进程也就拥有了相应的连接。如果在fork()之前父进程没有打开数据库连接,那么也不用担心这个问题。比如Celery使用的prefork池,虽然是多进程模型,但是celery在派子进程前时不会打开数据库连接的,所以不用担心在celery任务中会出现数据库连接混乱的问题。
我做的项目里的多进程的场景之一就是使用tornado来跑web应用,在派生多个web应用实例时,确保此前创建的数据库连接被销毁。
app = Flask()
db = Sqlalchemy()
db.init_app(app)
...
...
db.get_engine(app=app).dispose() # 先销毁已有的engine,确保父进程没有数据库连接
...
...
fork() # 派生子进程 # 例如
tornado.start() # 启动多个web实例进程
Flask解析(二):Flask-Sqlalchemy与多线程、多进程的更多相关文章
- Flask系列(二)Flask基础
知识点回顾 1.flask依赖wsgi,实现wsgi的模块:wsgiref(django),werkzeug(flask),uwsgi(上线) 2.实例化Flask对象,里面是有参数的 app = F ...
- python 全栈开发,Day142(flask标准目录结构, flask使用SQLAlchemy,flask离线脚本,flask多app应用,flask-script,flask-migrate,pipreqs)
昨日内容回顾 1. 简述flask上下文管理 - threading.local - 偏函数 - 栈 2. 原生SQL和ORM有什么优缺点? 开发效率: ORM > 原生SQL 执行效率: 原生 ...
- python 全栈开发,Day141(flask之应用上下文,SQLAlchemy)
一.flask之应用上下文 由于时间关系,详细过程略... 草稿图 参考链接: http://www.cnblogs.com/zhaopanpan/p/9457343.html 总结: 上下文管理(应 ...
- 第六章 Flask数据库(二)
Flask-SQLALchemy Flask-SQLALchemy 是一个给你的应用添加 SQLALchemy 支持的 Flask 扩展. 它需要 SQLAlchemy 0.6 或更高的版本.它致力于 ...
- flask 之(二) --- 视图|模版|模型
Flask框架 打开pycharm编译器,新建一个Flask项目,选择提前建好的虚拟环境 . 项目结构: static:静态资源文件,可以直接被浏览器访问 templates:模版文件,必须在项目的p ...
- flask系列四之SQLAlchemy
一.SQLAlchemy简介 (1)flask_sqlalchemy是一套ORM框架. (2)ORM(Object Relationship Mapping):模型关系映射 (3)ORM的好处:可以让 ...
- flask笔记(三)Flask 添加登陆验证装饰器报错,及解析
Flask 添加登陆验证装饰器报错,及解析 写这个之前,是想到一个需求,这个是关于之前写Flask笔记(二)中的一个知识点,路由相关 需求为 : 有一些页面必须是登陆之后才能访问的,比如Shoppin ...
- Flask备注二(Configurations, Signals)
Flask备注二(Configuration, Signals) Flask是一个使用python开发Web程序的框架.依赖于Werkzeug提供完整的WSGI支持,以及Jinja2提供templat ...
- Flask知识点二
一 模板 1.模板的使用 Flask使用的是Jinja2模板,所以其语法和Django无差别 2.自定义模板方法 Flask中自定义模板方法的方式和Bottle相似,创建一个函数并通过参数的形式传入 ...
- flask插件系列之SQLAlchemy基础使用
sqlalchemy是一个操作关系型数据库的ORM工具.下面研究一下单独使用和其在flask框架中的使用方法. 直接使用sqlalchemy操作数据库 安装sqlalchemy pip install ...
随机推荐
- 基于Docker搭建大数据集群(二)基础组件配置
主要内容 jdk环境搭建 scala环境搭建 zookeeper部署 mysql部署 前提 docker容器之间能免密钥登录 yum源更换为阿里源 安装包 微云分享 | tar包目录下 JDK 1.8 ...
- 详解http报文
摘要 作为一个web开发者,每天都在使用者Http协议,却总是一知半解.本文参看Http RFC7230规范,梳理了http报文部分. http 报文构成 start-line: 起始行,描述请求或响 ...
- 为何stop()和suspend()方法不推荐使用(转)
stop()方法作为一种粗暴的线程终止行为,在线程终止之前没有对其做任何的清除操作,因此具有固有的不安全性. 用Thread.stop()方法来终止线程将会释放该线程对象已经锁定的所有监视器.如果以前 ...
- at,crontab例行性任务
at:仅执行一次就结束的调度命令 at [-mldvc] TIME -m:当at的工作完成后,即使没有输出信息,也会以email的方式通知用户工作已完成 -l:相当于atq,列出系统上所有该用户的at ...
- http转换为https
1.下载ssl 证数 百度ssl 证数都有 其中以便宜ssl为例子 注册登陆 选择免费版 可以使用3个月: 申请过程中需要检测该域名是否为本人所有 ,所以邮箱检测或者域名配置 很简单检测就好了: 验证 ...
- ELK 学习笔记之 Logstash之filter配置
Logstash之filter: json filter: input{ stdin{ } } filter{ json{ source => "message" } } o ...
- egret引擎中使用tiled运行在微信小游戏中
egret的官方文档,对tiled的介绍不是很细致,很多东西都需要摸索.现在把踩的坑记录下来.作为一个备忘 引用tiledmap的库 在GitHub上下载egret的tiledmap支持库:https ...
- HashMap 取数算法
Map,百度翻译给我的解释是映射,在Java编程中,它是存储键值对(key-value)的一种容器,也是Java程序员常用的对象.这篇博客介绍下HashMap的实现:java是面向对象编程语言,jdk ...
- 刷新:重新发现.NET与未来
微软在比尔·盖茨手中创立并崛起, 成为PC互联网时代的霸主,很多70,80后都有MVP Ediwang的体验: 当时的微软对我来说就是神的存在.因为我认识电脑到使用电脑的一切几乎都离不开这家伟大的公司 ...
- 02-17 kd树
目录 kd树 一.kd树学习目标 二.kd树引入 三.kd树详解 3.1 构造kd树 3.1.1 示例 3.2 kd树搜索 3.2.1 示例 四.kd树流程 4.1 输入 4.2 输出 4.3 流程 ...