Django中解决MySQL连接丢失(MySQL server has gone away)的有效方案
本文深入分析Django在使用MySQL时出现的“MySQL server has gone away”错误,指出其根源在于连接空闲时间超过MySQL的wait_timeout。通过对比CONN_MAX_AGE配置与源码机制,揭示Django在单次请求内复用连接导致的问题,并提供调用django.db.close_old_connections()作为即时解决方案,同时探讨了异步处理等根本性优化思路。
问题
在使用Django框架操作MySQL数据库时,开发者偶尔会遭遇数据库连接意外断开的情况。这类错误通常表现为以下两种具体形式:
OperationalError: (2006, 'MySQL server has gone away') OperationalError: (2013, 'Lost connection to MySQL server during query')
通过执行SQL命令SHOW GLOBAL VARIABLES;查询MySQL的全局变量,可以观察到wait_timeout这一参数。该参数定义了数据库连接在空闲状态下的最大存活时间。如果客户端复用同一个连接进行多次数据库查询,只要查询操作连续进行,连接通常不会断开;但如果连续查询之间停顿的时间超过了wait_timeout设定的阈值,再次发起查询时就会触发数据库连接丢失的错误。
复现
接下来,我们将通过Django环境来复现这一连接丢失的问题:
首先将MySQL的wait_timeout参数设置为10秒,随后进入Django的Shell交互环境进行模拟查询(以下错误日志仅保留了关键部分):
In[1]:import time In[2]:from django.contrib.auth.models import User In[3]:list(User.objects.filter(id=1)) Out[3]:[<User: admin>] In[4]:time.sleep(15) # 模拟耗时较长的代码逻辑(期间无数据库查询操作),或者单纯等待一段时间,该等待时间需大于`wait_timeout`的值 list(User.objects.filter(id=1)) Traceback (most recent call last):
File "<ipython-input-4-3574ae8220ee>", line 1, in <module> list(User.objects.filter(id=1))
File "/usr/lib/python3.6/site-packages/pymysql/connections.py", line 1037, in _read_bytes CR.CR_SERVER_LOST, "Lost connection to MySQL server during query") django.db.utils.OperationalError: (2013, 'Lost connection to MySQL server during query')
寻求
上述现象基本证实了错误是由连接空闲时间过长导致的。
Django为了减少频繁创建和关闭数据库连接带来的开销,采用了连接复用机制。当发起一个请求时,Django会建立一个连接池来存放连接,并在该请求的生命周期内复用同一个连接。由此推测,Django保存连接的时间可能超过了MySQL的wait_timeout设置。如果能让Django在连接即将失效前重新建立连接,即可避免此类错误。官方文档确实提到了此问题,并建议设置数据库配置中的CONN_MAX_AGE参数,示例如下:
DATABASES = {
"default": {
'ENGINE': 'django.db.backends.mysql',
'NAME': '',
'USER': '',
'PASSWORD': '',
'HOST': '',
'CONN_MAX_AGE': 9 # 比wait_timeout小一些
}
}
当我们测试后却发现,事情并非想想中那么简单。为何错误依旧出现?这一切的背后, 是人性的扭曲还是道德的沦丧?敬请收看下节《突破》。
突破
深入搜索Django源码中关于CONN_MAX_AGE的处理逻辑,顺藤摸瓜可以发现Django关闭失效连接的核心方法django.db.close_old_connections():
# Register an event to reset transaction state and close connections past # their lifetime. def close_old_connections(**kwargs): for conn in connections.all(): conn.close_if_unusable_or_obsolete() signals.request_started.connect(close_old_connections) signals.request_finished.connect(close_old_connections)
重点在最后两行,通过signal实现特定事件时执行此方法,两个特定事件顾名思义是请求开始和请求结束。而我们报错的是在一次请求中,所以此法通常无效,仅仅是实现每个请求关闭并重新建立连接。
解决
保持之前复现问题的Django Shell会话不关闭,继续执行以下代码:
In[5]:from django.db import close_old_connections In[6]:close_old_connections() In[7]:list(User.objects.filter(id=1)) Out[7]: [<User: admin>]
调用django.db.close_old_connections后再次查询就没有错误了。 那么我们要避免此错误就要执行每个数据库查询前调用django.db.close_old_connections方法。
在常规应用场景下,这类问题并不常见,因为一个请求中通常是连续不间断地进行数据库查询,无需在每个请求中都调用此方法,这属于过度设计。
然而,在某些特定场景下,一个请求中涉及的数据量较大,流程可能是先查询数据库获取数据,随后进行一段较长时间的非数据库操作(如数据处理、生成Excel文件、保存文件并生成URL)。如果已知后续处理耗时非常长,那么在最终将生成的URL保存回数据库之前,最好先调用django.db.close_old_connections()以刷新连接,防止因超时导致连接丢失。
题外话 实际上②所述情况最好从根本上解决处理慢的问题,也可以换作异步处理,从根本上解决问题。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













