MySQL服务器已断开 - 结账事件处理程序的断开处理无效

8 投票
4 回答
5687 浏览
提问于 2025-04-17 05:05

更新 3/4:

我做了一些测试,证明使用 checkout 事件处理程序来检查断开连接在 Elixir 中是有效的。 我开始觉得我的问题可能和从子进程调用 session.commit() 有关? 更新:我刚刚推翻了自己的想法,通过在子进程中调用 session.commit(),下面的例子已经更新。我正在使用 multiprocessing 模块来创建子进程。

下面是展示应该如何工作的代码(甚至不需要使用 pool_recycle!):

from sqlalchemy import exc
from sqlalchemy import event
from sqlalchemy.pool import Pool
from elixir import *
import multiprocessing as mp

class SubProcess(mp.Process):
    def run(self):
        a3 = TestModel(name="monkey")
        session.commit()

class TestModel(Entity):
    name = Field(String(255))

@event.listens_for(Pool, "checkout")
def ping_connection(dbapi_connection, connection_record, connection_proxy):
    cursor = dbapi_connection.cursor()
    try:
        cursor.execute("SELECT 1")
    except:
        # optional - dispose the whole pool
        # instead of invalidating one at a time
        # connection_proxy._pool.dispose()

        # raise DisconnectionError - pool will try
        # connecting again up to three times before raising.
        raise exc.DisconnectionError()
    cursor.close()

from sqlalchemy import create_engine
metadata.bind = create_engine("mysql://foo:bar@localhost/some_db", echo_pool=True)
setup_all(True)

subP = SubProcess()

a1 = TestModel(name='foo')
session.commit()

# pool size is now three.

print "Restart the server"
raw_input()

subP.start()

#a2 = TestModel(name='bar')
#session.commit()

更新 2:

由于 MySQL-python 1.2.2 版本之后不再支持 reconnect 参数,我被迫寻找其他解决方案。有没有人有解决办法? :\

更新 1(旧解决方案,不适用于 MySQL-python 版本 > 1.2.2):

找到了解决方案:将 connect_args={'reconnect':True} 传递给 create_engine 调用可以解决问题,自动重新连接。似乎甚至不需要 checkout 事件处理程序。

所以,在问题中的例子里:

metadata.bind = create_engine("mysql://foo:bar@localhost/db_name", pool_size=100, pool_recycle=3600, connect_args={'reconnect':True})

原始问题:

我在谷歌上搜索了很久这个问题,但似乎没有找到针对 Elixir 的具体解决方案——我试图使用 SQLAlchemy 文档中的 "断开连接处理 - 悲观" 示例来处理 MySQL 的断开连接。然而,当我测试这个(通过重启 MySQL 服务器)时,"MySQL server has gone away" 的错误在我的 checkout 事件处理程序之前就被抛出了。

这是我用来初始化 Elixir 的代码:

##### Initialize elixir/SQLAlchemy
# Disconnect handling
from sqlalchemy import exc
from sqlalchemy import event
from sqlalchemy.pool import Pool

@event.listens_for(Pool, "checkout")
def ping_connection(dbapi_connection, connection_record, connection_proxy):
    logging.debug("***********ping_connection**************")
    cursor = dbapi_connection.cursor()
    try:
        cursor.execute("SELECT 1")
    except:
        logging.debug("######## DISCONNECTION ERROR #########")            
        # optional - dispose the whole pool
        # instead of invalidating one at a time
        # connection_proxy._pool.dispose()

        # raise DisconnectionError - pool will try
        # connecting again up to three times before raising.
        raise exc.DisconnectionError()
    cursor.close()

metadata.bind= create_engine("mysql://foo:bar@localhost/db_name", pool_size=100, pool_recycle=3600)

setup_all()

我创建了 Elixir 实体对象,并通过 session.commit() 保存它们,在这个过程中我看到上面定义的事件生成的 "ping_connection" 消息。然而,当我重启 MySQL 服务器并再次测试时,它在 ping 连接事件之前就失败了,显示了 mysql server has gone away 的消息。

以下是从相关行开始的堆栈跟踪:

  File "/usr/local/lib/python2.6/dist-packages/elixir/entity.py", line 1135, in get_by
    return cls.query.filter_by(*args, **kwargs).first()
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 1963, in first
    ret = list(self[0:1])
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 1857, in __getitem__
    return list(res)
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 2032, in __iter__
    return self._execute_and_instances(context)
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/orm/query.py", line 2047, in _execute_and_instances
    result = conn.execute(querycontext.statement, self._params)
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1399, in execute
    params)
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1532, in _execute_clauseelement
    compiled_sql, distilled_params
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1640, in _execute_context
    context)
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/base.py", line 1633, in _execute_context
    context)
  File "/usr/local/lib/python2.6/dist-packages/sqlalchemy/engine/default.py", line 330, in do_execute
    cursor.execute(statement, parameters)
  File "/usr/lib/pymodules/python2.6/MySQLdb/cursors.py", line 166, in execute
    self.errorhandler(self, exc, value)
  File "/usr/lib/pymodules/python2.6/MySQLdb/connections.py", line 35, in defaulterrorhandler
    raise errorclass, errorvalue
OperationalError: (OperationalError) (2006, 'MySQL server has gone away') 

4 个回答

0

你是在同一个会话中进行操作吗?也就是说,在重启mysqld之前和之后都是用的同一个会话?如果是这样的话,"checkout"事件只会在你开始一个新事务的时候发生。当你调用commit()时,就会开始一个新的事务(除非你使用的是自动提交模式),这时连接就会被“签出”。所以你是在“签出”之后重启mysqld的。

在第二次操作之前(并且在重启mysqld之后)简单地调用一下commit()rollback(),应该能解决你的问题。否则的话,考虑每次在上一次提交后等了很久时,使用一个新的会话。

2

实际的问题是,每次你调用sessionmaker工厂的时候,sqlalchemy都会给你同一个会话。这就可能导致你在后面查询的时候,使用的是一个很早之前打开的会话,只要你没有调用session.remove()。每次请求会话的时候都要记得调用remove()可真让人头疼,不过sqlalchemy提供了一个更简单的解决方案:上下文“作用域”会话。

要创建一个作用域会话,只需把你的sessionmaker包裹起来:

from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker())

这样每次你调用工厂的时候,都会得到一个上下文绑定的会话,这意味着一旦调用的函数结束,sqlalchemy会自动为你调用session.remove()。详细信息可以查看这里:sqlalchemy - 上下文会话的生命周期

2

最后的解决办法是在处理和加载elixir实体之前,先调用session.remove()。这样做的效果是把连接归还给连接池,这样下次再用的时候,连接池会触发一个事件,我们的处理程序就能检测到连接断开了。来自SQLAlchemy文档

在请求结束时并不一定要移除会话,还有其他选择,比如在结束时调用Session.close()、Session.rollback()或Session.commit(),这样现有的会话就会把连接归还给连接池,并清除任何现有的事务上下文。如果不做任何操作也是可以的,只要每个控制器方法负责确保在请求结束后没有事务保持打开状态。

这是个很重要的小信息,我希望在elixir文档中能提到。不过我想这可能假设了读者对SQLAlchemy有一定的了解吧?

撰写回答