mysql.connector.errors.OperationalError: 1040 (08004): Too many connections

从报错截图来看,核心错误信息是:
mysql.connector.errors.OperationalError: 1040 (08004): Too many connections

这意味着你的 Python 程序(具体是在 Streamlit 框架下运行)向 MySQL 数据库发起了过多的连接请求,超出了数据库允许的最大并发连接数(默认为 151),并且之前的连接没有被及时关闭

为什么会发生这种情况?

  1. 连接未关闭(Connection Leak): 代码中打开了数据库连接(connect()),但在使用完后没有调用 conn.close()
  2. Streamlit 的运行机制: 我注意到报错路径中有 streamlit。Streamlit 的特点是每当用户与页面交互(点击按钮、刷新等)时,整个 Python 脚本会从头到尾重新运行一次
    • 看你的代码位置:utils\scm_db.py 第 9 行直接执行了 conn = mysql.connector.connect(...)
    • 每次页面刷新,这行代码就运行一次,创建一个新连接。
    • 旧连接还在占用资源,新连接不断增加,很快就耗尽了 MySQL 的连接池。

解决方案

方案一:使用 Streamlit 的缓存机制(推荐,最适合你的场景)

在 Streamlit 中,数据库连接应该被缓存,确保整个应用只创建一次连接,而不是每次刷新都创建。

修改 utils/scm_db.py

python 复制代码
import mysql.connector
import streamlit as st
# 你的 Settings 导入

# 使用 @st.cache_resource 装饰器,确保连接只被创建一次
@st.cache_resource
def init_connection():
    return mysql.connector.connect(
        host=Settings.DB_HOST,
        user=Settings.DB_USER,
        password=Settings.DB_PASSWORD,
        database=Settings.DB_NAME
    )

# 在其他文件中调用时:
# conn = init_connection()

注意:@st.cache_resource 是 Streamlit 新版的方法,旧版可能叫 @st.experimental_singleton

方案二:确保连接即用即关(标准做法)

如果你不需要长连接,应当在查完数据后立刻关闭连接。最好使用 try...finally 或者上下文管理器。

python 复制代码
# 修改你的调用逻辑
def get_data():
    conn = None
    cursor = None
    try:
        conn = mysql.connector.connect(host=..., ...) # 建立连接
        cursor = conn.cursor()
        
        # 执行查询
        cursor.execute("SELECT * FROM ...")
        result = cursor.fetchall()
        return result
        
    except Exception as e:
        print(e)
    finally:
        # 无论是否报错,这里都会执行,确保关闭连接
        if cursor:
            cursor.close()
        if conn:
            conn.close()
方案三:临时解决(重启服务)

为了立刻让程序能跑起来,你需要清除当前占满的"僵尸"连接:

  1. 重启 MySQL 服务 :这会强制断开所有现有连接。
    • Windows: 在"服务"中找到 MySQL 并重启。
    • Linux: sudo service mysql restart
  2. 重启 Streamlit 应用:关闭终端里的 Streamlit 进程并重新运行。
方案四:增加 MySQL 最大连接数(治标不治本)

如果你的应用确实需要很高并发(通常开发环境不需要),可以在 MySQL 中修改配置。

  1. 登录 MySQL 命令行。
  2. 执行命令查看当前限制:SHOW VARIABLES LIKE 'max_connections';
  3. 临时修改限制(例如改为 1000):SET GLOBAL max_connections = 1000;

总结

问题出在 Streamlit 每次刷新页面都重新执行了连接数据库的代码,且没有关闭旧连接 。请优先采用方案一进行代码优化。

这是一个非常好的问题,触及了 SQLAlchemy(通常配合 pandas 使用)的核心机制------连接池(Connection Pooling)

简单直接的回答是:

  1. 算一次连接吗? 是的,它会占用或者建立一个与数据库的会话。
  2. 如何关闭? 你不需要(也不能)手动关闭它 。只要你传给 read_sql 的是 engine,pandas 会自动处理"借出连接"和"归还连接"的操作。

但结合你之前遇到的 Too many connections 报错,这里有很深的"坑",请仔细看下面的分析:


一、 pd.read_sql 的内部机制

当你执行如下代码时:

python 复制代码
import pandas as pd
from sqlalchemy import create_engine

# 假设 engine 已经创建好
df = pd.read_sql("SELECT * FROM table", engine)

Pandas 内部其实做了以下事情:

  1. 向 Engine 申请连接 :它调用 engine.connect()
  2. 连接池机制
    • 如果连接池里有空闲连接,Engine 直接把旧连接拿来用(速度快,MySQL 看来连接数没变)。
    • 如果连接池是空的,Engine 会创建一个新的物理连接 (MySQL 的 Threads_connected +1)。
  3. 执行 SQL:利用这个连接读取数据。
  4. 自动关闭(归还) :数据读取完毕后,Pandas 会自动调用连接的 .close() 方法。

