1. 为什么你的 PyMySQL 连接总是断:从一次线上事故说起
PyMySQL 是用纯 Python 写的 MySQL 客户端库,它把「连接」这件事抽象成一个Connection对象,你所有的查询、事务、游标都挂在这个对象上。适合谁?适合所有用 Python 直接操作 MySQL 的后端开发、数据脚本作者、定时任务维护者。它能做什么?一句话:让你用 Python 代码完成建连、执行 SQL、提交事务、回滚、关闭这一整套生命周期动作。
我见过太多项目把pymysql.connect()写成一个全局变量,然后跑着跑着就报(2006, "MySQL server has gone away")或者(2013, "Lost connection to MySQL server during query")。原因往往不是 MySQL 挂了,而是Connection对象的生命周期没管好:连接空闲超过wait_timeout被服务端单方面掐断,客户端却还以为连接活着,下一次查询直接炸。
这篇就围绕Connection对象本身来讲。不是泛泛介绍参数表,而是把「建连 → 游标 → 事务 → 复用 → 排障」这条链路拆开,给你能直接复制的代码。同时我会演示一个容易被忽略的点:多环境(开发/测试/生产)的数据库凭证怎么统一管理,避免把密码硬编码在connect()里。这里我用 TaoToken 的统一 Key/API 通道来托管凭证,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,后面第 2 节会讲清楚它和数据库连接的关系。
先明确一个认知:Connection不是「一次查询的工具」,而是一个有状态的会话。它维护着 socket、事务上下文、字符集、当前数据库、autocommit 标志。你把它当短连接用完就关,或者当长连接一直不 ping,都会出问题。理解它的状态机,才是稳健用法的起点。
2. TaoToken 前置:用统一通道托管多环境数据库凭证
在讲Connection参数之前,先解决一个现实问题:你的connect()里那串password="xxx"从哪来?很多人的做法是写死在代码里,或者塞进.env然后提交到仓库。前者改密码要改代码,后者是安全事故的常客。
我的做法是把数据库凭证这类敏感配置,通过 TaoToken 的统一 Key/API 通道来管理。TaoToken 提供的是一个统一的 API 入口,你可以把它理解成「凭证和模型/服务访问的统一网关」。它的 API 地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,是干净的接口根路径。
具体怎么和 PyMySQL 配合?思路是这样的:数据库的 host、user、password 这些不直接写在 Python 文件里,而是通过环境变量注入,环境变量的值来自你在 TaoToken 控制台配置的凭证。这样开发、测试、生产三套环境用同一份代码,只切换环境变量即可。
你需要先在 TaoToken 控制台创建 API Key,控制台入口在这里:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。创建好 Key 之后,在 API Keys 页面可以管理你的密钥:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
这里要澄清一个常见误解:TaoToken 不是数据库代理,它不会替你转发 MySQL 的 TCP 流量。它管的是「凭证的分发与访问控制」。你的 PyMySQL 依然直连你的 MySQL 实例,只是连接参数从统一通道取,而不是散落在代码各处。这样做的收益是:轮换密码时只改一处,审计时知道谁在什么时候取了哪套凭证。
如果你还想在写代码时随时验证模型或调试 SQL 生成逻辑,可以用模型对话入口:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。长期做编码和 Agent 任务的话,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
把凭证管理前置讲清楚,是因为后面所有Connection初始化代码都会引用这些环境变量。你先把这套通道搭好,再往下看配置,代码就能直接跑。
3. 可复制的 Connection 初始化配置:参数、游标与事务
现在进入正题。pymysql.connect()返回的就是一个Connection对象。它的构造参数很多,但生产环境真正需要你显式设置的,其实就那么几个。我先把一份可直接复制的配置给你,再逐项解释。
import os import pymysql from pymysql.cursors import DictCursor def get_connection(): conn = pymysql.connect( host=os.environ["DB_HOST"], port=int(os.environ.get("DB_PORT", 3306)), user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], database=os.environ["DB_NAME"], charset="utf8mb4", cursorclass=DictCursor, autocommit=False, connect_timeout=10, read_timeout=30, write_timeout=30, max_allowed_packet=16 * 1024 * 1024, ) return conn这份配置里有几个关键决策,我逐个说。
charset="utf8mb4"必须显式写。默认的charset=""会让 PyMySQL 用服务端默认字符集,很多老 MySQL 默认是latin1,存中文直接乱码。utf8mb4才能存 emoji 和四字节字符。
cursorclass=DictCursor让查询结果以字典返回,而不是元组。这样你写row["user_name"]而不是row[3],字段顺序变了也不怕。代价是内存略高,但可读性收益远大于此。
autocommit=False是重点。PyMySQL 默认就是False,意味着你必须显式commit()或rollback()。很多人以为执行完execute()数据就进库了,其实没有,连接一关就回滚。这个默认值是对的,它逼你思考事务边界。
connect_timeout=10是建连超时,取值 1 到 31536000。read_timeout和write_timeout是读写超时,默认None表示永不超时——这在生产环境是危险的,一个慢查询能把你的线程挂死。设成 30 秒是合理起点。
max_allowed_packet默认 16MB,主要影响LOAD DATA LOCAL INFILE这类大批量操作。如果你要导入大文件,调大它。
关于凭证,前面说的环境变量在这里体现为os.environ["DB_HOST"]等。你可以写一个.env文件配合python-dotenv加载,但.env本身不要提交到仓库。更规范的做法是通过 TaoToken 控制台下发,本地开发时用 CLI 拉取到环境变量。
如果你用配置文件而不是环境变量,可以写一份 TOML:
[mysql] host = "127.0.0.1" port = 3306 user = "app_user" database = "app_db" charset = "utf8mb4" connect_timeout = 10 read_timeout = 30 write_timeout = 30 autocommit = false然后读取:
import tomllib with open("config.toml", "rb") as f: cfg = tomllib.load(f)["mysql"] conn = pymysql.connect( host=cfg["host"], port=cfg["port"], user=cfg["user"], password=os.environ["DB_PASSWORD"], database=cfg["database"], charset=cfg["charset"], connect_timeout=cfg["connect_timeout"], read_timeout=cfg["read_timeout"], write_timeout=cfg["write_timeout"], autocommit=cfg["autocommit"], )注意密码依然走环境变量,配置文件里不放密码。这是「配置与密钥分离」的基本原则。
游标创建用conn.cursor(),不传参数就是默认Cursor,传DictCursor就是字典游标。你也可以在connect()里用cursorclass设全局默认,这样每次cursor()都自动是字典。
事务方面,conn.begin()显式开启事务,conn.commit()提交,conn.rollback()回滚。autocommit=False时,第一条 SQL 执行会自动开启事务,但显式begin()更清晰。
4. 验证请求与成功结果:一次完整的事务提交演示
配置写好了,得验证它真的能跑通。下面这段代码演示一个完整流程:建连、建表、插入、提交、查询、关闭。你可以直接复制运行。
import pymysql from pymysql.cursors import DictCursor conn = pymysql.connect( host="127.0.0.1", port=3306, user="root", password="your_password", database="test_db", charset="utf8mb4", cursorclass=DictCursor, autocommit=False, ) try: with conn.cursor() as cursor: cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 """) cursor.execute( "INSERT INTO users (name) VALUES (%s)", ("alice",) ) conn.commit() print("insert committed, lastrowid =", cursor.lastrowid) with conn.cursor() as cursor: cursor.execute("SELECT id, name, created_at FROM users WHERE name=%s", ("alice",)) row = cursor.fetchone() print("query result:", row) finally: conn.close()跑通后你会看到类似输出:
insert committed, lastrowid = 1 query result: {'id': 1, 'name': 'alice', 'created_at': datetime.datetime(2024, 5, 20, 10, 30, 0)}这里有几个细节值得说。with conn.cursor() as cursor会在块结束时自动关闭游标,但不会提交事务。所以conn.commit()必须显式调用,且要在游标块之外或之内都行,只要在close()之前。
cursor.lastrowid是刚插入行的自增 ID,只在 INSERT 后有效。cursor.rowcount是受影响行数,UPDATE 和 DELETE 后常用。
验证回滚也很重要。把conn.commit()换成conn.rollback(),再查一次,你会发现数据没进去。这就是事务的意义。
如果你想验证连接是否还活着,用conn.ping(reconnect=True)。它会检查服务器可用性,如果连接已断且reconnect=True,会自动重连。注意:重连后你之前的事务上下文会丢失,所以 ping 一般用在「取连接时」而不是「事务中间」。
conn.open属性返回布尔值,表示连接是否打开。conn.select_db("other_db")可以切换当前数据库,不用重连。
一个完整的成功验证应该覆盖:建连成功、DDL 执行、DML 执行、commit 生效、查询可见、rollback 生效、close 无异常。这七步都过了,你的Connection用法才算基本正确。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth
即使配置写对了,实际跑起来还是会撞到各种报错。这一节我把最常见的几类列出来,对照真实错误信息给排查路径。
报错一:pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'")
这是建连失败,不是认证失败。先确认 MySQL 在跑:systemctl status mysql或docker ps。再确认端口对:默认 3306,如果你改了端口,port参数要跟着改。如果 MySQL 在容器里,host不能写127.0.0.1,要写容器网络里的服务名或宿主 IP。
报错二:pymysql.err.OperationalError: (1045, "Access denied for user 'app_user'@'localhost'")
这是认证失败,等价于 HTTP 里的 401。密码错了,或者用户没有从该 host 连接的权限。MySQL 的权限是user@host粒度,app_user@localhost和app_user@%是两回事。检查SELECT user, host FROM mysql.user;。如果你用 TaoToken 管理凭证,确认拉取的是当前环境的那套,别把测试环境的密码用到生产。
报错三:pymysql.err.OperationalError: (2006, "MySQL server has gone away")
连接被服务端掐了。最常见原因是空闲时间超过wait_timeout(默认 28800 秒,8 小时)。解决方案有两个:一是用连接池,取连接时ping(reconnect=True);二是缩短连接生命周期,用完就关。长连接场景必须配 ping。
报错四:pymysql.err.InterfaceError: (0, "")
这个错误信息很空,通常是连接已关闭还在用。检查是不是在conn.close()之后又执行了 SQL,或者多线程共享了同一个Connection。PyMySQL 的Connection不是线程安全的,每个线程要有自己的连接。
报错五:local proxy failed或reading choices类错误
这类错误通常出现在你通过某个中间层访问服务时。如果你在用统一通道管理凭证,确认 API 地址写的是 https://taotoken.net/api ,不要多加路径或参数。reading choices往往意味着返回体不是预期的 JSON,可能是网络中断或认证头缺失。检查你的请求头里 Key 是否正确带上。
报错六:OAuth 相关错误
如果你用 OAuth 方式获取访问令牌,报错通常是 token 过期或 scope 不足。重新走一遍授权流程,确认 scope 包含了你需要的权限。令牌要存在环境变量里,不要硬编码。
报错七:RuntimeError: cryptography is required for sha256_password
MySQL 8 默认用caching_sha2_password认证插件,PyMySQL 需要cryptography包。pip install cryptography即可。或者把用户改成mysql_native_password,但不推荐,安全性差。
排查的通用思路:先看错误码,2003 是网络,1045 是认证,2006 是超时,2013 是查询中断。错误码定位了方向,再去查对应配置。
6. 语义一致的收尾:把 Connection 当成有状态会话去管理
写到这里,核心的东西都覆盖了。我想再强调一个观念:Connection对象不是无状态的函数调用,它是有状态的会话。它的状态包括 socket 连接、事务上下文、字符集、当前数据库、autocommit 标志。你每一次execute()都在这个会话里留下痕迹。
所以稳健用法的本质是:明确它的创建时机、使用边界、销毁时机。短任务用短连接,用完即关;长任务用连接池,取连接时 ping;事务要显式 commit 或 rollback;多线程不共享连接;凭证不硬编码。
如果你要把这套用法沉淀成团队规范,建议把连接参数抽成配置,凭证走统一通道。TaoToken 的接入文档在这里:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有完整的 Key 管理和 API 调用说明。Claude Code 相关的接入配置也可以参考:https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后给你一个实用技巧:在get_connection()里加一行日志,记录建连耗时和当前环境。线上出问题时,这行日志能帮你快速判断是建连慢还是查询慢。连接泄漏的排查也简单:在close()前后打点,统计打开和关闭的次数,差值持续增长就是泄漏。