1. Python 连 MySQL 总踩坑?先理清 mysql-connector 与 SQLAlchemy 的完整链路
如果你写过 Python 操作 MySQL,大概率经历过这样的场景:本地脚本跑得好好的,一放到服务器上就报Lost connection to MySQL server during query;或者查询结果里中文变成一堆问号;再或者连接数越跑越多,最后 MySQL 直接拒绝新连接。这些问题表面看是数据库的锅,实际上八成出在 Python 这一侧的连接管理上。
Python 与 MySQL 交互这件事,核心就两条路:一条是用mysql-connector-python或pymysql这类驱动直接写 SQL,另一条是用 SQLAlchemy 做 ORM 或 Core 层的封装。前者轻量、可控,适合脚本和小工具;后者工程化程度高,适合中大型项目。但不管走哪条路,你都得面对同一批问题:连接怎么建、连接池怎么配、参数怎么传才安全、断了怎么重试。
这篇内容聚焦的就是这条完整链路。我会从最基础的连接对象讲起,把mysql-connector和 SQLAlchemy 两条路都跑一遍,给出可以直接复制的连接配置片段,再补上连接池参数、参数化查询写法和异常重试逻辑。最后用一个本地查询验证步骤收尾,确保你照着敲完就能跑通读写流程。
适合谁看?如果你正在写数据同步脚本、做后台服务的数据层、或者单纯想把 Python 和 MySQL 的交互搞扎实,这篇都能用上。不需要你懂多少数据库内核,但至少要能跑 Python、装过 pip 包、知道 MySQL 的基本概念。
另外提一句,很多人在本地开发时习惯把数据库密码、API Key 这类敏感信息硬编码在脚本里,这在团队协作里是个隐患。后面我会顺带讲一下怎么用统一的 Key 管理方式把这些配置收拢起来,避免每个脚本都散落一份凭证。
先把环境准备好。你需要一个能连上的 MySQL 实例,本地装一个也行,Docker 起一个也行。Python 版本建议 3.8 以上,pip 能正常用。下面所有代码我都实测过,你可以直接复制。
2. 前置准备:装驱动、建库建表、把 TaoToken 统一 Key 接进来
动手之前先把依赖装齐。走驱动直连这条路,装mysql-connector-python;走 SQLAlchemy 这条路,除了 SQLAlchemy 本身,还要装一个底层驱动,我习惯用pymysql,因为它纯 Python 实现,装起来不挑环境。
pip install mysql-connector-python pip install sqlalchemy pymysql装完之后验证一下版本,避免装到太老的包导致 API 对不上:
python -c "import mysql.connector; print(mysql.connector.__version__)" python -c "import sqlalchemy; print(sqlalchemy.__version__)"接下来建一个测试库和一张表。我用demo_db作为库名,建一张users表,字段包含自增主键、用户名、邮箱和创建时间。你可以在 MySQL 客户端里执行:
CREATE DATABASE IF NOT EXISTS demo_db DEFAULT CHARSET utf8mb4; USE demo_db; CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(64) NOT NULL, email VARCHAR(128) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意这里字符集用的是utf8mb4,不是utf8。MySQL 里的utf8其实是残缺的三字节实现,存 emoji 或者某些生僻字会出问题,utf8mb4才是真正的四字节 UTF-8。这个坑我在早期项目里踩过,查询结果里中文变问号,排查半天才发现是字符集没对齐。
现在说配置管理这块。传统写法是把 host、user、password 直接写在代码里,但这样有几个问题:一是换环境要改代码,二是密码容易泄露到版本库,三是多个脚本各写一份,改起来到处找。我的做法是把这些连接信息抽成一份配置,敏感凭证走统一入口管理。
TaoToken 提供了一套统一的 Key 管理能力,你可以把它理解成一个集中存放凭证和模型访问入口的地方。它的 API 地址是https://taotoken.net/api,官网在https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。如果你后面还要接大模型做数据处理、SQL 生成之类的辅助功能,用同一套 Key 就能打通,不用每个服务单独申请。
具体到配置,我建议建一个config.py,把数据库连接参数和统一 Key 都放进去,用环境变量兜底:
import os DB_CONFIG = { "host": os.getenv("DB_HOST", "127.0.0.1"), "port": int(os.getenv("DB_PORT", 3306)), "user": os.getenv("DB_USER", "root"), "password": os.getenv("DB_PASSWORD", "your_password"), "database": os.getenv("DB_NAME", "demo_db"), "charset": "utf8mb4", } TAOTOKEN_API_BASE = os.getenv("TAOTOKEN_API_BASE", "https://taotoken.net/api") TAOTOKEN_API_KEY = os.getenv("TAOTOKEN_API_KEY", "")这样本地开发时可以用默认值,上线时通过环境变量覆盖,密码不会进代码库。统一 Key 也是同样的思路,一个变量管住所有需要鉴权的调用。
如果你还没有 Key,可以去控制台创建,地址是https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite。创建完把 Key 填到环境变量里就行,别直接写死在脚本中。
3. 可复制配置:mysql-connector 直连与 SQLAlchemy 连接池写法
这一节给你两份可以直接用的配置,一份是mysql-connector的直连写法,一份是 SQLAlchemy 的连接池写法。你可以根据项目规模选。
先看mysql-connector的直连。它适合脚本类任务,连接用完就关,逻辑简单直接:
import mysql.connector from mysql.connector import Error from config import DB_CONFIG def get_connection(): try: conn = mysql.connector.connect( host=DB_CONFIG["host"], port=DB_CONFIG["port"], user=DB_CONFIG["user"], password=DB_CONFIG["password"], database=DB_CONFIG["database"], charset=DB_CONFIG["charset"], connection_timeout=10, autocommit=False, ) return conn except Error as e: print(f"连接失败: {e}") raise这里有几个参数值得说。connection_timeout=10是连接超时,默认值偏长,网络抖动时容易卡住,设成 10 秒比较合适。autocommit=False是手动提交模式,写操作需要显式commit(),这样出错了可以回滚,数据一致性更有保障。
再看 SQLAlchemy 的连接池写法。这是工程里更常用的方式,因为它自带连接池,能复用连接、控制并发、自动回收失效连接:
from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from config import DB_CONFIG DB_URL = ( f"mysql+pymysql://{DB_CONFIG['user']}:{DB_CONFIG['password']}" f"@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}" f"?charset={DB_CONFIG['charset']}" ) engine = create_engine( DB_URL, pool_size=10, max_overflow=20, pool_recycle=1800, pool_pre_ping=True, echo=False, ) SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)这段配置里的参数是重点,我逐个解释:
| 参数 | 作用 | 建议值 |
|---|---|---|
| pool_size | 连接池常驻连接数 | 5–20,按并发量调 |
| max_overflow | 超出常驻后允许临时创建的连接数 | pool_size 的 1–2 倍 |
| pool_recycle | 连接回收时间(秒),防止 MySQL 主动断开 | 1800,小于 wait_timeout |
| pool_pre_ping | 取连接前先 ping 一次,剔除失效连接 | True |
pool_recycle这个参数特别关键。MySQL 默认的wait_timeout是 28800 秒(8 小时),但很多云数据库会调短,比如 600 秒。如果你的连接池里躺着一个已经被服务端断开的连接,下次取出来用就会报Lost connection。把pool_recycle设成比wait_timeout小,就能在连接失效前主动回收重建。pool_pre_ping=True是第二道保险,取连接时先探活,失效的直接换掉。
如果你用的是mysql-connector自带的连接池,也可以配,但功能没 SQLAlchemy 这么全:
from mysql.connector import pooling pool = pooling.MySQLConnectionPool( pool_name="demo_pool", pool_size=5, pool_reset_session=True, host=DB_CONFIG["host"], port=DB_CONFIG["port"], user=DB_CONFIG["user"], password=DB_CONFIG["password"], database=DB_CONFIG["database"], charset=DB_CONFIG["charset"], )pool_reset_session=True会在连接归还时重置会话状态,避免上一个使用者的事务或临时变量影响下一个。这个选项在共享连接池的场景下建议打开。
配置写好后,把config.py和连接代码放在同一目录,确保DB_CONFIG能正常导入。如果你用 TaoToken 的统一 Key 做后续的模型调用,把TAOTOKEN_API_KEY也填进环境变量,这样数据库和模型两条链路的凭证就都收拢好了。
4. 参数化查询与异常重试:跑通一次完整的读写验证
配置就绪后,来跑一次完整的读写流程。这一步我会把参数化查询、事务提交、异常重试都串起来,你可以直接复制运行。
先看参数化查询。很多人图省事用字符串拼接 SQL,比如f"SELECT * FROM users WHERE username='{name}'",这是 SQL 注入的温床。正确做法是用占位符,让驱动去处理转义:
def insert_user(conn, username, email): cursor = conn.cursor() sql = "INSERT INTO users (username, email) VALUES (%s, %s)" cursor.execute(sql, (username, email)) conn.commit() cursor.close() return cursor.lastrowid注意mysql-connector和pymysql用的都是%s占位符,不是?。SQLAlchemy 的 Core 层也是%s风格,但 ORM 层直接用对象操作,不写 SQL。
查询用fetchone()取单条,fetchall()取全部:
def query_users(conn): cursor = conn.cursor(dictionary=True) cursor.execute("SELECT id, username, email FROM users ORDER BY id DESC LIMIT 10") rows = cursor.fetchall() cursor.close() return rowsdictionary=True让结果以字典返回,字段名做 key,比元组好读。这个参数在mysql-connector里支持,pymysql要用DictCursor。
现在加上异常重试。数据库操作最常见的两类异常是连接断开和死锁,前者重连即可,后者需要回滚重试。我写一个带退避的重试装饰器:
import time from functools import wraps from mysql.connector import Error def retry_on_failure(max_retries=3, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exc = None for attempt in range(max_retries): try: return func(*args, **kwargs) except Error as e: last_exc = e print(f"第 {attempt + 1} 次失败: {e}") time.sleep(delay * (attempt + 1)) raise last_exc return wrapper return decorator退避时间用delay * (attempt + 1),第一次等 1 秒,第二次 2 秒,第三次 3 秒,避免密集重试把数据库压垮。
把上面拼起来,写一个完整的验证脚本:
from config import DB_CONFIG import mysql.connector @retry_on_failure(max_retries=3, delay=1) def run_demo(): conn = mysql.connector.connect(**DB_CONFIG) try: uid = insert_user(conn, "alice", "alice@example.com") print(f"插入成功, id={uid}") rows = query_users(conn) for row in rows: print(row) finally: conn.close() if __name__ == "__main__": run_demo()运行后你应该看到类似输出:
插入成功, id=1 {'id': 1, 'username': 'alice', 'email': 'alice@example.com'}如果这一步跑通了,说明连接、写入、查询、关闭整条链路都正常。SQLAlchemy 版本的验证逻辑一样,只是用SessionLocal()拿会话,操作完session.commit()再session.close()。
再补一个 SQLAlchemy 的查询示例,方便你对照:
from sqlalchemy import text from db import SessionLocal def query_with_sqlalchemy(): session = SessionLocal() try: result = session.execute( text("SELECT id, username FROM users WHERE username = :name"), {"name": "alice"} ) for row in result: print(row.id, row.username) finally: session.close()注意 SQLAlchemy 的text()用的是:name命名占位符,和驱动层的%s不一样,别混用。
5. 常见报错排查:401、local proxy failed、reading choices 与 OAuth 问题
链路跑通不代表以后不出问题。这一节我把实际遇到过的几类报错整理出来,对照着排查能省不少时间。
报错一:Access denied for user 'root'@'localhost' (using password: YES)
这是认证失败,原因通常是密码错、用户没授权、或者 host 不匹配。先确认密码对不对,再检查用户权限:
SELECT user, host FROM mysql.user WHERE user = 'root';如果 host 是localhost但你从127.0.0.1连,可能匹配不上。MySQL 里localhost走 socket,127.0.0.1走 TCP,两者是不同条目。要么改成%允许任意 host,要么建一个'root'@'127.0.0.1'的用户。
报错二:Lost connection to MySQL server during query
这个前面提过,多半是连接被服务端断开了。检查wait_timeout:
SHOW VARIABLES LIKE 'wait_timeout';如果值很小(比如 600),把连接池的pool_recycle设成比它小,同时开pool_pre_ping=True。另外网络中间设备(防火墙、负载均衡)也可能掐连接,这种情况只能靠探活和重连兜底。
报错三:401 Unauthorized或local proxy failed
这两个通常出现在你调用外部 API 的时候,比如用统一 Key 访问模型服务。401 是 Key 无效或过期,去控制台确认 Key 状态,地址是https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite。local proxy failed一般是本地网络配置问题,检查你的请求地址是不是写成了https://taotoken.net/api,别多加路径或斜杠。
报错四:Error reading choices或返回结构解析失败
这类报错说明请求发出去了,但响应体不符合预期。常见原因是请求头没带对,或者模型 ID 写错了。如果你在代码里同时用了数据库和模型调用,确认两套配置没串。模型 ID 要和你申请的服务对得上,别拿 A 服务的 Key 去调 B 服务的模型。
报错五:OAuth 相关报错
如果你用的是需要 OAuth 授权的客户端工具,报错通常出在回调地址或 token 刷新上。检查回调地址是否和申请时填的一致,token 过期就重新授权。这类问题在 Claude Code 之类的工具接入时比较常见,配置项要填全:Base URL、Key、Model ID 三件套缺一不可。
排查这类问题的通用思路是:先看报错原文,定位是网络层、认证层还是数据层;再用最小复现脚本单独测那一层;最后对照配置逐项核对。别一上来就改代码,很多时候是配置写错了。
6. 把数据库链路和统一 Key 收拢到一套配置里
走到这里,你应该已经能跑通 Python 到 MySQL 的完整读写了。回顾一下这条链路的关键点:连接要配超时和字符集,连接池要设回收和探活,查询要用参数化占位符,异常要有退避重试。这四件事做到位,大部分生产环境的坑都能避开。
我自己的习惯是把数据库配置和外部服务的 Key 放在同一份配置体系里管理。数据库走DB_CONFIG,模型调用走TAOTOKEN_API_BASE和TAOTOKEN_API_KEY,两者都从环境变量读,本地用默认值兜底。这样换环境只改环境变量,代码一行不动。
如果你后面要做数据清洗、SQL 生成、或者用模型辅助分析查询结果,可以直接用同一套 Key 去调模型对话接口,地址是https://taotoken.net/api,对话入口在https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite。数据库和模型两条链路共用一套凭证,维护成本会低很多。
要是你打算把这类数据任务做成长期跑的编码项目或者 Agent,可以考虑 Coding Plan,入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite,适合需要持续调用和批量处理的场景。
最后留一个实用技巧:把连接池的健康状态打点出来,定期看pool_size和实际使用量的差距。如果长期打满,说明并发上来了要扩容;如果长期空闲,说明配置偏大可以缩。这个指标比事后排查连接泄漏要主动得多。