mysql 如何处理僵尸事务(空等待不提交也不回滚的事务)

创建存储过程,处理僵尸事务线程,如果发现僵尸事务线程超过2分钟不提交不回滚,杀死线程。

CREATE DEFINER=`root`@`%` PROCEDURE `kill_idle_transactions`()
BEGIN
    #定义变量
    DECLARE done INT DEFAULT FALSE; -- 游标中止状态的默认值
    DECLARE kill_command VARCHAR(255);  -- 杀死线程的命令
    DECLARE trx_started DATETIME;  -- 事务的开始时间
    DECLARE process_id BIGINT; -- 事务线程的id
    DECLARE process_user VARCHAR(16); -- 执行事务线程的用户
    DECLARE process_host VARCHAR(64); -- 执行事务线程所在的机器
    DECLARE process_db VARCHAR(64); -- 当前事务线程所在的数据库
    DECLARE process_command VARCHAR(16); -- 当前事务线程的执行状态
    DECLARE process_time INT; -- 当前事务线程所执行语句的执行时间
    DECLARE process_state VARCHAR(64); -- 当前事务线程的状态
    DECLARE process_info LONGTEXT; -- 当前事务线程执行的语句
		
    -- 定义游标 这里查询的是没有执行语句,没有提交,空等待并且睡眠中的事务链接以及相关的附加消息, 
    -- 同一个事务的执行时间{process.TIME}每次执行语句都会重置, 如果出现时间特别长的并且执行语句{process.INFO}为空,就可以判断是僵尸事务线程。
    DECLARE cur CURSOR FOR
    SELECT
      CONCAT('KILL ', trx_mysql_thread_id, ';') AS kill_command,  -- 拼接杀死线程的命令
      trx.trx_started, -- 事务的开始时间
      process.ID, -- 事务线程的id
      process.USER, -- 执行事务线程的用户
      process.HOST, -- 执行事务线程所在的机器
      process.DB, -- 当前事务线程所在的数据库
      process.COMMAND, -- 当前事务线程的执行状态
      process.TIME,  -- 当前事务线程当前语句的执行时间
      process.STATE, -- 当前事务线程的状态
      process.INFO -- 当前事务线程执行的语句
    FROM
      information_schema.innodb_trx trx
    LEFT JOIN information_schema.PROCESSLIST process ON trx_mysql_thread_id = process.ID
    WHERE
      process.db =  DATABASE()  -- 判断是当前数据库的
      AND process.INFO IS NULL -- 判断当前执行语句为空的
      AND process.`command`='Sleep'; -- 判断当前事务执行状态为睡眠状态的
    -- 声明 CONTINUE HANDLER 处理游标完成后的状态
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
		
		
		
		#创建日志表
    SET @create_table_sql = NULL;

    -- 动态生成表名
    -- SET @dynamic_table_name = CONCAT('event_kill_idle_tran_log_', DATABASE());
    SET @dynamic_table_name = 'event_kill_idle_tran_log';
    -- 构建创建表的SQL语句
    SET @create_table_sql = CONCAT(
        'CREATE TABLE IF NOT EXISTS ', @dynamic_table_name, ' (
            execution_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT ''执行时间'',
            kill_command VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT ''杀死线程的命令'',
            trx_started DATETIME DEFAULT NULL COMMENT ''事务的开始时间'',
            process_id BIGINT DEFAULT NULL COMMENT ''事务线程的id'',
            process_user VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT ''执行事务线程的用户'',
            process_host VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT ''执行事务线程所在的机器'',
            process_db VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT ''当前事务线程所在的数据库'',
            process_command VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT ''当前事务线程的执行状态'',
            process_time INT DEFAULT NULL COMMENT ''当前事务线程当前语句的执行时间'',
            process_state VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT ''当前事务线程的状态'',
            process_info LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT ''当前事务线程执行的语句'',
            PRIMARY KEY (execution_time)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT=''记录杀死僵尸线程的日志消息''
    ');

    -- 执行创建表的SQL语句
    PREPARE create_table_stmt FROM @create_table_sql;
    EXECUTE create_table_stmt;
    DEALLOCATE PREPARE create_table_stmt;
		
		
		
    -- 打开游标
    OPEN cur;

    -- 循环遍历游标中的每一行
    read_loop: LOOP
        FETCH cur INTO kill_command, trx_started, process_id, process_user, process_host, process_db, process_command, process_time, process_state, process_info;
        IF done THEN
            LEAVE read_loop;
        END IF;

        -- 在这里你可以插入到你的表中,示例为:
        SET @insert_sql = CONCAT('
            INSERT INTO ', @dynamic_table_name, ' (
                kill_command, trx_started, process_id, process_user, process_host, process_db,
                process_command, process_time, process_state, process_info
            ) VALUES ('
                ,IFNULL(CONCAT("'",kill_command,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",trx_started,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_id,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_user,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_host,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_db,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_command,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_time,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_state,"'"),'NULL'), ','
                ,IFNULL(CONCAT("'",process_info,"'"),'NULL'), ')');

    -- 如果执行时间超过120秒 并且执行语句为空的 就杀死线程并记录日志
    IF process_time > 120 AND process_info IS NULL THEN
            -- 执行插入操作
            PREPARE insert_stmt FROM @insert_sql;
            EXECUTE insert_stmt;
            DEALLOCATE PREPARE insert_stmt;

            -- 执行 KILL 命令
            SET @kill_stmt = kill_command;
            PREPARE kill_stmt FROM @kill_stmt;
            EXECUTE kill_stmt;
            DEALLOCATE PREPARE kill_stmt;
    END IF;
    END LOOP;

    -- 关闭游标
    CLOSE cur;
		

END// 
DELIMITER ;

创建事件每分钟执行一次,执行存储过程逻辑

DELIMITER //
CREATE EVENT `kill_idle_transactions` 
ON SCHEDULE EVERY 1 MINUTE -- 每一分钟执行一次
STARTS '2024-08-15 16:56:18' -- 开始执行时间为2024-08-15 16:56:18
ON COMPLETION NOT PRESERVE -- 事件完成后不保留(即事件完成后不会自动重新调度)
ENABLE -- 启用该事件
COMMENT '每分钟执行杀死僵尸线程的存储过程' -- 事件的描述性注释,解释事件的用途
DO
BEGIN
    CALL kill_idle_transactions(); -- 调用存储过程 kill_idle_transactions
END;// 
DELIMITER;