MySQL锁问题一键诊断:从死锁到锁等待,这套脚本帮你快速定位根因

一、运维痛点:锁问题为什么总是难排查?

在 MySQL 运维场景中,锁问题是最常见也最棘手的故障类型之一:业务突然卡顿、接口超时、数据库连接打满,排查一圈发现是锁等待甚至死锁,但定位过程往往耗时费力。

传统排查方式存在三大痛点:

  1. 版本割裂 :MySQL 5.7 与 8.0 的锁系统表完全不同,8.0 移除了INNODB_LOCKS等系统表,老脚本直接失效;
  2. 信息碎片化:锁等待只看得到「谁在等」,看不到「谁在堵」,找不到阻塞源头就无法快速止损;
  3. 根因难追溯:只看到锁现象,看不到背后的长事务、慢SQL、隐式转换等根本诱因,问题反复出现。

本文分享的一键诊断脚本,正是为了解决这些问题------一条命令覆盖锁等待、死锁、长事务、慢查询全维度,兼容 5.7/8.0 全版本,从「发现现象」到「定位根因」一站式完成。

二、核心设计:锁问题诊断的完整闭环

一套好用的锁诊断工具,不能只罗列数据,而要形成「现象-链路-根因-方案」的完整闭环。脚本在设计上遵循四层诊断逻辑:

  1. 现状层:当前有没有锁等待?谁被阻塞了?阻塞了多久?
  2. 链路层:是谁造成了阻塞?阻塞事务在执行什么SQL?持有多少行锁?
  3. 根因层:是不是长事务导致锁长期不释放?是不是慢SQL拉长了锁持有时间?是不是隐式转换导致全表加锁?
  4. 历史层:历史上发生过多少次死锁?最近一次死锁的事务与SQL是什么?

基于这个逻辑,脚本设计了6个核心模块,逐层深入定位问题。

三、模块原理解析:每个诊断项的技术依据

1. 锁等待阻塞链:从「谁在等」到「谁在堵」

原脚本只查询了等待中的事务,这在实际运维中几乎没用------解决锁等待的核心是找到阻塞者,而不是等待者

优化后的脚本通过表关联,一次性输出完整阻塞链路:

  • MySQL 5.7:通过INNODB_LOCK_WAITS关联INNODB_TRX,同时取出等待事务与阻塞事务
  • MySQL 8.0:通过performance_schema.data_lock_waits + data_locks实现同等能力
  • 输出字段包含:事务ID、等待时长、锁类型/模式、锁定表与索引、阻塞SQL预览

拿到阻塞事务的连接ID后,直接KILL即可快速解除锁等待,实现分钟级止损。

2. 死锁双渠道检测:累计统计 + 详情回溯

死锁排查有两个核心需求:「发生了多少次」和「最近一次为什么发生」。

  • 累计统计 :读取全局状态Innodb_deadlocks,获取实例启动以来的死锁总数,评估死锁频发程度;
  • 详情回溯 :通过SHOW ENGINE INNODB STATUS提取最近一次死锁的完整信息,包括两个事务的SQL、加锁顺序、回滚的事务;
  • 日志补充 :若开启了innodb_print_all_deadlocks,还会从错误日志统计历史死锁数量,支持更长周期的分析。
3. 长事务:锁问题的万恶之源

绝大多数锁等待故障,本质都是长事务导致的------事务长时间不提交,就会一直持有行锁,阻塞后续所有对相同行的写入操作。

脚本新增了长事务诊断模块,可自定义时间阈值(默认10秒),输出运行中超长事务的:

  • 事务ID、连接ID、事务持续时间
  • 锁定行数、修改行数
  • 当前执行的SQL预览

这是排查锁问题的核心抓手:找到长事务,再分析为什么执行这么久,就能解决80%的锁等待问题。

4. 慢查询联动:锁持有时间的放大器

慢SQL本身不直接造成锁问题,但写入类慢SQL会大幅拉长锁的持有时间,把原本毫秒级的锁占用拉长到秒级,进而引发连锁式锁等待。

脚本适配两种慢查询分析方式:

  • 安装了pt-query-digest的环境,直接分析慢查询日志输出TOP慢SQL;
  • 无第三方工具的环境,通过performance_schema的语句统计表,输出累计耗时最高的SQL。
5. 隐式转换:最容易被忽略的锁诱因

隐式类型转换是很多开发容易踩的坑:字符串字段用数字查询(如WHERE phone = 13800000000),会导致索引失效,触发全表扫描。

