问题描述
写了一个
apscheduler
定时任务
里面用到SQLAlchemy
在同一时间点开启了18个任务,用于更新18个表
但是最终看数据库表的时候,发现有2个表未更新。查看打印日志均正常,查找原因。
参考以下
提问
最近把原来的单线程改为多线程,从而引起了sqlalchemy的错误。
我自己简单封装了 一个sqlalchemy类,用来进行各种数据库操作。
自从改为多线程后,线程一多,在更新数据的时候就会出错。
我自己封装的sqlalchemy,是共用一个 Session的
def __init__():
Session = sessionmaker(bind=self.engine)
self.session = Session()
请教:如果我们想继续使用多线程,应该怎么改进我自己的封装的 sqlalchemy类,
是做数据库连接池,还是可以多建几个 session , 一个engine可以同时建立几个 session??
回答
我也遇到了同样的问题,产生问题的原因是session并不是线程安全,当你在一个线程commit时,所有线程都会commit(好像是这样,不是100%确定),文档推荐使用scoped_session
数据库连接池SQLAlchemy中多线程安全的问题
正确示范:
1、数据库模块model.py
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
session_factory = sessionmaker(bind=some_engine)
Session = scoped_session(session_factory)
2、业务模块thread.py
import threading
from model import Session
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(20))
fullname = Column(String(20))
password = Column(String(20))
age = Column(Integer)
class MyThread(threading.Thread):
def __init__(self, threadName):
super(MyThread, self).__init__()
self.name = threading.current_thread().name
def run(self):
session = Session() #每个线程都可以直接使用数据库模块定义的Session
session.query(User).all()
user = User(name="hawk-%s"%self.name, fullname="xxxx",password="xxxx",age=10)
session.add(user)
time.sleep(1)
if self.name == "thread-9":
session.commit()
Session.remove()
if __name__ == "__main__":
arr = []
for i in xrange(10):
arr.append(MyThread('thread-%s' % i))
for i in arr:
i.start()
for i in arr:
i.join()
错误示范:
class MyThread(threading.Thread):
def __init__(self, threadName):
super(MyThread, self).__init__()
self.session = Session() #错误!
self.name = threading.current_thread().name
def run(self):
self.session.query(User).all()
user = User(name="hawk-%s"%self.name, fullname="xxxx",password="xxxx",age=10)
self.session.add(user)
time.sleep(1)
if self.name == "thread-9":
self.session.commit()
Session.remove()
错误解析:
看了SQLAlchemy源码之后发现,Session() 返回的是一个threading.local()对象的成员变量,threading.local()对象只有在线程内部才能实现线程隔离,因此只能放在run()函数里,而不能作为类成员变量。
如果按照错误示例来运行,所有线程其实公用了一个session,没有做到线程隔离,session.commit()操作会互相影响,我们原本只想将thread-9中的数据插入,结果会发现,所有线程中的数据全部被插入。
使用Nullpool
避免使用SQLAlchemy使用连接池
在使用 create_engine创建引擎时,如果默认不指定连接池设置的话,一般情况下,SQLAlchemy会使用一个 QueuePool绑定在新创建的引擎上。并附上合适的连接池参数。
在以默认的方法create_engine时(如下),就会创建一个带连接池的引擎。
engine = create_engine('postgresql://[email protected]/dbname')
在这种情况下,当你使用了session后就算显式地调用session.close(),也不能把连接关闭。连接会由QueuePool连接池进行管理并复用。
这种特性在一般情况下并不会有问题,不过当数据库服务器因为一些原因进行了重启的话。最初保持的数据库连接就失效了。随后进行的session.query()等方法就会抛出异常导致程序出错。
如果想禁用SQLAlchemy提供的数据库连接池,只需要在调用create_engine是指定连接池为NullPool,SQLAlchemy就会在执行session.close()后立刻断开数据库连接。当然,如果session对象被析构但是没有被调用session.close(),则数据库连接不会被断开,直到程序终止。
结论
结合以上,因为添加过nullpool,所以整个过程只保持一个session(相当于connection)连接。
而在apscheduler开启的多线程环境下,session.commit()、session.close()操作会互相影响,导致多次(误)commit,甚至在执行session.close()后断开数据库连接,导致写数据失败
SQLAlchemy的session
不是线程安全的
在apscheduler开启的多线程环境下,所有线程公用了一个session
每个线程中session.commit()
会互相影响,一次调用,全部提交
如果调用了session.rollback()
,所有线程全部rollback,导致不可预期影响
解决方式
方式1:避免任务重叠
方式2:似乎可以使用 scoped_session,重构代码吧,那就是另外一个坑了
拓展阅读
使用flask-sqlalchemy写代码码到一半,突然想到,Session是否是线程安全的?于是上官方文档,答案是否
结论:flask-sqlalchemy 对 sqlachemy 做了非常好的封装,是线程安全的