在任何软件系统中,错误和异常是不可避免的。尤其是在与外部系统(如数据库)交互时,网络波动、数据库宕机、SQL 语法错误、数据完整性冲突等各种问题层出不穷。一个健壮的应用程序不仅要能正常处理预期的数据流,更要能够优雅地处理各种错误情况,避免程序崩溃、数据损坏或资源泄漏。PyMySQL
作为数据库驱动,其错误处理机制是构建可靠、稳定应用的关键一环。
PyMySQL
的错误体系严格遵循了 Python 数据库 API 规范 (PEP 249,即 DB-API 2.0)。这个规范定义了一套标准的异常类,使得不同的数据库驱动(如 psycopg2
for PostgreSQL, sqlite3
for SQLite, pymysql
for MySQL)能够提供统一的错误处理接口。理解这些错误类型对于编写可移植且健壮的数据库代码至关重要。
DB-API 2.0 定义了一个异常层次结构,所有与数据库相关的异常都继承自 Error
类。
StandardError (Python 内置异常)
├── Warning
└── Error
├── InterfaceError
├── DatabaseError
├── DataError
├── OperationalError
├── IntegrityError
├── InternalError
├── ProgrammingError
└── NotSupportedError
让我们逐一解析这些异常类及其在 PyMySQL
中的具体含义:
Warning
(警告):
Error
pymysql.Warning
Error
(一般错误):
StandardError
。在 Python 3 中,它直接继承自 Exception
。pymysql.Error
PyMySQL
错误时,可以捕获此类型。InterfaceError
(接口错误):
Error
pymysql.InterfaceError
PyMySQL
库)内部的错误、无法连接到数据库服务器之外的网络层问题、或 PyMySQL 与底层操作系统或网络库的交互问题有关。这种情况通常较少见,且难以通过应用程序代码直接修复。DatabaseError
(数据库错误):
Error
pymysql.DatabaseError
以下是 DatabaseError
的子类:
DataError
(数据错误):
DatabaseError
pymysql.DataError
1264
(Out of range value for column), 1406
(Data too long for column), 1366
(Incorrect string value).OperationalError
(操作错误):
DatabaseError
pymysql.OperationalError
2003
(Can’t connect to MySQL server), 2006
(MySQL server has gone away), 1040
(Too many connections), 1045
(Access denied).IntegrityError
(完整性错误):
DatabaseError
pymysql.IntegrityError
FOREIGN KEY constraint fails
)、非空约束 (NOT NULL constraint fails
) 等。1062
(Duplicate entry for key), 1452
(Cannot add or update a child row: a foreign key constraint fails), 1048
(Column ‘X’ cannot be null).InternalError
(内部错误):
DatabaseError
pymysql.InternalError
ProgrammingError
(编程错误):
DatabaseError
pymysql.ProgrammingError
1064
(You have an error in your SQL syntax), 1146
(Table ‘db.table’ doesn’t exist), 1054
(Unknown column ‘X’ in ‘field list’), 1046
(No database selected).NotSupportedError
(不支持的错误):
DatabaseError
pymysql.NotSupportedError
理解这个层次结构可以帮助你编写更精确的错误处理逻辑。例如,你可以捕获 IntegrityError
来专门处理数据完整性问题,或者捕获更通用的 DatabaseError
来处理所有数据库引擎层面的错误。
ERR_Packet
) 的映射当 MySQL 服务器遇到错误时,它会向客户端发送一个特殊的错误包 (ERR_Packet
)。这个包包含了错误的关键信息,PyMySQL
接收到这个包后,会将其解析并转换为相应的 Python 异常。理解 ERR_Packet
的结构,有助于我们更深入地理解 PyMySQL
是如何报告错误的。
ERR_Packet
的基本结构如下:
payload_length (3 bytes)
, sequence_id (1 byte)
。0xFF
(1 byte): 错误包的标志字节。error_code
(2 bytes): MySQL 错误码(无符号短整型)。这是识别特定错误类型的核心。例如,1062
表示重复条目,2003
表示无法连接。sql_state_marker
(1 byte): 通常是 #
。sql_state
(5 bytes): 遵循 SQL 标准的 SQLSTATE 错误码。例如,23000
表示完整性约束违规。error_message
(N bytes): 错误消息字符串,提供了更详细的错误描述。PyMySQL
在接收到 ERR_Packet
后,会解析 error_code
和 sql_state
,并根据这些信息来实例化上面介绍的 DB-API 2.0 异常类。同时,原始的错误码和错误消息通常会作为异常对象的属性提供,例如 e.args[0]
会是错误码,e.args[1]
会是错误消息。
例如,当 PyMySQL
捕获到一个 MySQL 错误码 1062
(Duplicate entry for key)时,它会抛出 pymysql.IntegrityError
,因为这是一个违反唯一性约束的错误。
重要提示: 尽管 PyMySQL
会将 MySQL 错误码映射到 DB-API 异常,但在某些复杂的错误场景下,相同的 MySQL 错误码可能在逻辑上对应不同的 DB-API 异常类型。因此,在实践中,最好同时考虑捕获 DB-API 异常类型,并检查异常对象的 args[0]
(MySQL 错误码) 来进行更细致的判断。
import pymysql
import logging
from pymysql.err import OperationalError, ProgrammingError, IntegrityError, DataError, Error # 导入所有可能用到的异常类
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
db_config_errors = {
'host': '127.0.0.1',
'port': 3306,
'user': 'root',
'password': 'your_password',
'charset': 'utf8mb4',
'autocommit': True, # 这里设置为 True,简化每个操作的提交逻辑,专注于错误本身
'database': 'pymysql_test_db',
}
def setup_error_demo_tables(config):
"""
创建用于错误演示的表。
"""
logging.info("--- 准备错误演示表 ---") # 打印提示
conn = None # 初始化连接
try: # 确保连接被正确关闭
# 临时连接用于创建数据库,确保数据库存在
temp_conn = pymysql.connect(
host=config['host'],
port=config['port'],
user=config['user'],
password=config['password'],
charset=config['charset']
)
with temp_conn.cursor() as cursor: # 创建游标
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {
config['database']} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;") # 创建数据库
temp_conn.commit() # 提交
temp_conn.close() # 关闭临时连接
conn = pymysql.connect(**config) # 连接到指定数据库
with conn.cursor() as cursor: # 创建游标
# 表 1: 用于演示 ProgrammingError, DataError, IntegrityError
table_name_users = "error_demo_users" # 定义用户表名
logging.info(f"创建表 '{
table_name_users}' (如果不存在)...") # 打印提示
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {
table_name_users} (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL, -- 唯一约束
email VARCHAR(100) NOT NULL,
age INT CHECK (age > 0) -- 检查约束
) ENGINE=InnoDB;
""") # 创建用户表
# 表 2: 用于演示外键 IntegrityError
table_name_orders = "error_demo_orders" # 定义订单表名
logging.info(f"创建表 '{
table_name_orders}' (如果不存在)...") # 打印提示
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {
table_name_orders} (
order_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
order_date DATE NOT NULL,
FOREIGN KEY (user_id) REFERENCES {
table_name_users}(id)
) ENGINE=InnoDB;
""") # 创建订单表
conn.commit() # 提交表创建操作
logging.info("所有错误演示表已就绪。") # 打印提示
# 清空数据以确保演示效果
cursor.execute(f"TRUNCATE TABLE {
table_name_users};") # 清空用户表
cursor.execute(f"TRUNCATE TABLE {
table_name_orders};") # 清空订单表
conn.commit() # 提交清空操作
logging.info("所有错误演示表数据已清空。") # 打印提示
except Exception as e: # 捕获所有异常
logging.critical(f"错误演示表设置失败: {
e}", exc_info=True) # 记录关键错误信息
finally: # 最终
if conn: # 如果连接存在
conn.close() # 关闭连接
# --- 2.8.3 常见的数据库错误及其处理策略 ---
def demonstrate_operational_error(config):
"""
演示 OperationalError (操作错误),通常与连接或环境相关。
"""
logging.info("\n--- 演示 OperationalError (连接问题) ---") # 打印提示
# 尝试连接一个不存在的或错误的端口/地址
bad_config = config.copy() # 复制配置
bad_config['port'] = 12345 # 设置一个错误的端口
bad_config['connect_timeout'] = 3 # 设置连接超时时间
conn = None # 初始化连接
try: # 确保连接被正确关闭
logging.warning(f"尝试连接到不存在的端口 {
bad_config['port']} (预计失败)...") # 打印警告
conn = pymysql.connect(**bad_config) # 尝试连接
logging.info("意外: 连接成功 (这不应该发生在错误端口)。") # 打印意外信息
except OperationalError as e: # 捕获操作错误
# MySQL 错误码 2003: Can't connect to MySQL server on 'hostname' (10061)
logging.error(f"成功捕获 OperationalError: 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
# 实际应用中:
# 1. 记录详细错误日志
# 2. 如果是瞬态错误 (如网络抖动),可以尝试重试
# 3. 如果是长期错误 (如数据库宕机),通知运维团队,启动备用系统或显示友好的错误消息
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
finally: # 最终
if conn: # 如果连接存在
conn.close() # 关闭连接
logging.info("OperationalError 演示连接已关闭。") # 打印提示
def demonstrate_programming_error(config):
"""
演示 ProgrammingError (编程错误),通常是 SQL 语法或对象不存在。
"""
logging.info("\n--- 演示 ProgrammingError (SQL 语法/对象不存在) ---") # 打印提示
conn = None # 初始化连接
try: # 确保连接被正确关闭
conn = pymysql.connect(**config) # 连接到数据库
with conn.cursor() as cursor: # 创建游标
# 1. SQL 语法错误
bad_sql = "SELCT * FROM error_demo_users;" # 错误的 SQL 语句 (SELECT 拼写错误)
logging.warning(f"尝试执行错误的 SQL 语句: '{
bad_sql}' (预计失败)...") # 打印警告
try: # 确保错误被捕获
cursor.execute(bad_sql) # 执行错误 SQL
logging.info("意外: 错误 SQL 语句执行成功。") # 打印意外信息
except ProgrammingError as e: # 捕获编程错误
# MySQL 错误码 1064: You have an error in your SQL syntax
logging.error(f"成功捕获 ProgrammingError (语法错误): 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
# 2. 表不存在
non_existent_table_sql = "SELECT * FROM non_existent_table;" # 查询不存在的表
logging.warning(f"尝试查询不存在的表: '{
non_existent_table_sql}' (预计失败)...") # 打印警告
try: # 确保错误被捕获
cursor.execute(non_existent_table_sql) # 执行查询
logging.info("意外: 不存在的表查询成功。") # 打印意外信息
except ProgrammingError as e: # 捕获编程错误
# MySQL 错误码 1146: Table 'database.table' doesn't exist
logging.error(f"成功捕获 ProgrammingError (表不存在): 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
except Exception as e: # 捕获所有异常
logging.critical(f"ProgrammingError 演示失败: {
e}", exc_info=True) # 记录关键错误信息
finally: # 最终
if conn: # 如果连接存在
conn.close() # 关闭连接
logging.info("ProgrammingError 演示连接已关闭。") # 打印提示
def demonstrate_integrity_error(config):
"""
演示 IntegrityError (完整性错误),通常是违反约束。
"""
logging.info("\n--- 演示 IntegrityError (违反约束) ---") # 打印提示
conn = None # 初始化连接
try: # 确保连接被正确关闭
conn = pymysql.connect(**config) # 连接到数据库
with conn.cursor() as cursor: # 创建游标
table_name_users = "error_demo_users" # 用户表名
table_name_orders = "error_demo_orders" # 订单表名
# 1. 插入重复的唯一键 (username)
logging.info("插入第一个用户 'testuser_unique'...") # 打印提示
insert_user_sql = f"INSERT INTO {
table_name_users} (username, email, age) VALUES (%s, %s, %s);" # 插入用户 SQL
cursor.execute(insert_user_sql, ("testuser_unique", "[email protected]", 25)) # 插入用户
conn.commit() # 提交
logging.warning("尝试插入重复的唯一键 'testuser_unique' (预计失败)...") # 打印警告
try: # 确保错误被捕获
cursor.execute(insert_user_sql, ("testuser_unique", "[email protected]", 30)) # 插入重复用户
conn.commit() # 尝试提交
logging.info("意外: 成功插入重复唯一键。") # 打印意外信息
except IntegrityError as e: # 捕获完整性错误
# MySQL 错误码 1062: Duplicate entry 'value' for key 'PRIMARY' / 'username'
logging.error(f"成功捕获 IntegrityError (重复唯一键): 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
conn.rollback() # 回滚,确保数据库状态一致
logging.info("操作已回滚。") # 打印提示
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
if conn: conn.rollback() # 确保回滚
# 2. 插入违反 NOT NULL 约束
logging.warning("尝试插入违反 NOT NULL 约束的数据 (username 为 NULL, 预计失败)...") # 打印警告
try: # 确保错误被捕获
cursor.execute(f"INSERT INTO {
table_name_users} (username, email, age) VALUES (NULL, %s, %s);", ("[email protected]", 20)) # 插入 NULL 用户名
conn.commit() # 尝试提交
logging.info("意外: 成功插入 NULL 唯一键。") # 打印意外信息
except IntegrityError as e: # 捕获完整性错误
# MySQL 错误码 1048: Column 'username' cannot be null
logging.error(f"成功捕获 IntegrityError (NOT NULL 约束): 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
conn.rollback() # 回滚
logging.info("操作已回滚。") # 打印提示
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
if conn: conn.rollback() # 确保回滚
# 3. 插入违反外键约束
logging.warning("尝试插入违反外键约束的订单 (user_id 不存在, 预计失败)...") # 打印警告
try: # 确保错误被捕获
# 假设 user_id 999 不存在于 users_with_points 表中
cursor.execute(f"INSERT INTO {
table_name_orders} (user_id, order_date) VALUES (%s, %s);", (999, "2023-11-01")) # 插入不存在的用户 ID
conn.commit() # 尝试提交
logging.info("意外: 成功插入违反外键约束的订单。") # 打印意外信息
except IntegrityError as e: # 捕获完整性错误
# MySQL 错误码 1452: Cannot add or update a child row: a foreign key constraint fails
logging.error(f"成功捕获 IntegrityError (外键约束): 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
conn.rollback() # 回滚
logging.info("操作已回滚。") # 打印提示
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
if conn: conn.rollback() # 确保回滚
except Exception as e: # 捕获所有异常
logging.critical(f"IntegrityError 演示失败: {
e}", exc_info=True) # 记录关键错误信息
finally: # 最终
if conn: # 如果连接存在
conn.close() # 关闭连接
logging.info("IntegrityError 演示连接已关闭。") # 打印提示
def demonstrate_data_error(config):
"""
演示 DataError (数据错误),通常是数据类型或值范围不匹配。
"""
logging.info("\n--- 演示 DataError (数据类型/值范围不匹配) ---") # 打印提示
conn = None # 初始化连接
try: # 确保连接被正确关闭
conn = pymysql.connect(**config) # 连接到数据库
with conn.cursor() as cursor: # 创建游标
table_name_users = "error_demo_users" # 用户表名
# 1. 插入年龄为负数 (违反 CHECK 约束)
logging.warning("尝试插入年龄为负数的用户 (age INT CHECK (age > 0), 预计失败)...") # 打印警告
try: # 确保错误被捕获
cursor.execute(f"INSERT INTO {
table_name_users} (username, email, age) VALUES (%s, %s, %s);", ("invalid_age_user", "[email protected]", -5)) # 插入负年龄
conn.commit() # 尝试提交
logging.info("意外: 成功插入负年龄用户。") # 打印意外信息
except DataError as e: # 捕获数据错误
# MySQL 错误码 3819: Check constraint 'users_with_points_chk_1' is violated.
# 或者 1264: Out of range value
logging.error(f"成功捕获 DataError (CHECK 约束/范围外): 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
conn.rollback() # 回滚
logging.info("操作已回滚。") # 打印提示
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
if conn: conn.rollback() # 确保回滚
# 2. 插入过长的字符串到 VARCHAR 列
logging.warning("尝试插入过长的字符串到 username 列 (VARCHAR(50), 预计失败)...") # 打印警告
long_username = "a" * 51 # 创建一个过长的字符串
try: # 确保错误被捕获
cursor.execute(f"INSERT INTO {
table_name_users} (username, email, age) VALUES (%s, %s, %s);", (long_username, "[email protected]", 30)) # 插入过长用户名
conn.commit() # 尝试提交
logging.info("意外: 成功插入过长字符串。") # 打印意外信息
except DataError as e: # 捕获数据错误
# MySQL 错误码 1406: Data too long for column 'username'
logging.error(f"成功捕获 DataError (数据过长): 错误码={
e.args[0]}, 消息='{
e.args[1]}'") # 记录错误信息
conn.rollback() # 回滚
logging.info("操作已回滚。") # 打印提示
except Exception as e: # 捕获其他异常
logging.error(f"捕获到其他意外错误: {
type(e).__name__}: {
e}", exc_info=True) # 记录错误信息
if conn: conn.rollback() # 确保回滚
except Exception as e: # 捕获所有异常
logging.critical(f"DataError 演示失败: {
e}", exc_info=True) # 记录关键错误信息
finally: # 最终
if conn: # 如果连接存在
conn.close() # 关闭连接
logging.info("DataError 演示连接已关闭。") # 打印提示
##### 2.8.4 健壮的错误处理实践:构建高可用应用
仅仅捕获错误是不够的,一个真正健壮的应用程序需要一套完整的错误处理策略。
###### 2.8.4.1 精确捕获与通用捕获的平衡
* **精确捕获**: 针对特定的已知错误类型(如 `IntegrityError` 用于处理唯一性冲突,`OperationalError` 用于处理连接问题)进行捕获。这允许你为不同的错误原因提供高度定制化的响应。
```python
try: # 尝试执行数据库操作
# ... 数据库操作 ...
except IntegrityError as e: # 捕获完整性错误
if e.args[0] == 1062: # 检查 MySQL 错误码是否为重复键
logging.warning(f"检测到重复数据: {
e.args[1]}") # 记录警告信息
# 返回特定错误代码给前端,或提示用户数据已存在
else: # 如果是其他完整性错误
logging.error(f"未知完整性错误: {
e}", exc_info=True) # 记录错误信息
raise # 重新抛出异常
except OperationalError as e: # 捕获操作错误
logging.error(f"数据库连接或操作环境错误: {
e}", exc_info=True) # 记录错误信息
# 尝试重试,或者通知管理员
raise # 重新抛出异常
except Exception as e: # 捕获所有其他意外错误
logging.critical(f"发生未预料的数据库错误: {
e}", exc_info=True) # 记录关键错误信息
raise # 重新抛出异常
```
* **通用捕获**: 使用 `except Exception as e:` 来捕获所有未被特定 `except` 块处理的异常。这作为最后的防线,可以防止程序崩溃,并确保所有未知的错误都被记录下来。在通用捕获中,通常会重新抛出异常 (`raise`),或者将异常转换为更高级别的自定义异常,以便上层调用者能够处理。
###### 2.8.4.2 错误日志记录 (`logging`)
详细的错误日志是调试和监控生产系统不可或缺的工具。在捕获到异常时,应使用 Python 的 `logging` 模块记录错误信息:
* **使用不同的日志级别**:
* `logging.warning()`: 对于可恢复或预期内的轻微问题(如数据不存在,但不影响整体流程)。
* `logging.error()`: 对于导致操作失败但不会立即崩溃的错误。
* `logging.critical()`: 对于导致应用程序无法正常运行的严重错误,可能需要立即人工干预。
* **包含上下文信息**: 除了错误消息,还应记录相关的上下文信息,如:
* 发生错误的函数名/模块。
* 传递给 SQL 语句的参数。
* 受影响的用户 ID 或业务实体 ID。
* 使用 `exc_info=True` 来记录完整的堆栈跟踪信息。
```python
import logging
# ... (其他导入) ...
try: # 尝试执行数据库操作
user_id = 123
data_to_insert = ("new_user", "[email protected]", 30)
# ... cursor.execute(...) ...
except IntegrityError as e: # 捕获完整性错误
logging.error(f"插入用户 {
data_to_insert[0]} 时发生重复键错误: {
e.args[1]}, 用户ID: {
user_id}", exc_info=True) # 记录详细错误信息
# ... 处理逻辑 ...
```
###### 2.8.4.3 重试机制 (Retries) for 瞬态错误
对于一些瞬态错误(Transient Errors),如数据库连接短暂中断、死锁、临时性网络抖动等 `OperationalError`,应用程序可以通过简单的**重试机制**来提高健壮性。
* **重试次数限制**: 避免无限重试导致死循环。
* **指数退避 (Exponential Backoff)**: 每次重试时,等待的时间逐渐增加。这可以避免在数据库服务器不堪重负时,应用程序大量重试反而加剧问题。例如,第一次等待 1 秒,第二次等待 2 秒,第三次等待 4 秒。
* **抖动 (Jitter)**: 在指数退避的基础上引入随机延迟,避免所有重试在同一时间点发生,从而减少“惊群效应”。
\[
\text{
wait\_time} = \min(\text{
max\_wait}, \text{
base\_delay} \times 2^{
\text{
retries}})
\]
或者带抖动:
\[
\text{
wait\_time} = \text{
random}(0, \min(\text{
max\_wait}, \text{
base\_delay} \times 2^{
\text{
retries}}))
\]
图片占位符:一个简单的指数退避公式
```python
import time
import random # 导入随机模块
MAX_RETRIES = 5 # 最大重试次数
BASE_DELAY_SECONDS = 0.5 # 基础延迟秒数
MAX_WAIT_SECONDS = 30 # 最大等待秒数
def execute_with_retry(config, sql_query, params=None, is_write_operation=False):
"""
带重试机制的数据库操作函数。
针对 OperationalError (连接问题、服务器暂时性不可用) 进行重试。
对于写操作,需要考虑幂等性。
"""
conn = None # 初始化连接
for attempt in range(1, MAX_RETRIES + 1): # 循环重试
try: # 确保连接被正确关闭
logging.info(f"尝试连接数据库并执行操作 (第 {
attempt}/{
MAX_RETRIES} 次尝试)...") # 打印提示
conn = pymysql.connect(**config) # 连接到数据库
with conn.cursor() as cursor: # 创建游标
cursor.execute(sql_query, params) # 执行 SQL
if is_write_operation: # 如果是写操作
conn.commit() # 提交
else: # 如果是读操作
# 如果是读操作,可能需要返回结果
return cursor.fetchall() # 返回所有结果
logging.info(f"操作成功 (第 {
attempt} 次尝试)。") # 打印成功信息
return True # 返回成功
except OperationalError as e: # 捕获操作错误
logging.warning(f"捕获到 OperationalError: {
e.args[1]} (错误码: {
e.args[0]})。") # 记录警告
if attempt < MAX_RETRIES: # 如果未达到最大重试次数
wait_time = min(MAX_WAIT_SECONDS, BASE_DELAY_SECONDS * (2 ** (attempt - 1))) # 计算等待时间 (指数退避)
jitter = random.uniform(0, wait_time * 0.2) # 引入 20% 的抖动
final_wait_time = wait_time + jitter # 最终等待时间
logging.info(f"等待 {
final_wait_time:.2f} 秒后重试...") # 打印提示
time.sleep(final_wait_time) # 等待
else: # 如果达到最大重试次数
logging.error(f"达到最大重试次数 ({
MAX_RETRIES}),操作最终失败。") # 记录错误
raise # 重新抛出异常
except Exception as e: # 捕获其他所有异常
logging.error(f"在重试过程中捕获到非 OperationalError 异常: {
e}", exc_info=True) # 记录错误信息
# 对于非瞬态错误,不应重试,直接抛出
raise # 重新抛出异常
finally: #