ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python连接MySQL全指南:驱动选型、连接池与防坑实战

Python连接MySQL全指南:驱动选型、连接池与防坑实战 说实话我接触Python连MySQL这个主题最早是在帮一个朋友处理爬虫数据落库时踩了无数坑后来又在项目里反复查资料、翻源码才慢慢把一套连接、查询、防坑的路子理清楚。今天这篇就把我在实际开发中积累的完整方案整理出来从驱动选型到连接配置、增删改查、参数化防注入再到连接池和故障排查一条龙讲透。这套内容对应的场景很广你在写爬虫、做数据分析、搭Web后端或者给自动化脚本加个数据持久化功能基本都绕不开“Python脚本连接MySQL”这步。适合两类人看一类是刚看完Python语法、准备接触数据库的新手另一类是已经会写业务代码但没系统梳理过数据库连接的细节、总在半夜被线上连接数打爆的开发者。无论你是哪种这篇都尽量把每一步背后的原因讲清楚而不是丢一堆命令让你死记。1. 内容整体设计与思路拆解1.1 连接MySQL前必须想明白的三件事先说一句大实话Python连接MySQL没有“唯一的正确答案”不同项目阶段、不同性能要求选型差别很大。所以在动手敲代码之前我建议你先想清楚下面三件事。第一件事是驱动选型。Python官方推荐的驱动是mysql-connector-python社区和全球开发者用得最多的则是PyMySQL另外还有一条“进阶路径”MySQLdbPython 2时代遗留Python 3基本废弃。我在实际项目里默认选PyMySQL原因很朴素纯Python实现安装无编译依赖虚拟环境里一个pip install pymysql就完事再一个它在GitHub上迭代勤快遇到Bug修得也快。如果你的服务器是生产环境要求极致性能可以考虑mysql-connector-python底层用了MySQL的C扩展个别场景下握手速度会快一点。但就我实测绝大多数业务场景两者的差距远没有网上说得那么玄乎选一个你顺手的就好。第二件事是字符集编码。这是新手最容易踩的坑。MySQL服务端默认字符集可能是latin1或者你建表时没指定utf8mb4结果Python这边存个emoji直接报Incorrect string value。我的建议是连接参数里显式指定charsetutf8mb4同时建表语句里也用utf8mb4两种方式双保险。这个我在后面实操部分会再细说。第三件事是异常处理策略。很多刚入门的同事写数据库代码只盯着成功路径连接失败怎么办、SQL执行报错怎么办、连接用完了要不要关闭完全没有概念。结果脚本一跑崩数据库连接池直接被打满。这件事必须在动手前就有预案因为后续所有代码都是围绕“怎么稳定地原样执行、怎么优雅地回收连接”来设计的。1.2 为什么流程图和“一条龙脚本”不是好方案网上搜“Python连接MySQL”会搜到大量“复制粘贴就能跑”的脚本。这类脚本通常长这样连接、查询、打印、关连接最后还用print输出结果。它们在演示层面没问题但在真实项目中几乎不能用。原因有三个第一数据库连接是昂贵资源每次用完就丢频繁断开重连在高并发场景下是性能灾难第二SQL直接字符串拼接极易被注入第三没有任何日志和异常状态出问题了一个黑屏窗口什么也看不出来。所以这篇指南的整体设计会偏向项目实践而不是让你死记一个模板。我会先给你一套可以“抄作业”的基础连接代码再从这套代码出发讲清楚增删改查的完整姿势、为什么必须参数化、连接池怎么搭、常见报错怎么诊断。你照着走完一遍本质上不是背下来一个脚本而是把整个连接栈的脉络摸了一遍。2. 环境准备与驱动选型2.1 本机环境检查Python 和 MySQL 版本怎么搭配动手之前先在终端确认两件事Python版本、MySQL版本。Windows上我习惯用python --version和mysql --version前提是MySQL的bin目录加进了PATHmacOS/Linux则是python3 --version和mysql --version。这步别觉得多余版本匹配不同驱动包的支持情况会有细微差别比如PyMySQL从不挑Python大版本但mysql-connector-python偶尔会因为Python小版本太新没有预编译包导致无法安装。MySQL版本方面我建议尽量用官方长期支持版比如8.x系列因为8.x默认的认证插件是caching_sha2_password老版本的驱动如果不支持这个认证方式连接时会报Authentication plugin caching_sha2_password cannot be loaded。如果你只能用MySQL 5.7连接时的注意事项会不太一样后面常见问题里我会专门讲这个。还有个容易忽略的地方检查Python位数。Windows下如果你装的是32位Python而MySQL是64位个别驱动尤其是带C扩展的版本可能装不上。用python -c import platform; print(platform.architecture())看一眼省得后面浪费一晚上排查环境问题。2.2 驱动安装的两种姿势pip 与源码安装驱动安装看起来是一行命令但实际有几种姿势我按推荐顺序说一下。第一种常规环境直接用pip。pip install pymysql如果你想装官方驱动则是pip install mysql-connector-python这里有个细节mysql-connector和mysql-connector-python是两个不同的包前者是老版本遗留PyPI上的维护状态不太清晰后者才是Oracle官方支持的。新手直接认准带-python后缀的别装错了。第二种特殊网络环境下用国内镜像加速。如果你在的公司网络访问PyPI不稳定可以指定清华源pip install pymysql -i https://pypi.tuna.tsinghua.edu.cn/simple第三种是源码安装一般只有驱动有本地补丁需求时才会这么做正常业务开发不太需要这里不展开。安装完验证一下python -c import pymysql; print(pymysql.__version__)能正常输出版本号说明驱动已经就位。2.3 没有MySQL服务端怎么办Docker 5分钟拉起一个测试库很多时候你并不是没有MySQL而是开发机上没有装、或者装了但版本太老这时候我强烈推荐用Docker跑一个干净的MySQL实例。这一步对新手尤其友好因为你可以随便折腾启动参数写错了删掉容器重来就行不会污染本机环境。一个最小可用的启动命令是这样的docker run --name mysql-test -e MYSQL_ROOT_PASSWORD123456 -e MYSQL_DATABASEtest_db -p 3306:3306 -d mysql:8.0简单解释下参数--name是容器名字-e MYSQL_ROOT_PASSWORD指定root密码-e MYSQL_DATABASE会自动帮你创建一个空数据库-p 3306:3306是把宿主机的3306端口映射到容器的3306端口-d表示后台运行。跑起来之后用本机的任何MySQL客户端工具就能连上127.0.0.1:3306。这个方式有个好处就是后面讲连接池、讲错误排查时你可以放心大胆地调参数、模拟异常反正容器随时能推倒重来。我也建议你养成习惯测试环境和生产环境尽量保持MySQL大版本一致很多“本地好好的线上就挂了”的诡异问题其实就是版本差异带来的。3. 核心细节解析与连接参数深挖3.1 连接参数逐个拆解host、port、user、password、database、charset连接MySQL的关键就是构造一个连接对象而构造连接对象的关键是理解各个参数。很多人背代码背得很熟但换一个环境就懵因为根本没搞懂这些参数在干什么。以PyMySQL为例最核心的无非这几个hostMySQL服务端所在主机的IP或域名。本地开发常用localhost或127.0.0.1注意两者在某些系统上解析行为有细微差别localhost可能走IPv6建议直接用127.0.0.1。portMySQL监听端口默认3306。如果你Docker映射时改成别的端口比如3307:3306这里就必须填3307。user用户名默认至少有一个root。生产环境肯定不能用root连这点在权限设计里我会补充。password对应用户名的密码。database你要连接的具体数据库名不是MySQL服务器的名字。如果这里留空连接成功后会处于“没有选中任何数据库”的状态执行SQL时必须写全库名.表名。charset通信字符集推荐utf8mb4。为什么不是utf8因为utf8在MySQL里最多存3个字节的字符而utf8mb4支持完整的Unicode包括emoji和生僻字基本是当面容量问题。还有一个叫connect_timeout的参数经常被忽略但关键时刻救命它设置连接超时秒数。默认是10秒如果你连的数据库在公网、或经常网络抖动建议自己设置pymysql.connect( host127.0.0.1, port3306, userroot, password123456, databasetest_db, charsetutf8mb4, connect_timeout5 )3.2 权限设计为什么不建议用root连数据库说到user参数我多说一嘴和连接息息相关的权限问题。新手图省事连接配置里直接填root这在测试环境没问题但在真实项目里是个定时炸弹。一旦脚本被注入或者源码泄露攻击者拿到的就是最高权限。我的建议是给应用创建专用账号并只授予必要权限。比如一个只做增删改查的脚本可以这样创建用户CREATE USER app_user% IDENTIFIED BY your_password; GRANT SELECT, INSERT, UPDATE, DELETE ON test_db.* TO app_user%; FLUSH PRIVILEGES;注意app_user%这里的%表示允许从任意主机连接。如果脚本只在指定机器上运行建议改成具体IP比如app_user192.168.1.100进一步减小暴露面。这样即使口令泄露攻击者最多也只能操作指定库的几张表不能DROP DATABASE也不能改权限。3.3 连接对象、游标对象与上下文管理器连接对象和游标对象的关系我用一个生活类比来讲。连接对象像是“通话信道”代表你的程序与MySQL服务器之间的一条物理或逻辑链路它是重量级资源不能随便创建销毁。游标对象则是“会话中的笔记本”你用它在信道上发起一条条SQL、记录返回结果。一个连接可以创建多个游标游标用完关闭即可不用关连接连接用完了才需要关闭。代码上我强烈建议用with语句。PyMySQL的连接对象支持上下文管理器协议能自动处理提交和异常回滚。来看一段基础示范import pymysql conn pymysql.connect( host127.0.0.1, port3306, userapp_user, passwordyour_password, databasetest_db, charsetutf8mb4 ) try: with conn.cursor() as cursor: cursor.execute(SELECT 1) result cursor.fetchone() print(result) finally: conn.close()这里with conn.cursor() as cursor会在游标用完以后自动关闭游标但连接本身还在必须手动conn.close()。如果你忘了关闭连接脚本退出后连接会一直挂在MySQL服务端直到MySQL的wait_timeout把它断开。脚本自己可能看不出问题但数据库侧会累积一堆Sleep连接最后把max_connections打满。4. 实操过程从零完成数据库连接与增删改查4.1 先建库建表从SQL端做好准备工作到这一步假设你已经有了一个可用的MySQL实例并且知道root密码或者用Docker跑了一个。先在命令行或客户端工具里执行建库建表语句别试图用脚本一次性建所有东西调试起来麻烦还不直观。CREATE DATABASE IF NOT EXISTS test_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE test_db; CREATE TABLE IF NOT EXISTS user_info ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, age INT, email VARCHAR(120), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表结构里我故意加了age和email是为了后面增删改查时能覆盖多种数据类型。引擎用InnoDB是为了支持事务和行级锁后面讲游标的锁行为时这个背景知识会很有用。4.2 完整连接脚本初始化连接验证连通性建好库表之后我们来写第一个完整脚本。这一步的目标只有两个能连上数据库能执行最简单的一条查询。新建一个test_connection.pyimport pymysql def get_connection(): return pymysql.connect( host127.0.0.1, port3306, userapp_user, passwordyour_password, databasetest_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, connect_timeout5, ) if __name__ __main__: conn get_connection() with conn.cursor() as cursor: cursor.execute(SELECT VERSION() AS version) row cursor.fetchone() print(MySQL版本:, row[version]) conn.close()这里有个关键参数cursorclasspymysql.cursors.DictCursor。默认情况下游标返回的是元组比如(8.0.36,)用row[0]访问改成DictCursor后返回的是字典用row[version]访问。开发阶段强烈建议用DictCursor代码可读性高也很难因为字段顺序变化而踩坑。运行一下如果能打出版本号说明环境已经通了一半。4.3 增删改查的完整演示插入、查询、更新、删除连接通了接下来就是正经的增删改查。我把最常见的四个场景写成一个脚本这样你能完整看到commit、rollback、fetchone、fetchall到底在什么位置生效。import pymysql def get_connection(): return pymysql.connect( host127.0.0.1, port3306, userapp_user, passwordyour_password, databasetest_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, autocommitFalse, ) def insert_user(conn, username, age, email): sql INSERT INTO user_info (username, age, email) VALUES (%s, %s, %s) with conn.cursor() as cursor: cursor.execute(sql, (username, age, email)) # 增删改都需要手动提交否则不生效 conn.commit() print(插入成功影响行数:, cursor.rowcount) return cursor.lastrowid def query_users(conn): sql SELECT id, username, age, email FROM user_info ORDER BY id DESC with conn.cursor() as cursor: cursor.execute(sql) rows cursor.fetchall() return rows def update_user(conn, user_id, new_age): sql UPDATE user_info SET age %s WHERE id %s with conn.cursor() as cursor: cursor.execute(sql, (new_age, user_id)) conn.commit() print(更新成功影响行数:, cursor.rowcount) def delete_user(conn, user_id): sql DELETE FROM user_info WHERE id %s with conn.cursor() as cursor: cursor.execute(sql, user_id) conn.commit() print(删除成功影响行数:, cursor.rowcount) if __name__ __main__: conn get_connection() try: new_id insert_user(conn, 张三, 28, zhangsanexample.com) print(新增用户ID:, new_id) rows query_users(conn) for row in rows: print(row) if rows: update_user(conn, rows[0][id], 29) delete_user(conn, new_id) except Exception as e: conn.rollback() print(出错已回滚:, e) finally: conn.close()这个脚本里最容易忽略的是conn.commit()。很多新手第一次执行INSERT后再查数据库什么都没有就是忘了提交。pymysql默认autocommitFalse意味着执行DML语句后必须显式commit()数据才会真正落库如果中途出异常调用rollback()之前执行的操作全部作废。这块是数据库事务的范畴但连接层面就已经在控这件事了。4.4 参数化查询为什么宁可多写代码也不要字符串拼SQL接着上面增删改查的脚本我必须单独把“参数化”这件事拎出来单独讲。因为这是我见过最常见、也最危险的错误。错误示范sql fSELECT * FROM user_info WHERE username {username} cursor.execute(sql)这样写等于把用户输入的东西直接当成SQL执行。输入一个 OR 11进去你整张表都被看光。再狠一点输入; DROP TABLE user_info; --表都没了。这不是危言耸听自动化扫描工具对这类漏洞一抓一个准。正确示范是占位符%ssql SELECT * FROM user_info WHERE username %s cursor.execute(sql, (username,))pymysql会把第二个参数里的值安全地转义后传给服务端而不是拼接进SQL文本。注意即使条件是数字也不要图省事写%d然后手拼统一走%s占位符就行。另外如果只有一个参数记得写成元组(username,)逗号别丢。这算Python的一个老坑(username)只是括号表达式不是元组。上面的参数化不仅防注入还顺带解决了一个编码问题你不用手动处理引号转义和编码转换驱动全包了。4.5 存储过程调用进阶场景的补充如果你在的团队习惯用存储过程封装复杂业务PyMySQL也是支持的。调用方式与普通执行相似只是参数传递上略有讲究。假设有这样一个存储过程DELIMITER // CREATE PROCEDURE get_user_count(IN min_age INT, OUT total INT) BEGIN SELECT COUNT(*) INTO total FROM user_info WHERE age min_age; END // DELIMITER ;Python端调用with conn.cursor() as cursor: cursor.callproc(get_user_count, (18, 0)) # callproc 的结果要通过 SELECT 来获取 cursor.execute(SELECT _get_user_count_1) result cursor.fetchone() print(result)这里的_get_user_count_1是存储过程的第一个OUT参数对应的用户变量。实际项目中我建议尽量少用存储过程因为版本管理和调试都不方便但既然存在这个场景知道一个能用的写法总是好事。5. 连接池、性能优化与工程化封装5.1 为什么你的脚本一上线就报Too many connections脚本还没接入业务时连接数永远不是问题。一旦你在Web服务里每个请求都新建连接、用完不关就会出现一个经典报错Too many connections。你可能会想我明明每次用完都conn.close()了啊问题在于连接对象不是立刻消亡而是进入TCP的TIME_WAIT状态。请求一多数据库服务端的连接数短暂飙升max_connections默认值通常是151很容易被打满。一台MySQL服务端单机扛几百个短连接就已经冒烟了如果你的服务有多个实例每个实例每秒几十个连接请求不挂才怪。解决思路有两个层面。应用层做好连接的复用和池化数据库层适当调大max_connections但这不是根本方案——调大一时解决了“连接不够用”代价是系统资源被一堆空闲连接白白浪费。真正优雅的做法是引入连接池。5.2 手写一个简单的连接池理解核心思想网上有专门的连接池库比如dbutils但为了让你知其所以然我先带你手写一个极简版本。核心思想就一句话提前创建一批连接放在池子里谁用谁取用完归还不够则按策略补充。import queue import threading import pymysql import time class SimpleConnectionPool: def __init__(self, max_size5, **conn_kwargs): self._pool queue.Queue(maxsizemax_size) self._conn_kwargs conn_kwargs self._max_size max_size self._created 0 self._lock threading.Lock() def _create_conn(self): return pymysql.connect(**self._conn_kwargs) def acquire(self): try: return self._pool.get_nowait() except queue.Empty: with self._lock: if self._created self._max_size: self._created 1 return self._create_conn() # 池已满且无空闲连接阻塞等待归还 return self._pool.get(timeout5) def release(self, conn): try: self._pool.put_nowait(conn) except queue.Full: conn.close() pool SimpleConnectionPool( max_size5, host127.0.0.1, userapp_user, passwordyour_password, databasetest_db, charsetutf8mb4 )这个池子比业务上常用的要简单但骨架在。它有一个队列存空闲连接加锁控制并发创建当池满时新请求会阻塞等待归还。你用的时候conn pool.acquire()用完别忘了pool.release(conn)否则连接池会越用越少最后所有线程全部卡死。实际项目中我建议直接用DBUtils库Python 3下包名是dbutils它提供了PooledDB底层实现了连接有效性检测、自动重连等一堆细节。用法大概是这样from dbutils.pooled_db import PooledDB import pymysql pool PooledDB( creatorpymysql, maxconnections10, mincached2, maxcached5, blockingTrue, host127.0.0.1, userapp_user, passwordyour_password, databasetest_db, charsetutf8mb4 ) conn pool.connection()这里mincached2表示启动时就预先创建2个连接maxcached5指池中最多保留5个空闲连接blockingTrue表示连接不够时请求阻塞等待而不是直接报错。这个参数组合在绝大多数中小型项目里够用很久。5.3 SQL 日志、重试与超时处理让连接更健壮连接池解决的是资源复用问题但实际运行中还会遇到网络抖动、MySQL重启、慢查询等问题。我通常会再对连接和查询做几层加固。第一层是SQL日志。写一个装饰器把每次执行的SQL和参数打出来线上排障时这个日志价值巨大。import functools import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) def log_sql(func): functools.wraps(func) def wrapper(cursor, sql, argsNone): logging.info(f执行SQL: {sql} | 参数: {args}) return func(cursor, sql, args) return wrapper # 使用时 class LoggedCursor: def __init__(self, cursor): self._cursor cursor def execute(self, sql, argsNone): logging.info(f执行SQL: {sql} | 参数: {args}) return self._cursor.execute(sql, args)第二层是短超时。pymysql支持在连接级别设置read_timeout和write_timeout。有些奇葩SQL能把数据库整个卡住如果没有超时应用线程会一直挂在那。设置一个合理的读取超时比如30秒至少能让你的服务“快速失败”为后续自动重试留出余地。第三层是有限重试。连接建立失败偶尔是因为MySQL在做主从切换或者网络闪断。盲目重试一万次是不行的我习惯用“退避重试”策略第一次失败等1秒第二次等2秒第三次等4秒最多5次。简单实现如下import time def connect_with_retry(max_retries5, backoff_factor2, **kwargs): for i in range(max_retries): try: return pymysql.connect(**kwargs) except pymysql.Error as e: wait_time backoff_factor ** i logging.warning(f连接失败({i1}次): {e}, {wait_time}秒后重试) time.sleep(wait_time) raise RuntimeError(数据库连接失败已重试多次)这套东西单看不复杂但配合连接池能在数据库短暂故障期间最大程度避免应用雪崩。这些都是生产环境里用血泪换来的教训。6. 常见问题与排查技巧实录6.1 连接失败类问题认证、网络、端口怎么查报错Access denied for user xxxxlocalhost这个报错90%是用户名或密码不对。但还有一种隐藏场景你用root从远程连MySQL 8.0MySQL默认root只允许本地登录自然被拒。检查思路是SELECT user, host FROM mysql.user;看看目标用户在host字段是localhost还是%。如果只是localhost要么把连接IP改成127.0.0.1要么单独创建允许远程连接的用户。报错Cant connect to MySQL server on 127.0.0.1 (timed out)先确认MySQL服务是否启动systemctl status mysqlLinux或任务管理器查看服务Windows。再确认端口telnet 127.0.0.1 3306不通就检查防火墙、Docker端口映射。另外MySQL默认绑定127.0.0.1的话远程连不进来需要在配置文件里把bind-address改成0.0.0.0改完重启。报错Authentication plugin caching_sha2_password cannot be loaded这是MySQL 8.0老驱动不兼容的经典报错。解决办法有两种升级驱动版本pip install --upgrade pymysql最新版一般都支持或者把用户认证插件改回mysql_native_passwordALTER USER app_user% IDENTIFIED WITH mysql_native_password BY your_password; FLUSH PRIVILEGES;不过这属于兼容性妥协新项目我还是建议直接上支持caching_sha2_password的新驱动。6.2 编码与字符集问题中文乱码和emoji报错中文乱码基本是“两端不一致”闹的。首先是数据库或表用了latin1其次是你连接时没指定charset。排查一句话SHOW CREATE TABLE user_info;看到CHARSETlatin1就说明问题在这。要么ALTER TABLE转换要么重建表。连接代码里统一charsetutf8mb4这个方向对了绝大多数编码问题都能解决。emoji报错Incorrect string value: \xF0\x9F\x98\x80 for column xxx典型原因就是列字符集不是utf8mb4。MySQL里utf8只支持最多3字节emoji是4字节必须用utf8mb4。改列字符集ALTER TABLE user_info MODIFY COLUMN username VARCHAR(50) CHARACTER SET utf8mb4 NOT NULL;注意如果表里已有数据执行前先备份字符集转换在数据量大的情况下可能很慢。6.3 连接泄漏与连接数上限线上突然拒绝服务的元凶报错Too many connections我接手过好几个项目服务跑着跑着突然数据库拒绝所有请求最后定位都是连接泄漏。代码里某个异常分支没有conn.close()连接一直不归还。排查有几步第一SHOW PROCESSLIST;看当前连接来源。看到大量来自同一个IP、State为Sleep的连接基本就是泄漏了。 第二查连接状态时间如果好几个连接Time已经几千秒说明应用侧没关闭。 第三在代码里全局搜索pymysql.connect确认每一处连接是不是都在finally或上下文管理器里关闭了。预防措施也不复杂连接统一走连接池池会帮你回收手动创建连接时用with或try/finally保证关闭线上适当调低MySQL的wait_timeout、interactive_timeout让闲置连接尽快被服务端回收。6.4 事务、锁等待与常见报错速查表报错Lock wait timeout exceeded; try restarting transaction这个报错意味着你的事务里要更新的行正被别的事务锁住等超过了innodb_lock_wait_timeout默认50秒。新手最常见的操作是脚本里开了事务、执行了UPDATE但忘记commit事务一直不结束后续所有对该行的操作全部排队。排查SHOW ENGINE INNODB STATUS\G看锁等待关系找到持有锁的事务ID。找到之后要么等它提交要么在数据库层KILL掉那个线程-- 先找到线程ID SELECT * FROM information_schema.innodb_trx; -- 终止阻塞事务 KILL 线程ID;日常编码建议事务保持短小DML之后尽快commit不在事务里做耗时的外部接口调用。我把这些高频问题整理成一张速查表方便你遇到时报错时快速对照报错信息常见原因解决方向Cant connect to MySQL server服务未启动 / 端口错误 / 防火墙检查服务、端口连通性Access denied for user用户名密码错误 / host限制检查用户授权Unknown database数据库名写错检查database参数Table doesnt exist库名表名大小写问题 / 未选中数据库使用完整库名.表名Lost connection to MySQL server during query连接超时 / 包过大调大max_allowed_packetDuplicate entry xxx for key PRIMARY主键或唯一键冲突先查是否已存在或改用INSERT ... ON DUPLICATE KEY UPDATEIncorrect string value字符集不是utf8mb4改表、改连接字符集Too many connections连接数打满 / 连接泄漏连接池 排查泄漏Lock wait timeout exceeded事务锁等待超时检查未提交事务、KILL阻塞线程6.5 一个真实案例排查线上“偶发连接失败”的完整过程最后分享一个我自己踩过的坑包含完整的排查思路。一次同事反馈某个Python脚本每天凌晨会“偶发”报错错误是Connection reset by peer。第一印象是网络问题查了一圈没发现异常。第二天加了SQL日志才发现凌晨正好是定时任务批量写数据的时间单条SQL涉及的数据量特别大超过了MySQL的max_allowed_packet默认值4MB服务端直接断开连接。解决方式有两个根据业务选。一是调大MySQL配置[mysqld] max_allowed_packet64M二是拆批一次只插入500行分多次执行。实际项目中我两种方案都做了因为调大上限是兜底拆批是性能优化两者并不冲突。这类“偶发报错”其实是最难调的因为不是必现你必须把每条SQL的执行时间、返回行数、参数大小全部记下来才能找到真正的规律。7. 从能用走向好用工程化封装的几点心得文章写到最后说点掏心窝的话。上面所有代码片段你在真实项目里不会直接拷成一堆散件而是会封装成一个模块、一个类统一管理连接参数、日志、重试、熔断。我自己常用的封装方式是写一个Database类把get_conn、query、execute、callproc全包进去再配一个全局的日志句柄。这样业务代码里基本只出现db.query(sql, args)可读性和可维护性都好很多。围绕这个工程化方向我最后再分享几点经验权当给这篇文章做个实打实的补充。第一连接信息务必从配置中心或环境变量读取不要硬编码在源码里。哪怕你是个人项目密码写死在代码里哪天开源或者发给别人就是安全事故。第二给每台应用服务器上的Python进程单独创建MySQL用户而不是所有服务共享一个app_user。这样出了问题你能通过SHOW PROCESSLIST;一眼看出是哪个服务在捣乱关闭某个服务的权限也只需要REVOKE不用动其他服务。第三脚本长时间跑批时要注意MySQL的wait_timeout。默认8小时如果你的脚本有一个超长的查询连接可能会被服务端提前断开。解决方案是加上自动重连的逻辑比如连接前先cursor.execute(SELECT 1)探测一下异常的时候重新建立连接。最后再说一个很多人忽略的点别把数据库连接代码和SQL业务逻辑全塞进一个文件里。Python脚本连接MySQL本身不复杂复杂的是连接以后那堆业务逻辑。把连接、查询、业务解耦开来至少拆成db_utils.py和service.py两个模块你会发现在调试和测试时整个人的心态都会好很多。这篇指南从环境准备讲到驱动选型从核心连接参数讲到增删改查和连接池最后把常见报错和真实踩坑经历都摊开来聊了一遍。我认为只要沿着这条主线路走一遍Python连接MySQL这件事你不仅能跑通还能知道为什么这样写、哪些地方容易翻车、线上一旦出问题该往哪个方向查。这些才是真正有价值的东西。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进