从报错截图来看,核心错误信息是:
mysql.connector.errors.OperationalError: 1040 (08004): Too many connections
这意味着你的 Python 程序(具体是在 Streamlit 框架下运行)向 MySQL 数据库发起了过多的连接请求,超出了数据库允许的最大并发连接数(默认为 151),并且之前的连接没有被及时关闭。
为什么会发生这种情况?
- 连接未关闭(Connection Leak): 代码中打开了数据库连接(
connect()),但在使用完后没有调用conn.close()。 - 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()
方案三:临时解决(重启服务)
为了立刻让程序能跑起来,你需要清除当前占满的"僵尸"连接:
- 重启 MySQL 服务 :这会强制断开所有现有连接。
- Windows: 在"服务"中找到 MySQL 并重启。
- Linux:
sudo service mysql restart
- 重启 Streamlit 应用:关闭终端里的 Streamlit 进程并重新运行。
方案四:增加 MySQL 最大连接数(治标不治本)
如果你的应用确实需要很高并发(通常开发环境不需要),可以在 MySQL 中修改配置。
- 登录 MySQL 命令行。
- 执行命令查看当前限制:
SHOW VARIABLES LIKE 'max_connections'; - 临时修改限制(例如改为 1000):
SET GLOBAL max_connections = 1000;
总结
问题出在 Streamlit 每次刷新页面都重新执行了连接数据库的代码,且没有关闭旧连接 。请优先采用方案一进行代码优化。
这是一个非常好的问题,触及了 SQLAlchemy(通常配合 pandas 使用)的核心机制------连接池(Connection Pooling)。
简单直接的回答是:
- 算一次连接吗? 是的,它会占用或者建立一个与数据库的会话。
- 如何关闭? 你不需要(也不能)手动关闭它 。只要你传给
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 内部其实做了以下事情:
- 向 Engine 申请连接 :它调用
engine.connect()。 - 连接池机制 :
- 如果连接池里有空闲连接,Engine 直接把旧连接拿来用(速度快,MySQL 看来连接数没变)。
- 如果连接池是空的,Engine 会创建一个新的物理连接 (MySQL 的
Threads_connected+1)。
- 执行 SQL:利用这个连接读取数据。
- 自动关闭(归还) :数据读取完毕后,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 次刷新 :创建了
Engine对象_A(自带一个连接池)。read_sql建立了一个连接(Conn_1)。用完后,Conn_1 回到了Engine对象_A的池子里。 - 第 2 次刷新 :脚本重新运行,创建了
Engine对象_B(自带新 的连接池)。read_sql建立了一个连接(Conn_2)。用完后,Conn_2 回到了Engine对象_B的池子里。- 问题 :
Engine对象_A虽然被 Python 变量抛弃了,但它建立的 Conn_1 还在 MySQL 那边挂着!直到 Python 进行垃圾回收(GC)或者 MySQL 超时,这个连接才会断。
- 问题 :
- 第 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()
总结
pd.read_sql(sql, engine)会自动处理连接的开启和关闭 (归还给池子),你不需要手动写conn.close()。- 你的报错是因为重复创建了太多 Engine 对象,每个对象后面都拖着一堆空闲连接没断开。
- 解决方法 :使用
@st.cache_resource缓存create_engine的结果,确保全局只有一个 Engine。