【Python SQLAlchemy】数据库连接池SQLAlchemy中多线程安全的问题

问题描述

写了一个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 做了非常好的封装,是线程安全的

发布了610 篇原创文章 · 获赞 232 · 访问量 23万+

猜你喜欢

转载自blog.csdn.net/sinat_42483341/article/details/104406552