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。
相关推荐
产幻少年20 分钟前
redis位图
数据库·redis·缓存
短剑重铸之日1 小时前
《7天学会Redis》Day 4 - 高可用架构设计与实践
数据库·redis·缓存
NineData1 小时前
第三届数据库编程大赛-八强决赛成绩揭晓
数据库·算法·代码规范
難釋懷1 小时前
认识Redis
数据库·redis·缓存
超级种码2 小时前
Redis:Redis脚本
数据库·redis·缓存
想唱rap2 小时前
表的约束条件
linux·数据库·mysql·ubuntu·bash
超级种码2 小时前
Redis:Redis 命令详解
数据库·redis·bootstrap
千寻技术帮2 小时前
10341_基于Springboot的珠宝销售网站
spring boot·mysql·毕业设计·商城·珠宝商城
qq_401700412 小时前
Qt 事件处理机制
java·数据库·qt
Elastic 中国社区官方博客2 小时前
使用 jina-embeddings-v3 和 Elasticsearch 进行多语言搜索
大数据·数据库·人工智能·elasticsearch·搜索引擎·全文检索·jina