对于UPDATE/DELETE语句,全表扫描意味着会对扫描过的所有行依次加锁,不仅性能极差,还极易引发死锁和大面积锁等待。脚本会自动扫描业务库中的高风险字符串字段,提示隐式转换风险。

四、使用指南与最佳实践

  1. 部署使用
bash 复制代码
# 赋予执行权限
chmod +x mysql-lock-diagnose.sh

# 本地数据库执行
./mysql-lock-diagnose.sh -uroot -p'your_password'

# 远程数据库执行
./mysql-lock-diagnose.sh -h 192.168.1.100 -P 3306 -uroot -p'your_password'
  1. 权限要求

    数据库账号需具备:PROCESS权限(查看所有连接与事务)、SELECT权限(访问系统库)、FILE权限非必需。

  2. 排查标准流程

    遇到业务卡顿、写入超时的时候:

    ① 运行脚本,先看「活跃锁等待」模块,找到阻塞事务ID与连接ID;

    ② 紧急情况直接KILL阻塞连接,先恢复业务;

    ③ 再看「长事务」与「慢查询」模块,定位事务执行慢的根因;

    ④ 最后结合「死锁详情」与「隐式转换提示」,做SQL与索引优化。

五、脚本

bash 复制代码
#!/bin/bash
# =====================================================
# mysql-lock-diagnose.sh - MySQL 全链路锁诊断工具
# 兼容 MySQL 5.7 / 8.0 | 行锁等待 + MDL锁 + 死锁 + 长事务 + 慢查询
# 用法:
#   文本模式(默认): ./mysql-lock-diagnose.sh [-h 主机] [-P 端口] [-u 用户] [-p密码]
#   JSON模式(对接监控): ./mysql-lock-diagnose.sh [mysql参数] --json
# 权限要求: PROCESS / SELECT / 系统库访问权限
# =====================================================
set -o pipefail

# -------------------------- 参数解析 --------------------------
OUTPUT_MODE="text"
MYSQL_ARGS=()
for arg in "$@"; do
    if [[ "$arg" == "--json" ]]; then
        OUTPUT_MODE="json"
    else
        MYSQL_ARGS+=("$arg")
    fi
done

# -------------------------- 配置区 --------------------------
if [[ "$OUTPUT_MODE" == "json" ]]; then
    RED='' GREEN='' YELLOW='' BLUE='' NC=''
