MySql数据库断连

在使用 MySql 数据库时,如果使用了长连接或连接池,可能会遇到数据库连接断开的情况。

收到的错误信息例如:

MySQLdb.OperationalError: (2013, 'Lost connection to MySQL server during query')

原因#

发生这种情况的原因是 MySql 客户端和服务器端设置的连接时常不一致导致。

服务器端超时#

在 MySql 的服务器端,可以设置连接的等待时长,即连接在长时间不活跃时,主动关闭该连接。

https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_wait_timeout https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_interactive_timeout

可以通过下面的语句来动态设置等待时长:

SET GLOBAL interactive_timeout=600;
SET GLOBAL wait_timeout=600;

其中 interactive_timeout 是针对交互式连接的等待时长,wait_timeout 是针对非交互式连接的等待时长。

客户端超时#

通常在长连接或连接池的设置中,可以设置连接的时长,即连接在创建后多长时间后就销毁重新建立新连接。

不同的客户端配置项不一样,需要参考对应的文档进行配置。

例如:Django 是通过 CONN_MAX_AGE 来设定连接的 https://docs.djangoproject.com/en/5.1/ref/settings/#conn-max-age

当客户端设定的时间比服务器端长的时候,服务器会先关闭连接,这时客户端还认为连接可用,仍然使用该连接进行查询,就会产生断连的异常。

解决方案#

SqlAlchemy 文档中很好的描述了应对数据库断连的几种方法 https://docs.sqlalchemy.org/en/20/core/pooling.html#dealing-with-disconnects

正确设置客户端超时时间#

将客户端超时时间设置为小于服务器端的超时时间,避免出现两端不一致的情况。

健康检测#

成熟的客户端会有配置,在连接复用之前,先检测一下是否可用。

Django 的连接健康检测默认是关闭的 https://docs.djangoproject.com/en/5.1/ref/settings/#conn-health-checks

SqlAlchemy 可用通过设置 Pool.pre_ping 参数开启健康检测 https://docs.sqlalchemy.org/en/20/core/pooling.html#disconnect-handling-pessimistic

失败重试#

另一种方案就是在遇到连接断开时,重新建立连接并重新进行请求。

下面代码是从网上拷贝的一个简单的 Django 进行失败重试的代码:

from django.db.backends.mysql import base


def check_mysql_gone_away(db_wrapper):
    def decorate(func):
        def wrapper(self, query, args=None):
            try:
                return func(self, query, args)
            except (base.Database.OperationalError, base.Database.InterfaceError) as e:
                if 'MySQL server has gone away' in str(e)\
                        or 'Lost connection' in str(e)\
                        or type(e) == base.Database.InterfaceError:
                    db_wrapper.connection.close()
                    db_wrapper.connect()
                    self.cursor = db_wrapper.connection.cursor()
                    return func(self, query, args)
                # Map some error codes to IntegrityError, since they seem to be
                # misclassified and Django would prefer the more logical place.
                if e.args[0] in self.codes_for_integrityerror:
                    raise base.utils.IntegrityError(*tuple(e.args))
                raise
        return wrapper

    return decorate


class DatabaseWrapper(base.DatabaseWrapper):

    def create_cursor(self, name=None):

        class CursorWrapper(base.CursorWrapper):

            @check_mysql_gone_away(self)
            def execute(self, query, args=None):
                return self.cursor.execute(query, args)

            @check_mysql_gone_away(self)
            def executemany(self, query, args):
                return self.cursor.executemany(query, args)

        cursor = self.connection.cursor()
        return CursorWrapper(cursor)

请求包过大#

MySql 有一个最大包的限制,当请求包超过这个值的时候,MySql 服务器端也会主动关闭连接。

官方文档:https://dev.mysql.com/doc/refman/5.7/en/packet-too-large.html

遇到这种情况需要调整客户端和服务器端的配置相匹配,健康检查和重试都无法解决这个问题。

comments powered by Disqus