关键点来了:

对于 SQLAlchemy 的 Engine 来说,.close() 并不代表断开 TCP 连接 ,而是代表**"把连接还给连接池"
所以在 MySQL 服务端看来,这个连接依然是
连着(Sleep)**的状态,等待下一次被复用。

二、 既然自动关闭,为什么还会报错?

既然 read_sql 会自动归还连接,为什么你还会遇到连接数过多的报错?

罪魁祸首还是 Streamlit 的刷新机制。

如果你的代码是这样写的(在 Streamlit 中):

python 复制代码
# 错误写法:每次刷新页面都会运行这几行
engine = create_engine("mysql+mysqlconnector://user:pass@host/db") 
df = pd.read_sql("SELECT * FROM table", engine)

发生了什么?

  1. 第 1 次刷新 :创建了 Engine对象_A(自带一个连接池)。read_sql 建立了一个连接(Conn_1)。用完后,Conn_1 回到了 Engine对象_A 的池子里。
  2. 第 2 次刷新 :脚本重新运行,创建了 Engine对象_B(自带 的连接池)。read_sql 建立了一个连接(Conn_2)。用完后,Conn_2 回到了 Engine对象_B 的池子里。
    • 问题Engine对象_A 虽然被 Python 变量抛弃了,但它建立的 Conn_1 还在 MySQL 那边挂着!直到 Python 进行垃圾回收(GC)或者 MySQL 超时,这个连接才会断。
  3. 第 1000 次刷新 :你创建了 1000 个 Engine 对象,MySQL 里挂了 1000 个 Sleep 的连接 ------ BOOM! Too many connections.

三、 正确的解决方案

你要做的不是"手动关闭连接",而是**"防止重复创建 Engine"。你需要让整个 Streamlit 应用无论刷新多少次,全局只使用同一个** Engine 对象。

写法 1:使用 Streamlit 缓存 Engine(强烈推荐)

修改你的 utils/scm_db.py 或相关文件:

python 复制代码
import pandas as pd
from sqlalchemy import create_engine
import streamlit as st

# 使用 @st.cache_resource 装饰器
# 这告诉 Streamlit:这个函数的返回值(engine)要存起来(缓存),
# 下次再调用这个函数,直接返回存起来的那个,不要重新执行内部代码。
@st.cache_resource
def get_db_engine():
    # 这里填写你的连接字符串
    engine = create_engine("mysql+mysqlconnector://root:password@host:3306/db_name")
    return engine

# 在主程序中使用
def main():
    # 获取唯一的 engine 实例
    engine = get_db_engine()
    
    # 放心读取,由 Engine 自动管理连接池
    df = pd.read_sql("SELECT * FROM my_table", engine)
    st.dataframe(df)
写法 2:如果不使用缓存(不推荐),如何强制销毁?

如果你实在不想用缓存(通常不建议),你必须显式地销毁 Engine 以断开物理连接:

python 复制代码
engine = create_engine(...)
try:
    df = pd.read_sql(sql, engine)
finally:
    # 强制销毁 Engine,这会切断池子里所有的物理连接
    engine.dispose() 

总结

  1. pd.read_sql(sql, engine) 会自动处理连接的开启和关闭 (归还给池子),你不需要手动写 conn.close()
  2. 你的报错是因为重复创建了太多 Engine 对象,每个对象后面都拖着一堆空闲连接没断开。
  3. 解决方法 :使用 @st.cache_resource 缓存 create_engine 的结果,确保全局只有一个 Engine。
相关推荐
Railshiqian5 小时前
通过adb命令获取某个window或View/子View的绘制内容并输出为png图片的方法
android·adb·dump view
数据知道5 小时前
一文掌握向量数据库Chroma的详细使用
数据库·python·向量数据库
虹科网络安全5 小时前
艾体宝洞察 | Redis vs Valkey:解决 ElastiCache 的无序扩张与资源效率问题
数据库·redis·spring
weixin_439706255 小时前
Windows MySQL的主从复制配置记录
windows·mysql·adb
xu_ws5 小时前
2G服务器优化MySQL内存配置指南
数据库·mysql
TG:@yunlaoda360 云老大5 小时前
华为云国际站代理商的ESW主要有什么作用呢?
网络·数据库·华为云
漂亮的小碎步丶6 小时前
【8】分库分表与百亿级话单数据处理详解
数据库
计算机毕设指导66 小时前
基于微信小程序+django连锁火锅智慧餐饮管理系统【源码文末联系】
java·后端·python·mysql·微信小程序·小程序·django
风月歌6 小时前
php医院预约挂号系统小程序源代码(源码+文档+数据库)
数据库·微信小程序·小程序·毕业设计·php·源码