else
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[1;33m'
    BLUE='\033[0;34m'
    NC='\033[0m'
fi

LONG_TRX_THRESHOLD=10   # 长事务阈值(秒)
SLOW_TOP_N=5            # 慢查询TOP数量
# -----------------------------------------------------------

MYSQL_CMD=("mysql" "${MYSQL_ARGS[@]}" "-N" "--default-character-set=utf8mb4")

# -------------------------- 工具函数 --------------------------
info()    { [[ "$OUTPUT_MODE" == "text" ]] && echo -e "${BLUE}[INFO]${NC}  $*"; }
warn()    { [[ "$OUTPUT_MODE" == "text" ]] && echo -e "${YELLOW}[WARN]${NC}  $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }
success() { [[ "$OUTPUT_MODE" == "text" ]] && echo -e "${GREEN}[OK]${NC}    $*"; }

print_line() { [[ "$OUTPUT_MODE" == "text" ]] && echo "--------------------------------------------"; }

print_title() {
    [[ "$OUTPUT_MODE" != "text" ]] && return
    echo ""
    echo -e "${BLUE}===== 【$1】 =====${NC}"
    print_line
}

# JSON字符串转义
json_escape() {
    local str="$*"
    str="${str//\\/\\\\}"
    str="${str//\"/\\\"}"
    str="${str//$'\n'/\\n}"
    str="${str//$'\r'/\\r}"
    str="${str//$'\t'/\\t}"
    echo -n "$str"
}

# MySQL连通性校验
check_conn() {
    if ! "${MYSQL_CMD[@]}" -e "SELECT 1;" &>/dev/null; then
        error "MySQL连接失败,请检查账号密码、主机端口及权限"
        exit 1
    fi
}

# 获取MySQL主版本号
get_version() {
    "${MYSQL_CMD[@]}" -e "SELECT VERSION();" 2>/dev/null | awk -F. '{print $1}'
}

# 获取MySQL系统变量
get_var() {
    "${MYSQL_CMD[@]}" -e "SHOW VARIABLES LIKE '$1';" 2>/dev/null | awk '{print $2}'
}

# 制表符结果转JSON数组(输入: 表头+多行制表符分隔数据)
rows_to_json() {
    local input="$1"
    [[ -z "$input" ]] && echo "[]" && return
    
    echo "$input" | awk -F'\t' '
    BEGIN { print "["; first=1 }
    {
        if (!first) printf ",\n"
        first=0
        printf "  {"
        for (i=1; i<=NF; i++) {
            key = headers[i]
            val = $i
            gsub(/\\/, "\\\\", val)
            gsub(/"/, "\\\"", val)
            gsub(/\n/, "\\n", val)
            gsub(/\r/, "\\r", val)
            if (i>1) printf ", "
            printf "\"%s\":\"%s\"", key, val
        }
        printf "}"
    }
    END { print "\n]" }
    ' headers="$(echo "$input" | head -1 | tr '\t' '\n' | sed 's/"/\\"/g')"
}
# -----------------------------------------------------------

# ========== 初始化与前置检查 ==========
REPORT_TIME=$(date '+%Y-%m-%d %H:%M:%S')

if [[ "$OUTPUT_MODE" == "text" ]]; then
    echo "============================================"
    echo -e " ${GREEN}MySQL 全链路锁诊断报告${NC}"
    echo " 生成时间: $REPORT_TIME"
    echo "============================================"
fi

check_conn
MYSQL_VER=$(get_version)
info "MySQL 主版本: ${MYSQL_VER}.x"

LOG_ERROR=$(get_var "log_error")
SLOW_LOG_ON=$(get_var "slow_query_log")
SLOW_LOG_FILE=$(get_var "slow_query_log_file")
DEADLOCK_PRINT=$(get_var "innodb_print_all_deadlocks")

# ========== 【1】活跃行锁等待 ==========
print_title "1. 活跃行锁等待与阻塞链路"

if [[ "$MYSQL_VER" -ge 8 ]]; then
    LOCK_SQL="
SELECT
    w.WAITING_ENGINE_TRANSACTION_ID AS waiting_trx_id,
    TIMESTAMPDIFF(SECOND, trx.trx_started, NOW()) AS wait_sec,
    l.LOCK_TYPE AS lock_type,
    l.LOCK_MODE AS lock_mode,
    CONCAT(l.OBJECT_SCHEMA, '.', l.OBJECT_NAME) AS lock_table,
    l.INDEX_NAME AS lock_index,
    w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id,
    trx_b.trx_mysql_thread_id AS blocking_conn_id,
    LEFT(trx_b.trx_query, 100) AS blocking_query,
    LEFT(trx.trx_query, 100) AS waiting_query
FROM performance_schema.data_lock_waits w
JOIN performance_schema.data_locks l ON w.WAITING_ENGINE_LOCK_ID = l.ENGINE_LOCK_ID
JOIN information_schema.INNODB_TRX trx ON w.WAITING_ENGINE_TRANSACTION_ID = trx.trx_id
JOIN information_schema.INNODB_TRX trx_b ON w.BLOCKING_ENGINE_TRANSACTION_ID = trx_b.trx_id
ORDER BY trx.trx_started ASC;"
else
    LOCK_SQL="
SELECT
    r.trx_id AS waiting_trx_id,
    TIMESTAMPDIFF(SECOND, r.trx_started, NOW()) AS wait_sec,
    l.lock_type AS lock_type,
    l.lock_mode AS lock_mode,
    CONCAT(l.object_schema, '.', l.object_name) AS lock_table,
    l.index_name AS lock_index,
    b.trx_id AS blocking_trx_id,
    b.trx_mysql_thread_id AS blocking_conn_id,
    LEFT(b.trx_query, 100) AS blocking_query,
    LEFT(r.trx_query, 100) AS waiting_query
FROM information_schema.INNODB_LOCK_WAITS w
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.INNODB_LOCKS l ON l.lock_id = w.requested_lock_id
ORDER BY r.trx_started ASC;"
fi

LOCK_RAW=$("${MYSQL_CMD[@]}" -e "$LOCK_SQL" 2>/dev/null)
LOCK_WAITS_JSON=$(rows_to_json "$LOCK_RAW")

if [[ "$OUTPUT_MODE" == "text" ]]; then
    if [[ -z "$LOCK_RAW" ]]; then
        success "当前无活跃行锁等待"
    else
        echo "$LOCK_RAW" | awk -F'\t' '
        {
            printf "等待事务: %s | 等待时长: %ss | 锁: %s(%s) %s %s\n", $1, $2, $5, $6, $3, $4
            printf "  阻塞事务: %s | 连接ID: %s\n", $7, $8
            printf "  阻塞SQL: %s\n", $9
            printf "  等待SQL: %s\n\n", $10
        }'
    fi
fi

# ========== 【2】MDL元数据锁等待 ==========
print_title "2. MDL元数据锁等待"

if [[ "$MYSQL_VER" -ge 8 ]]; then
    MDL_SQL="
SELECT
    w.OBJECT_SCHEMA AS db_name,
    w.OBJECT_NAME AS table_name,
    w.LOCK_TYPE AS lock_type,
    TIMESTAMPDIFF(SECOND, w.LOCKED, NOW()) AS wait_sec,
    t.PROCESSLIST_ID AS waiting_conn_id,
    LEFT(t.PROCESSLIST_INFO, 100) AS waiting_query,
    t2.PROCESSLIST_ID AS holding_conn_id,
    LEFT(t2.PROCESSLIST_INFO, 100) AS holding_query,
    p2.STATE AS holding_state
FROM performance_schema.metadata_locks w
JOIN performance_schema.metadata_locks h
  ON w.OBJECT_SCHEMA = h.OBJECT_SCHEMA
  AND w.OBJECT_NAME = h.OBJECT_NAME
  AND w.LOCK_STATUS = 'PENDING'
  AND h.LOCK_STATUS = 'GRANTED'
JOIN performance_schema.threads t ON w.OWNER_THREAD_ID = t.THREAD_ID
JOIN performance_schema.threads t2 ON h.OWNER_THREAD_ID = t2.THREAD_ID
LEFT JOIN information_schema.PROCESSLIST p2 ON t2.PROCESSLIST_ID = p2.ID
ORDER BY wait_sec DESC;"
else
    SYS_EXISTS=$("${MYSQL_CMD[@]}" -e "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='sys';" 2>/dev/null)
    if [[ "$SYS_EXISTS" -gt 0 ]]; then
        MDL_SQL="
SELECT
    object_schema AS db_name,
    object_name AS table_name,
    waiting_lock_mode AS lock_type,
    ROUND(wait_age_seconds, 0) AS wait_sec,
    waiting_pid AS waiting_conn_id,
    LEFT(waiting_query, 100) AS waiting_query,
    blocking_pid AS holding_conn_id,
    LEFT(blocking_query, 100) AS holding_query,
    blocking_lock_mode AS holding_lock_mode
FROM sys.schema_table_lock_waits
ORDER BY wait_age_seconds DESC;"
    else
        MDL_SQL="
SELECT
    DB AS db_name,
    '' AS table_name,
    'METADATA' AS lock_type,
    TIME AS wait_sec,
    ID AS waiting_conn_id,
    LEFT(INFO, 100) AS waiting_query,
    '' AS holding_conn_id,
    '需开启sys库或升级MySQL8.0以定位阻塞源' AS holding_query,
    '降级检测' AS holding_lock_mode
FROM information_schema.PROCESSLIST
WHERE STATE LIKE '%Waiting for table metadata lock%'
ORDER BY TIME DESC;"
    fi
fi

MDL_RAW=$("${MYSQL_CMD[@]}" -e "$MDL_SQL" 2>/dev/null)
MDL_WAITS_JSON=$(rows_to_json "$MDL_RAW")

if [[ "$OUTPUT_MODE" == "text" ]]; then
    if [[ -z "$MDL_RAW" ]]; then
        success "当前无MDL元数据锁等待"
    else
        echo "$MDL_RAW" | awk -F'\t' '
        {
            printf "等待库表: %s.%s | 锁类型: %s | 等待时长: %ss\n", $1, $2, $3, $4
            printf "  等待连接ID: %s | SQL: %s\n", $5, $6
            printf "  持有连接ID: %s | 状态: %s\n", $7, $9
            printf "  持有端SQL: %s\n\n", $8
        }'
        warn "MDL锁通常由长事务、未提交事务、慢DDL引发,会阻塞所有表读写"
    fi
fi

# ========== 【3】死锁统计与详情 ==========
print_title "3. 死锁统计与详情回溯"

DEADLOCK_TOTAL=$("${MYSQL_CMD[@]}" -e "SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks';" 2>/dev/null | awk '{print $2}')
DEADLOCK_TOTAL=${DEADLOCK_TOTAL:-0}

# 最近一次死锁详情
DEADLOCK_DETAIL=$("${MYSQL_CMD[@]}" -e "SHOW ENGINE INNODB STATUS\G" 2>/dev/null \
    | grep -A 50 "LATEST DETECTED DEADLOCK" | tail -n +2 | head -30)

# 错误日志死锁统计
LOG_DEADLOCK=0
if [[ -n "$LOG_ERROR" && -f "$LOG_ERROR" ]]; then
    LOG_DEADLOCK=$(grep -c "WE ROLL BACK" "$LOG_ERROR" 2>/dev/null || echo 0)
fi

if [[ "$OUTPUT_MODE" == "text" ]]; then
    echo -e " 实例启动累计死锁: ${YELLOW}${DEADLOCK_TOTAL}${NC} 次"
    echo -e " 错误日志记录死锁: ${YELLOW}${LOG_DEADLOCK}${NC} 次 (日志路径: ${LOG_ERROR:-未配置})"
    echo ""
    echo " 最近一次死锁详情:"
    [[ -n "$DEADLOCK_DETAIL" ]] && echo "$DEADLOCK_DETAIL" || echo "  无死锁记录"
    
    if [[ "$DEADLOCK_PRINT" != "ON" ]]; then
        warn "未开启 innodb_print_all_deadlocks,仅保留最近1次死锁记录"
    fi
fi

# 死锁JSON序列化
DEADLOCK_DETAIL_ESC=$(json_escape "$DEADLOCK_DETAIL")
DEADLOCK_JSON="{
  \"total_count\": $DEADLOCK_TOTAL,
  \"error_log_count\": $LOG_DEADLOCK,
  \"latest_detail\": \"$DEADLOCK_DETAIL_ESC\"
}"

# ========== 【4】运行中长事务 ==========
print_title "4. 运行中长事务列表 (>${LONG_TRX_THRESHOLD}s)"

TRX_SQL="
SELECT
    trx_id,
    trx_state,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
    trx_mysql_thread_id AS conn_id,
    trx_rows_locked AS rows_locked,
    trx_rows_modified AS rows_modified,
    LEFT(trx_query, 100) AS query_preview
FROM information_schema.INNODB_TRX
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > $LONG_TRX_THRESHOLD
ORDER BY trx_started ASC;"

TRX_RAW=$("${MYSQL_CMD[@]}" -e "$TRX_SQL" 2>/dev/null)
LONG_TRX_JSON=$(rows_to_json "$TRX_RAW")

if [[ "$OUTPUT_MODE" == "text" ]]; then
    if [[ -z "$TRX_RAW" ]]; then
        success "无运行中超长事务"
    else
        echo "$TRX_RAW" | awk -F'\t' '
        {
            printf "事务ID: %s | 状态: %s | 持续: %ss | 连接ID: %s\n", $1, $2, $3, $4
            printf "  锁定行数: %s | 修改行数: %s\n", $5, $6
            printf "  当前SQL: %s\n\n", $7
        }'
        warn "长事务持续持有行锁,是引发大面积锁等待的首要原因"
    fi
fi

# ========== 【5】慢查询 TOP 分析 ==========
print_title "5. 慢查询 TOP ${SLOW_TOP_N}"

SLOW_JSON="[]"
if [[ "$SLOW_LOG_ON" != "ON" ]]; then
    warn "慢查询日志未开启,无法分析慢SQL"
    SLOW_JSON="[{\"note\": \"慢查询日志未开启\"}]"
else
    if [[ "$OUTPUT_MODE" == "text" ]]; then
        echo " 慢查询日志路径: $SLOW_LOG_FILE"
        echo ""
    fi
    
    # JSON模式/无pt工具时,用performance_schema结构化数据
    if [[ "$OUTPUT_MODE" == "json" ]] || ! command -v pt-query-digest &>/dev/null; then
        SLOW_SQL="
SELECT
    SCHEMA_NAME AS db_name,
    LEFT(DIGEST_TEXT, 80) AS query_preview,
    COUNT_STAR AS exec_times,
    ROUND(AVG_TIMER_WAIT/1000000000, 2) AS avg_time_sec,
    ROUND(SUM_TIMER_WAIT/1000000000, 2) AS total_time_sec,
    SUM_ROWS_EXAMINED AS total_rows_scan
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT $SLOW_TOP_N;"
        SLOW_RAW=$("${MYSQL_CMD[@]}" -e "$SLOW_SQL" 2>/dev/null)
        SLOW_JSON=$(rows_to_json "$SLOW_RAW")
        
        if [[ "$OUTPUT_MODE" == "text" ]]; then
            [[ -z "$SLOW_RAW" ]] && echo "  performance_schema无数据" || echo "$SLOW_RAW" | column -t
        fi
    else
        # 文本模式且有pt工具,使用pt-query-digest
        pt-query-digest --limit $SLOW_TOP_N "$SLOW_LOG_FILE" 2>/dev/null \
            | grep -E "Rank|Query ID|^#.*Time|^#.*Examined|^#.*P95|^#.*Rows sent" \
            | head -30
        SLOW_JSON="[{\"note\": \"pt-query-digest文本输出,无结构化数据\"}]"
    fi
fi

# ========== 【6】隐式类型转换风险 ==========
print_title "6. 隐式类型转换风险提示"

RISK_SQL="
SELECT
    CONCAT(TABLE_SCHEMA, '.', TABLE_NAME, '.', COLUMN_NAME) AS column_full_name,
    DATA_TYPE AS data_type
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA NOT IN ('information_schema','performance_schema','mysql','sys')
  AND DATA_TYPE IN ('varchar','char')
  AND (COLUMN_NAME LIKE '%id' OR COLUMN_NAME LIKE '%code' OR COLUMN_NAME LIKE '%no')
ORDER BY TABLE_SCHEMA, TABLE_NAME
LIMIT 20;"

RISK_RAW=$("${MYSQL_CMD[@]}" -e "$RISK_SQL" 2>/dev/null)
RISK_JSON=$(rows_to_json "$RISK_RAW")

if [[ "$OUTPUT_MODE" == "text" ]]; then
    info "字符串字段用数字查询会触发全表扫描加锁,极易引发死锁"
    echo ""
    [[ -z "$RISK_RAW" ]] && echo "  未检测到高风险字段" || echo "$RISK_RAW" | awk -F'\t' '{printf "  %s (%s)\n", $1, $2}'
fi

# ========== 最终输出 ==========
if [[ "$OUTPUT_MODE" == "json" ]]; then
    REPORT_TIME_ESC=$(json_escape "$REPORT_TIME")
    cat <<EOF
{
  "report_time": "$REPORT_TIME_ESC",
  "mysql_version": "$MYSQL_VER",
  "row_lock_waits": $LOCK_WAITS_JSON,
  "mdl_lock_waits": $MDL_WAITS_JSON,
  "deadlocks": $DEADLOCK_JSON,
  "long_transactions": $LONG_TRX_JSON,
  "slow_queries": $SLOW_JSON,
  "type_conversion_risks": $RISK_JSON,
  "summary": {
    "has_row_lock_wait": $(echo "$LOCK_RAW" | grep -c . &>/dev/null && echo true || echo false),
    "has_mdl_lock_wait": $(echo "$MDL_RAW" | grep -c . &>/dev/null && echo true || echo false),
    "long_trx_count": $(echo "$LONG_TRX_JSON" | grep -c '"trx_id"' || echo 0),
    "deadlock_total": $DEADLOCK_TOTAL
  }
}
EOF
    exit 0
fi

# 文本模式总结
print_title "7. 诊断总结与排查指南"
echo " 1. 紧急止损:锁等待爆发时,KILL阻塞事务的conn_id快速恢复业务"
echo " 2. MDL锁处理:找到持有锁的长事务连接,优先提交或回滚事务"
echo " 3. 死锁优化:统一加锁顺序、缩小事务粒度、避免无索引更新"
echo " 4. 根因治理:杜绝长事务、关闭隐式转换、开启慢查询与死锁日志"
echo " 5. 监控对接:追加 --json 参数可输出结构化数据对接监控平台"

echo ""
echo "============================================"
echo -e " ${GREEN}诊断完成${NC}"
echo "============================================"
exit 0

六、写在最后

MySQL 锁问题看似复杂,本质上都是「锁持有时间太长」或「加锁范围太大」导致的。这套脚本的价值,是把零散的排查命令系统化,把依赖经验的判断标准化,让一线运维和DBA在故障发生时,能快速拿到完整信息,把更多时间花在解决问题上,而不是拼凑信息上。