【Python】PyMySQL

2.8 错误处理与健壮性:构建永不崩溃的应用

在任何软件系统中,错误和异常是不可避免的。尤其是在与外部系统(如数据库)交互时,网络波动、数据库宕机、SQL 语法错误、数据完整性冲突等各种问题层出不穷。一个健壮的应用程序不仅要能正常处理预期的数据流,更要能够优雅地处理各种错误情况,避免程序崩溃、数据损坏或资源泄漏。PyMySQL 作为数据库驱动,其错误处理机制是构建可靠、稳定应用的关键一环。

2.8.1 PyMySQL 错误类型层级:遵循 DB-API 2.0 规范

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 中的具体含义:

  1. Warning (警告):

    • 继承自: Error
    • PyMySQL 中的实现: pymysql.Warning
    • 含义: 指示数据库操作过程中发生了非致命性的问题。这些问题通常不会阻止 SQL 语句的执行,但可能表示某些不符合预期的情况。例如,执行一个被截断的插入操作,或者在特定数据库配置下的一些提示。通常,这些警告可以被记录,但不需要中断程序流程。
    • 示例: 在 MySQL 中,如果你插入一个数值,但它超出了目标列的范围,MySQL 可能会将其截断并返回一个警告。
  2. Error (一般错误):

    • 继承自: Python 内置的 StandardError。在 Python 3 中,它直接继承自 Exception
    • PyMySQL 中的实现: pymysql.Error
    • 含义: 这是所有其他数据库相关错误的基类。当你需要捕获所有 PyMySQL 错误时,可以捕获此类型。
  3. InterfaceError (接口错误):

    • 继承自: Error
    • PyMySQL 中的实现: pymysql.InterfaceError
    • 含义: 表示数据库接口本身出现问题,而不是数据库或 SQL 语句的问题。这可能与驱动程序(PyMySQL 库)内部的错误、无法连接到数据库服务器之外的网络层问题、或 PyMySQL 与底层操作系统或网络库的交互问题有关。这种情况通常较少见,且难以通过应用程序代码直接修复。
    • 示例: 驱动程序无法加载,或者驱动程序内部的某些低级通信机制失效。
  4. DatabaseError (数据库错误):

    • 继承自: Error
    • PyMySQL 中的实现: pymysql.DatabaseError
    • 含义: 这是所有真正的数据库相关错误(与数据库引擎本身、数据或 SQL 语句相关的错误)的基类。

    以下是 DatabaseError 的子类:

    • DataError (数据错误):

      • 继承自: DatabaseError
      • PyMySQL 中的实现: pymysql.DataError
      • 含义: 指示数据处理方面的问题。例如,插入的数据类型与列类型不匹配、数据值超出列的有效范围、数据格式不正确等。
      • MySQL 错误码对应: 例如,1264 (Out of range value for column), 1406 (Data too long for column), 1366 (Incorrect string value).
    • OperationalError (操作错误):

      • 继承自: DatabaseError
      • PyMySQL 中的实现: pymysql.OperationalError
      • 含义: 指示数据库操作环境的问题,通常与数据库服务器的运行状态、网络连接或资源限制有关。这些错误通常是瞬态的(可能短暂出现,之后恢复正常,如网络抖动),或者表示数据库服务器不可用。
      • MySQL 错误码对应: 例如,2003 (Can’t connect to MySQL server), 2006 (MySQL server has gone away), 1040 (Too many connections), 1045 (Access denied).
    • IntegrityError (完整性错误):

      • 继承自: DatabaseError
      • PyMySQL 中的实现: pymysql.IntegrityError
      • 含义: 指示违反了数据库的完整性约束。这包括主键冲突、唯一键冲突、外键约束失败 (FOREIGN KEY constraint fails)、非空约束 (NOT NULL constraint fails) 等。
      • MySQL 错误码对应: 例如,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 中的实现: pymysql.InternalError
      • 含义: 指示数据库内部发生了意外错误。这通常是数据库服务器本身的 bug 或配置问题,应用程序难以直接处理或修复。
      • MySQL 错误码对应: 相对较少见,可能与数据库引擎崩溃或内部状态异常有关。
    • ProgrammingError (编程错误):

      • 继承自: DatabaseError
      • PyMySQL 中的实现: pymysql.ProgrammingError
      • 含义: 指示程序员在编写 SQL 语句或使用数据库 API 时出现的错误。例如,SQL 语法错误、表或列不存在、参数数量不匹配、操作权限不足等。
      • MySQL 错误码对应: 例如,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 中的实现: pymysql.NotSupportedError
      • 含义: 指示数据库驱动或数据库本身不支持所请求的功能。例如,尝试在一个不支持事务的数据库上开启事务,或者使用了数据库版本不支持的 SQL 特性。

理解这个层次结构可以帮助你编写更精确的错误处理逻辑。例如,你可以捕获 IntegrityError 来专门处理数据完整性问题,或者捕获更通用的 DatabaseError 来处理所有数据库引擎层面的错误。

2.8.2 错误码与 MySQL 状态包 (ERR_Packet) 的映射

当 MySQL 服务器遇到错误时,它会向客户端发送一个特殊的错误包 (ERR_Packet)。这个包包含了错误的关键信息,PyMySQL 接收到这个包后,会将其解析并转换为相应的 Python 异常。理解 ERR_Packet 的结构,有助于我们更深入地理解 PyMySQL 是如何报告错误的。

ERR_Packet 的基本结构如下:

  • Packet Header: payload_length (3 bytes), sequence_id (1 byte)
  • Payload:
    • 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_codesql_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: #

你可能感兴趣的:(python,开发语言)