一次系统响应慢的排查

先说一下,系统的相关信息,一共两个系统分别称呼是 A 和 B 吧。1. 两个系统都没有源码;2. A 是 springboot 项目,B 是传统的 Tomcat;3. 两者都是使用内网 IP 地址连接同一个数据库;4. 两个系统包括数据库都是一个月前老服务器迁移到了新服务器。表现是 A 系统也就是 springboot 系统响应速度正常,B 系统也就是那个 Tomcat 系统响应速度相对正常,就是有一点慢,但是一直没有放在心上,内部系统,能用,有一天发现 B 系统的某个接口特别特别慢,以至于会响应超时,然后排查这个问题,还有就是目前只发现了这一个接口慢,A 系统有一个类似功能的接口,响应速度也正常。 当时看到这个响应,第一反应:B 系统有个配置错了,因为一个月前刚迁移了系统,而 B 系统一共有三个配置文件,需要配置数据库连接,有个地方忘记修改了,刚好这个接口用的是这个系统的配置文件的连接地址,所以调用数据库连接超时报错,经排查发现三个地址的配置均修改了,是正确的。 在上一步的排查中也没有发现有任何报错日志,所以第二反应,出现了慢查询,但是感觉不太应该,数据库是完整迁移的,以前也不慢啊,而且 A 系统有个功能类似的接口,响应速度也算正常。其实正常是看慢查询日志,看有没有相关的 SQL,但是系统没有记录,所以通过 SQL 语句查询: SELECT trx_id AS 事务ID, trx_state AS 状态, trx_started AS 开始时间, trx_mysql_thread_id AS 线程ID, TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) AS 已运行秒数, trx_query AS 当前SQL FROM information_schema.INNODB_TRX ORDER BY trx_started\G 经查询,调用这个接口的时候,没有超长执行的 SQL 语句所以排除。 然后下一个排查方向是不是 jdbc 的驱动版本不对,B 系统的 jdbc 版本还是 5 点几,而数据库的版本已经是 8 点几,这有点对不上啊,大模型说 8 点几的认证方式修改了,所以那就修改 jdbc 的版本测试,然后没有任何变化。 ...

February 2, 2026 · 2 min · 222 words · Bridge Li

MySQL 备份及恢复脚本

作为开发,我们都知道数据备份的重要性,而数据备份最重要的就是数据库备份,前一段时间由于操作失误,误删过一次数据库,所以特把备份和恢复脚本分享出来,作为笔记。 MySQL 备份脚本 #!/bin/bash \# =================================================================== \# MySQL 分库全量备份脚本(生产级 | 自适应 &#8211;source-data / &#8211;master-data) \# 功能: \# &#8211; 自动发现用户数据库 \# &#8211; 每库独立压缩备份 \# &#8211; 自动选择 &#8211;source-data (8.0+) 或 &#8211;master-data (5.7) \# &#8211; 智能处理 GTID(仅在启用时设置) \# &#8211; 提取 binlog 位置生成 .info 文件 \# &#8211; 清理 N 天前旧备份 \# 作者:BridgeLi \# 版本:1.0 \# =================================================================== \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 配置区 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- BACKUP_DIR="/project/backup/mysql/dbs" CNF_FILE="/project/backup/mysql/my.cnf" RETENTION_DAYS=7 MIN_FREE_SPACE_GB=5 HOSTNAME=$(hostname -s) DT=$(date +%Y-%m-%d_%H%M%S) \# 排除系统库 EXCLUDED_DBS="^(mysql|sys|information_schema|performance_schema)$" \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 初始化 & 依赖检查 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- for cmd in mysql mysqldump gzip gunzip df date awk sed; do command -v "$cmd" >/dev/null || { echo "错误:缺少命令 &#8216;$cmd&#8217;"; exit 1; } done mkdir -p "$BACKUP_DIR" || { echo "错误:无法创建目录 $BACKUP_DIR"; exit 1; } LOG_FILE="$BACKUP_DIR/backup.log" LOCK_FILE="$BACKUP_DIR/.backup.lock" if [ -f "$LOCK_FILE" ]; then echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 错误:锁文件存在,可能已有备份在运行。" exit 1 fi trap "rm -f &#8216;$LOCK_FILE&#8217;" EXIT touch "$LOCK_FILE" AVAILABLE_GB=$(df -P "$BACKUP_DIR" | tail -1 | awk &#8216;{print int($4/1024/1024)}&#8217;) if [ "$AVAILABLE_GB" -lt "$MIN_FREE_SPACE_GB" ]; then echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 错误:磁盘空间不足 (${AVAILABLE_GB}GB < ${MIN_FREE_SPACE_GB}GB)" exit 1 fi exec > >(tee -a "$LOG_FILE") 2>&1 echo "\[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)\] \[$HOSTNAME\] 开始全量分库备份&#8230;" \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 检测 MySQL 版本和 GTID 状态 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- MYSQL_CMD="mysql &#8211;defaults-extra-file=$CNF_FILE -sN" \# 获取 MySQL 主版本(57, 80) MYSQL_VERSION=$($MYSQL_CMD -e "SELECT REPLACE(LEFT(VERSION(), 4), &#8216;.&#8217;, &#8221;);") if ! [[ "$MYSQL_VERSION" =~ ^[0-9]+$ ]]; then echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 错误:无法获取 MySQL 版本" exit 1 fi \# 选择 source-data / master-data if [ "$MYSQL_VERSION" -ge 80 ]; then REPLICATION_OPT="&#8211;source-data=2" else REPLICATION_OPT="&#8211;master-data=2" fi \# 检查 GTID 是否启用 GTID_MODE=$($MYSQL_CMD -e "SELECT @@GLOBAL.gtid_mode;" 2>/dev/null || echo "OFF") if [[ "$GTID_MODE" =~ ^(ON|ON_PERMISSIVE|OFF_PERMISSIVE)$ ]]; then GTID_PURGED_OPT="&#8211;set-gtid-purged=ON" GTID_ENABLED=true else GTID_PURGED_OPT="&#8211;set-gtid-purged=OFF" GTID_ENABLED=false fi echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] MySQL 版本: $MYSQL_VERSION, 使用: $REPLICATION_OPT, GTID: ${GTID_MODE:-OFF}" \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 获取所有非系统数据库 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- dbs=() while IFS= read -r db; do [[ -z "$db" ]] && continue if [[ ! "$db" =~ $EXCLUDED_DBS ]]; then dbs+=("$db") fi done < <($MYSQL_CMD -e "SHOW DATABASES;" 2>>"$LOG_FILE") if [ ${#dbs[@]} -eq 0 ]; then echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 警告:未找到可备份的数据库。" exit 0 fi echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 发现 ${#dbs[@]} 个数据库: ${dbs[*]}" \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 执行备份 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- SUCCESS_COUNT=0 FAILURE_COUNT=0 START_TIME=$(date +%s) for db in "${dbs[@]}"; do DUMP_FILE="${db}-${DT}.sql.gz" DUMP_PATH="$BACKUP_DIR/$DUMP_FILE" INFO_PATH="${DUMP_PATH%.gz}.info" echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 正在备份: $db -> $DUMP_PATH" mysqldump &#8211;defaults-extra-file="$CNF_FILE" \ &#8211;single-transaction \ &#8211;routines \ &#8211;triggers \ $REPLICATION_OPT \ $GTID_PURGED_OPT \ &#8211;databases "$db" 2>>"$LOG_FILE" | gzip -c > "$DUMP_PATH" if [ $? -eq 0 ] && [ -s "$DUMP_PATH" ] && gunzip -t "$DUMP_PATH" >/dev/null 2>&1; then \# 提取 binlog 位置(source/master 兼容) read master_file master_pos < <(gzip -dc "$DUMP_PATH" | sed -n "/^&#8211; CHANGE MASTER TO / s/.\*LOG_FILE=&#8217;\\([^&#8217;]\*\\)&#8217;,.\*LOG_POS=\\([0-9]\*\\).*/\\1 \\2/p" | head -1) \# 提取 GTID(仅当启用) if [ "$GTID_ENABLED" = true ]; then gtid_purged=$(gzip -dc "$DUMP_PATH" | sed -n "s/^SET @@GLOBAL.GTID_PURGED=&#8217;\\([^&#8217;]*\)&#8217;;\$/\\1/p" | head -1) [ -z "$gtid_purged" ] && gtid_purged="NONE" else gtid_purged="DISABLED" fi \# 写入 .info { [ -n "$master_file" ] && echo "File: $master_file" [ -n "$master_pos" ] && echo "Position: $master_pos" echo "GTID: $gtid_purged" } > "$INFO_PATH" chmod 600 "$DUMP_PATH" "$INFO_PATH" 2>/dev/null || true echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 成功: $db (binlog: $master_file, pos: $master_pos, gtid: $gtid_purged)" ((SUCCESS_COUNT++)) else echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 失败: $db" rm -f "$DUMP_PATH" "$INFO_PATH" ((FAILURE_COUNT++)) fi done \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 清理 N 天前的旧备份 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- if [ $FAILURE_COUNT -eq 0 ]; then cleanup_old() { local pattern="$1" local now_ts=$(date +%s) local files=() mapfile -t files < <(find "$BACKUP_DIR" -name "$pattern" -type f 2>/dev/null) for file in "${files[@]}"; do if [[ "$file" =~ -([0-9]{4})-([0-9]{2})-([0-9]{2})_([0-9]{2})([0-9]{2})([0-9]{2})\. ]]; then \# 提取各部分:年、月、日、时、分、秒 local y=${BASH_REMATCH[1]} local m=${BASH_REMATCH[2]} local d=${BASH_REMATCH[3]} local H=${BASH_REMATCH[4]} local M=${BASH_REMATCH[5]} local S=${BASH_REMATCH[6]} \# 构造合法日期字符串:2025-09-25 02:00:01 local datetime="$y-$m-$d $H:$M:$S" \# 转换为时间戳 local file_ts=$(date -d "$datetime" +%s 2>/dev/null) || continue local age_days=$(( (now_ts &#8211; file_ts) / 86400 )) if [ $age_days -ge $RETENTION_DAYS ]; then echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 清理过期文件: $file (已存在 $age_days 天)" rm -f "$file" fi fi done } cleanup_old "*.sql.gz" cleanup_old "*.info" END_TIME=$(date +%s) echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 备份完成: 成功 $SUCCESS_COUNT,失败 $FAILURE_COUNT,耗时 $((END_TIME &#8211; START_TIME)) 秒" else echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 警告:有 $FAILURE_COUNT 个数据库备份失败,跳过清理。" exit 1 fi exit 0 MySQL binlog 备份脚本: #!/bin/bash \# =================================================================== \# MySQL Binlog 增量备份脚本(优化版) \# 配合全量备份实现 PITR \# 作者:BridgeLi \# 版本:1.0 \# =================================================================== \# 配置参数 BINLOG_DIR="/var/lib/mysql" BACKUP_DIR="/project/backup/mysql/binlogs" CNF_FILE="/project/backup/mysql/my.cnf" LOG_FILE="$BACKUP_DIR/binlog_backup.log" LOCK_FILE="$BACKUP_DIR/.backup.lock" LAST_COPIED_FILE="$BACKUP_DIR/.last_binlog" \# 创建备份目录 mkdir -p "$BACKUP_DIR" || { echo "[$(date)] 错误:无法创建备份目录 $BACKUP_DIR" >&2; exit 1; } \# 使用 flock 防止并发执行 exec 200>"$LOCK_FILE" if ! flock -n 200; then echo "[$(date)] 错误:备份脚本已在运行,退出。" | tee -a "$LOG_FILE" exit 1 fi \# 重定向所有输出到日志 exec >> "$LOG_FILE" 2>&1 echo "==================================" echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 开始 binlog 增量备份&#8230;" \# 获取当前活跃 binlog CURRENT_LOG=$(mysql &#8211;defaults-extra-file="$CNF_FILE" -sN -e "SHOW MASTER STATUS;" 2>/dev/null | awk &#8216;{print $1}&#8217;) if [ -z "$CURRENT_LOG" ]; then echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 错误:无法获取当前 binlog 名称,请检查 MySQL 连接或权限。" exit 1 fi echo "当前活跃 binlog: $CURRENT_LOG" \# 读取上次备份的 binlog if [[ -f "$LAST_COPIED_FILE" ]]; then LAST_LOG=$(cat "$LAST_COPIED_FILE") echo "上次已备份至: $LAST_LOG" else LAST_LOG="" echo "首次运行,将备份所有历史 binlog(除当前外)。" fi COPIED=0 cd "$BINLOG_DIR" || { echo "无法进入 binlog 目录: $BINLOG_DIR"; exit 1; } \# 获取所有 binlog 文件并按版本排序 mapfile -t LOGS < <(find . -maxdepth 1 -name &#8216;mysql-bin.*&#8217; -type f -printf &#8216;%f\n&#8217; | sort -V) for log in "${LOGS[@]}"; do [[ ! -f "$log" ]] && continue \# 跳过当前活跃的 binlog [[ "$log" == "$CURRENT_LOG" ]] && continue \# 判断是否需要备份:log > LAST_LOG(版本排序) if [[ -n "$LAST_LOG" ]]; then \# 使用 sort -V 判断顺序 greater=$(printf &#8216;%s\n%s&#8217; "$LAST_LOG" "$log" | sort -V | tail -1) if [[ "$greater" != "$log" || "$log" == "$LAST_LOG" ]]; then continue fi fi \# 执行压缩备份 if gzip -c "$log" > "$BACKUP_DIR/${log}.gz"; then echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 已压缩备份: $log" ((COPIED++)) else echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] 错误:备份失败 $log" fi done \# 原子更新最后备份的 binlog echo "$CURRENT_LOG" > "${LAST_COPIED_FILE}.tmp" && mv "${LAST_COPIED_FILE}.tmp" "$LAST_COPIED_FILE" echo "[$(date +&#8217;%Y-%m-%d %H:%M:%S&#8217;)] binlog 增量备份完成,共复制 $COPIED 个文件。" echo "==================================" MySQL 健康检查脚本(非必需,只需要备份就行) #!/bin/bash \# =================================================================== \# MySQL 备份健康检查脚本(优化生产版 &#8211; 已修复) \# 功能:检查最近备份时效、磁盘使用率,记录日志,发送告警,输出 Prometheus 指标 \# 作者:BridgeLi \# 版本:1.0 \# =================================================================== \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 配置参数 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- BACKUP_DIR="/project/backup/mysql/dbs" LOG_DIR="/var/log/mysql" LOG_FILE="$LOG_DIR/health_check.log" ALERT_EMAIL="admin@example.com" HOSTNAME=$(hostname -s) \# Prometheus 指标输出路径 PROM_FILE="/tmp/backup_health.prom" PROM_TMP_FILE="/tmp/backup_health.prom.tmp" \# 告警阈值(小时) MAX_BACKUP_AGE_HOURS=26 DISK_WARN_THRESHOLD=80 DISK_CRIT_THRESHOLD=90 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 创建日志目录(避免 tee 报错) \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- if [ ! -d "$LOG_DIR" ]; then mkdir -p "$LOG_DIR" && chmod 755 "$LOG_DIR" [ $? -ne 0 ] && echo "ERROR: Cannot create log directory $LOG_DIR" && exit 1 fi \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 重定向输出:同时输出到日志和终端 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- exec > >(tee -a "$LOG_FILE") 2>&1 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 检查最近一次备份是否在合理时间内 \# 输出:状态信息 \# 返回值: \# 0 = OK \# 1 = CRITICAL(超时) \# 2 = ERROR(无备份) \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- check_last_backup() { local latest_entry=$(find "$BACKUP_DIR" -name "*.sql.gz" -type f -printf &#8216;%T@ %p\n&#8217; 2>/dev/null | sort -n | tail -1) if [ -z "$latest_entry" ]; then echo "ERROR no_recent_backup" return 2 fi \# 提取时间戳(取整数部分) local mtime_epoch=$(echo "$latest_entry" | awk &#8216;{split($1,a,"."); print a[1]}&#8217;) local now_epoch=$(date +%s) local age_seconds=$((now_epoch &#8211; mtime_epoch)) local age_hours=$((age_seconds / 3600)) if [ $age_hours -gt $MAX_BACKUP_AGE_HOURS ]; then echo "CRITICAL backup_too_old $age_hours hours" return 1 else echo "OK last_backup $age_hours hours ago" return 0 fi } \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 检查备份目录所在磁盘使用率 \# 返回值: \# 0 = OK (<80%) \# 1 = CRITICAL (>90%) \# 2 = WARN (80%~90%) \# 输出:状态信息 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- check_disk_usage() { if [ ! -d "$BACKUP_DIR" ]; then echo "ERROR backup_dir_not_found: $BACKUP_DIR" return 1 fi local df_out=$(df -P "$BACKUP_DIR" 2>/dev/null) if [ -z "$df_out" ]; then echo "ERROR disk_check_failed" return 1 fi local used_percent=$(echo "$df_out" | tail -1 | awk &#8216;{print $5}&#8217; | tr -d &#8216;%&#8217;) if ! [[ "$used_percent" =~ ^[0-9]+$ ]]; then echo "ERROR disk_usage_invalid: $used_percent" return 1 fi if [ $used_percent -gt $DISK_CRIT_THRESHOLD ]; then echo "CRITICAL disk_usage ${used_percent}%" return 1 elif [ $used_percent -gt $DISK_WARN_THRESHOLD ]; then echo "WARN disk_usage ${used_percent}%" return 2 else echo "OK disk_usage ${used_percent}%" return 0 fi } \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 发送告警邮件 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- send_alert() { local subject="$1" local body="$2" if command -v mail >/dev/null 2>&1; then echo -e "$body" | mail -s "$subject" "$ALERT_EMAIL" echo "Alert sent to $ALERT_EMAIL" else echo "WARNING: &#8216;mail&#8217; command not available. Skipping alert." logger "MySQL Backup Alert: $subject | $body" fi } \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 主逻辑开始 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- echo "=== Backup Health Check ($(date)) on $HOSTNAME ===" \# 执行检查,捕获输出和返回值(只执行一次!) output1=$(check_last_backup) res1=$? echo "$output1" output2=$(check_disk_usage) res2=$? echo "$output2" \# 判断是否需要告警 alert_needed=false if [ $res1 -eq 1 ] || [ $res1 -eq 2 ] || [ $res2 -eq 1 ]; then alert_needed=true fi \# 发送告警 if [ "$alert_needed" = true ]; then subject="⚠️ MySQL 备份异常 &#8211; $HOSTNAME" body="【备份状态】$output1\n【磁盘状态】$output2\n\n请立即检查备份目录:$BACKUP_DIR" send_alert "$subject" "$body" else echo "All checks OK." fi \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 输出 Prometheus 指标(原子写入) \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- ( echo "# HELP mysql_backup_last_success_age_hours Age of last successful backup in hours, -1 if none" echo "# TYPE mysql_backup_last_success_age_hours gauge" local latest_entry=$(find "$BACKUP_DIR" -name "*.sql.gz" -type f -printf &#8216;%T@ %p\n&#8217; 2>/dev/null | sort -n | tail -1) if [ -z "$latest_entry" ]; then echo "mysql_backup_last_success_age_hours -1" else local mtime_epoch=$(echo "$latest_entry" | awk &#8216;{split($1,a,"."); print a[1]}&#8217;) local now=$(date +%s) local age_hours=$(( (now &#8211; mtime_epoch) / 3600 )) echo "mysql_backup_last_success_age_hours $age_hours" fi echo "" echo "# HELP mysql_backup_disk_usage_percent Disk usage of the backup partition (%)" echo "# TYPE mysql_backup_disk_usage_percent gauge" df -P "$BACKUP_DIR" 2>/dev/null | tail -1 | awk &#8216;{gsub(/%/,"",$5); print "mysql_backup_disk_usage_percent", $5}&#8217; ) > "$PROM_TMP_FILE" && mv "$PROM_TMP_FILE" "$PROM_FILE" if [ $? -eq 0 ]; then echo "Prometheus metrics written to $PROM_FILE" else echo "ERROR: Failed to write Prometheus metrics" fi echo "=== Check completed ===" MySQL 恢复脚本 #!/bin/bash \# =================================================================== \# MySQL Point-in-Time Recovery (PITR) 脚本 \# 功能:基于全量备份 + binlog 恢复到指定时间点 \# 作者:BridgeLi \# 版本:1.0 # \# 用法: \# ./mysql-pitr-restore.sh "2025-09-24 10:00:00" [数据库名] \# ./mysql-pitr-restore.sh &#8211;dry-run "2025-09-24 10:00:00" [db_name] \# ./mysql-pitr-restore.sh &#8211;help # \# 依赖: \# mysql, mysqlbinlog, gzip, find, sort \# =================================================================== set -euo pipefail # 严格模式:出错即退出 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 默认配置 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- BACKUP_DIR="/project/backup/mysql/dbs" BINLOG_DIR="/project/backup/mysql/binlogs" RESTORE_DIR="/tmp/mysql_restore_$$" # 使用 PID 避免冲突 CNF_FILE="/project/backup/mysql/my.cnf" LOG_FILE="$BACKUP_DIR/restore.log" DRY_RUN=0 DEBUG=0 TARGET_TIME="" TARGET_DB="" \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 函数定义 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- usage() { cat << &#8216;EOF&#8217; 用法: ./mysql-pitr-restore.sh [选项] <目标时间 &#8216;YYYY-MM-DD HH:MM:SS&#8217;> [数据库名] 选项: &#8211;dry-run 模拟执行,不真正恢复数据 &#8211;debug 启用调试输出 &#8211;help 显示此帮助信息 示例: ./mysql-pitr-restore.sh "2025-09-24 10:00:00" ./mysql-pitr-restore.sh &#8211;dry-run "2025-09-24 10:00:00" mydb ./mysql-pitr-restore.sh &#8211;debug "2025-09-24 12:30:00" 注意: &#8211; 全量备份文件需包含 CHANGE MASTER TO 语句以提取 binlog 位置 &#8211; binlog 文件需为 .gz 压缩格式,命名如 mysql-bin.000001.gz &#8211; 恢复前请确保数据库无写入操作! EOF } log() { local level="${1}" shift echo "\[$(date &#8216;+%F %T&#8217;)\] \[$level\] $*" | tee -a "$LOG_FILE" } debug() { [[ $DEBUG -eq 1 ]] && log "DEBUG" "$@" } cleanup() { if [[ -d "$RESTORE_DIR" ]]; then debug "正在清理临时目录: $RESTORE_DIR" rm -rf "$RESTORE_DIR" fi } trap cleanup EXIT confirm_proceed() { log "WARN" "即将开始恢复至时间点: $TARGET_TIME" if [[ -n "$TARGET_DB" ]]; then log "INFO" "仅恢复数据库: $TARGET_DB" fi log "WARN" "请确保 MySQL 当前无写入操作,否则可能导致数据不一致!" read -p "确定继续?[y/N]: " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log "INFO" "用户取消操作" exit 1 fi } find_latest_full_backup() { local latest_file="" local latest_time="" local file time_str backup_time for file in "$BACKUP_DIR"/*.sql{,.gz}; do [[ -f "$file" ]] || continue \# 提取时间戳:匹配 -YYYY-MM-DD_HHMMSS.sql 或 .sql.gz if [[ "$file" =~ -([0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6})\.sql(\.gz)?$ ]]; then time_str="${BASH_REMATCH[1]}" backup_time="${time_str:0:10} ${time_str:11:2}:${time_str:13:2}:${time_str:15:2}" debug "发现备份: $file -> 时间: $backup_time" if [[ "$backup_time" < "$TARGET_TIME" ]]; then if [[ -z "$latest_time" || "$backup_time" > "$latest_time" ]]; then latest_file="$file" latest_time="$backup_time" fi fi else debug "跳过不匹配的文件: $file" fi done if [[ -z "$latest_file" ]]; then log "ERROR" "未找到早于 $TARGET_TIME 的全量备份" return 1 fi echo "$latest_file|$latest_time" } extract_binlog_position() { local backup_file="$1" local content_cmd="gzip -dc" # 默认是 .gz [[ "$backup_file" == *.sql ]] && content_cmd="cat" local line line=$(eval "$content_cmd" "$backup_file" | sed -n "s/.\*CHANGE MASTER TO MASTER_LOG_FILE=&#8217;\([^&#8217;]\*\)&#8217;,.\*, MASTER_LOG_POS=\([0-9]\*\).*/\1 \2/p" | head -1) if [[ -z "$line" ]]; then log "ERROR" "无法从备份中提取 binlog 位置信息,请检查是否启用 &#8211;master-data=2" return 1 fi echo "$line" } \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 参数解析 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- while [[ $# -gt 0 ]]; do case $1 in &#8211;dry-run) DRY_RUN=1 shift ;; &#8211;debug) DEBUG=1 shift ;; &#8211;help) usage exit 0 ;; -*) log "ERROR" "未知选项: $1" usage exit 1 ;; *) break ;; esac done if [[ $# -lt 1 ]]; then log "ERROR" "缺少目标时间参数" usage exit 1 fi TARGET_TIME="$1" if [[ ! "$TARGET_TIME" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}$ ]]; then log "ERROR" "时间格式无效,应为 &#8216;YYYY-MM-DD HH:MM:SS&#8217;" exit 1 fi TARGET_DB="${2:-}" \# 设置日志输出 exec > >(tee -a "$LOG_FILE") 2>&1 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- \# 主流程 \# &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;- log "INFO" "开始 PITR 恢复流程,目标时间: $TARGET_TIME" if (( DRY_RUN )); then log "DRYRUN" "运行在模拟模式 (&#8211;dry-run),不会执行实际恢复" fi \# 1. 查找最接近的全量备份 log "INFO" "正在查找最接近且早于 $TARGET_TIME 的全量备份&#8230;" result=$(find_latest_full_backup) if [[ $? -ne 0 ]]; then exit 1 fi FULL_BACKUP=$(echo "$result" | cut -d&#8217;|&#8217; -f1) FULL_TIME=$(echo "$result" | cut -d&#8217;|&#8217; -f2) log "INFO" "使用全量备份: $FULL_BACKUP (时间: $FULL_TIME)" \# 2. 用户确认 if (( !DRY_RUN )); then confirm_proceed fi \# 3. 恢复全量数据 log "INFO" "开始导入全量数据&#8230;" if (( DRY_RUN )); then log "DRYRUN" "将导入: $FULL_BACKUP" else mkdir -p "$RESTORE_DIR" if [[ "$FULL_BACKUP" == *.gz ]]; then if gzip -dc "$FULL_BACKUP" | mysql &#8211;defaults-extra-file="$CNF_FILE"; then log "INFO" "全量恢复成功" else log "ERROR" "全量导入失败" exit 1 fi else if mysql &#8211;defaults-extra-file="$CNF_FILE" < "$FULL_BACKUP"; then log "INFO" "全量恢复成功" else log "ERROR" "全量导入失败" exit 1 fi fi fi \# 4. 提取 binlog 起始位置 log "INFO" "提取 binlog 起始位置&#8230;" position_line=$(extract_binlog_position "$FULL_BACKUP") if [[ $? -ne 0 ]]; then exit 1 fi read -r START_FILE START_POS <<< "$position_line" log "INFO" "从 binlog 开始应用: $START_FILE, 位置: $START_POS" \# 5. 应用 binlog 到目标时间 log "INFO" "开始应用 binlog 增量日志&#8230;" applied=0 for binlog_gz in $(find "$BINLOG_DIR" -name "*.gz" | sort); do local binlog_base binlog_base=$(basename "$binlog_gz" .gz) \# 跳过早于起始文件的日志 if [[ "$binlog_base" < "$START_FILE" ]]; then continue fi log "INFO" "处理 binlog: $binlog_base" \# 解压到临时目录 mkdir -p "$RESTORE_DIR/binlogs" local tmp_binlog="$RESTORE_DIR/binlogs/$binlog_base" if (( DRY_RUN )); then log "DRYRUN" "将解压并应用: $binlog_gz -> $tmp_binlog" applied=1 continue fi gzip -dc "$binlog_gz" > "$tmp_binlog" \# 构建 mysqlbinlog 命令 local mysqlbinlog_cmd=( mysqlbinlog &#8211;start-position="$START_POS" &#8211;stop-datetime="$TARGET_TIME" "${TARGET_DB:+&#8211;database=$TARGET_DB}" "$tmp_binlog" ) local mysql_cmd=(mysql &#8211;defaults-extra-file="$CNF_FILE") debug "执行命令: ${mysqlbinlog_cmd[\*]} | ${mysql_cmd[\*]}" if "${mysqlbinlog_cmd[@]}" | "${mysql_cmd[@]}"; then log "INFO" "成功应用 binlog: $binlog_base" applied=1 else local ret=$? log "INFO" "完成 binlog 应用(可能已到达目标时间或中断),返回码: $ret" applied=1 break # 关键:不再处理后续 binlog fi rm -f "$tmp_binlog" START_POS=4 # 下一个文件从事件头后开始 done \# 6. 结果汇报 if (( applied == 0 )); then log "WARN" "未应用任何 binlog,请检查 binlog 是否存在、时间范围是否合理" fi if (( DRY_RUN )); then log "DRYRUN" "模拟执行结束。真实恢复请移除 &#8211;dry-run 参数。" else log "INFO" "恢复完成:已恢复至 $TARGET_TIME" log "INFO" "请立即验证数据一致性,并检查关键业务逻辑。" fi exit 0 my.cnf 文件(600 权限) [client] user=用户名 password=密码 host=localhost port=3306 crontab 表达式 \# 每天 2:00 全量备份 0 2 \* \* * /project/backup/mysql/backup_per_db.sh \# 每小时 0 分 增量 binlog 0 \* \* \* \* /project/backup/mysql/backup_binlog.sh \# 每天 3:10 健康检查 10 3 \* \* * /project/backup/mysql/check_backup_health.sh 如果报:bash: ./backup_per_db.sh: /bin/bash^M: 坏的解释器: 没有那个文件或目录,解决方案: 使用 vim 编辑器 vim backup_per_db.sh 在命令模式下执行: :set fileformat=unix :wq

October 23, 2025 · 11 min · 2332 words · Bridge Li

关于 MySQL 的三个小问题

之前曾经写过MySQL的一个小问题,就是 MySQL 如果采用的字符集是 utf8 的话,emoji 的话,是存不进去的,因为历史原因 MySQL 的 utf8 不是真正的 utf8,utf8mb4 才是真正的 utf8,所以在创建数据库的时候编码集推荐选择 真正的 utf8mb4。今天就再写两个小问题: allowMultiQueries=true 前一段时间同事有个需求,使用 mybatis 的时候,在同一个 mapper 方法中需要同时执行两个 MySQL 语句,当时同事说他把 SQL 拷贝出来执行都没有问题,但是在代码中执行就是报错,让我帮忙看一下原因,当时听他描述,我大概猜到了,然后一看果然就是在一个 mapper 方法中同时执行两个 SQL 语句,在 MySQL 的连接中加上 allowMultiQueries=true 搞定。 MySQL8.0 中 group by 报错的问题 可能因为我一直在互联网公司的原因,所以在我的职业生涯中,一直都是 MySQL,前几年在 MySQL8.0 还没成为主流,主流还是使用 5.6 的时候,有个刚入职不久的同事问如下的语句: select a, b from t_test group by a 说不报错吗?当时我很懵,不报错啊,代码中很多地方我也一直这么写啊,然后同事说他之前用 oracle 是不行的,然后到 MySQL5.7 之后,发现 MySQL 默认也不能这么写了,其实解决起来也很简单。 第一种方案: 修改 MySQL 的语句: select a from t_test group by a 或者: ...

October 19, 2024 · 1 min · 125 words · Bridge Li

MySQL 中 NOT IN 的坑 — 列为 null 的问题

前一段时间在公司做一个小功能的时候,统计一下某种情况下有多少条数据,然后修改的问题,当时感觉很简单,写了一个如下的 SQL: SELECT COUNT(*) FROM t1 where tl.c1 not IN (SELECT t2.c1 FROM t2); 预期的结果是:有多少条数据在 t1 中,同时不在 t2 中,结果为:0,也就是 t1 中数据都在 t2 中,但是很容易就发现某些数据在 t1 中不在 t2 中,所以就感觉很奇怪,这个 SQL 看着也没问题啊。经过一番查询原来是因为 t2 的 c1 字段包含了 null 值,修改如下两种形式都可以得到预期的结果: SELECT COUNT(*) FROM t1 LEFT JOIN t2 ON t1.c1 = t2.c1 WHERE t2.c1 IS NULL OR t2.c1 = &#8221;; 或者 select COUNT(*) from t1 where t1.c1 not in ( select t2.c1 from t2 where t2.c1 is not null AND t2.c1 != &#8221; ); 所以都是 null 引起的(为了避免错误我把空串也加上了),原因是 not in 的实现原理是,对每一个 t1.c1 和每一个 t2.c1 (括号内的查询结果)进行不相等比较(!=)。 ...

November 24, 2019 · 1 min · 114 words · Bridge Li

MySQL 系统参数 sql_safe_updates 小结

前一段时间,公司某个项目组某个项目因为失误,出现了一个严重 bug,动态 SQL 导致没有 where 条件,就把数据库某张表里面的数据全部更新了,虽然事后 DBA 同学很给力的恢复了,但是运维的同学讨论,让所有的项目都不许写动态 SQL,必须根据 ID 更新,并写了一个 sonar 插件,扫描代码,发现有动态 SQL 就报 bug,不过还好暂时没有强制要求改,个人认为如果强制要求,这不就是典型的因噎废食吗?没有动态 SQL,这代码量得增加多少?我们研发要不要按照代码量算钱?前一段时间,看《即刻时间》丁奇(原名林晓斌)的 《MySQL 实战 45 讲》里面有一句话关于 sql_safe_updates 的知识点一笔带过,小小研究一下,刚好可以解决这个问题,这是一个小结。 作用:防止忘记添加 WHERE 条件,导致数据被误更新或误删的情况和另外为了提高 SQL 性能,避免更新或删除的时候 WHERE 条件不走索引的情况;不过默认值是:关闭(值为0),所以可能更多的防止忘记添加 WHERE 条件吧,另外这个参数分为会话级别和全局级别。 查看 sql_safe_updates 的值和修改 show variables like &#8216;sql_safe_updates&#8217;; &#8212; 会话 show global variables like &#8216;sql_safe_updates&#8217;; &#8212; 全局 set sql_safe_updates=1; &#8212; 会话 set global sql_safe_updates=1; &#8212; 全局 具体的测试比较简单,就不一张张的贴图了,直接写结论了,大家可以自己测试一下就好了 操作 Delete Update NO WHERE No No NO WHERE + LIMIT No Yes WHERE KEY Yes Yes WHERE KEY + LIMIT Yes Yes WHERE NOKEY No No WHERE NOKEY+ LIMIT Yes Yes WHERE CONSTANT No No WHERE CONSTANT + LIMIT No Yes 其中 KEY 不仅是主键,同样也包括索引字段,CONSTANT 表示 where 1= 1(有些程序员的知识没有更新喜欢这么写,其实可以不用的),Yes 是可以,No 是不可以。 ...

October 27, 2019 · 1 min · 110 words · Bridge Li

MySQL sort 分页重复数据(转载)

前两天在写一个东西的时候,测试的同学说发现一个问题,排序分页,第二页和第一页有重复数据,当时我看了一下,确实有这个问题,然后就想到几年前我就曾经遇到过这个问题,淘宝数据库内核月报上也做了说明,所以这个时候就体现出了老程序员的价值:踩过的坑多,坑坑相连也就都成了平地,考虑到很多人不知道这个问题,所以把原文转载过来,以期能够让更多的人看到,原文如下: 背景 6.5 号,小编在 Aliyun 的论坛中发现一位开发者提的一个问题,说 RDS 发现了一个超级大 BUG,吓的小编一身冷汗 = =!! 赶紧来看看,背景是一个 RDS 用户创建了一张表,在一个都是 NULL 值的非索引字段上进行了排序并分页,用户发现第二页和第一页的数据有重复,然后以为是 NULL 值的问题,把这个字段都更新成相同的值,发现问题照旧。详细的信息可以登录阿里云的官方论坛查看。 小编进行了尝试,确实如此,并且 5.5 的版本和 5.6 的版本行为不一致,所以,必须要查明原因。 原因调查 在 MySQL 5.6 的版本上,优化器在遇到 order by limit 语句的时候,做了一个优化,即使用了 priority queue。参考伪代码: while (get_next_sortkey()) { if (using priority queue) push sort key into queue else { if (no free space in sort_keys buffers) { sort sort_keys buffer; dump sorted sequence to &#8216;tempfile&#8217;; dump BUFFPEK describing sequence location into &#8216;buffpek_pointers&#8217;; } put sort key into &#8216;sort_keys&#8217;; } } if (sort_keys has some elements && dumped at least once) sort-dump-dump as above; else don&#8217;t sort, leave sort_keys array to be sorted by caller 使用 priority queue 的目的,就是在不能使用索引有序性的时候,如果要排序,并且使用了limit n,那么只需要在排序的过程中,保留 n 条记录即可,这样虽然不能解决所有记录都需要排序的开销,但是只需要 sort buffer 少量的内存就可以完成排序。 之所以 5.6 出现了第二页数据重复的问题,是因为 priority queue 使用了堆排序的排序方法,而堆排序是一个不稳定的排序方法,也就是相同的值可能排序出来的结果和读出来的数据顺序不一致。 5.5 没有这个优化,所以也就不会出现这个问题。 ...

January 26, 2019 · 1 min · 197 words · Bridge Li

关于 MySQL 中 utf8 的问题

有一个段子,我每次看了都头皮发麻,这个段子是: 手持两把锟斤拷,口中疾呼烫烫烫,脚踏千朵屯屯屯,笑看万物锘锘锘 我相信凡是和我有一样感觉的应该都知道这是啥,所以为了避免这个问题,我们工作的建议就是,所有的地方都设置成:UTF-8,确实我们这样工作了很多年,不知道哪一年 emoji 突然火了,于是在上家公司做论坛的时候,有一个小伙伴遇到了类似于下面的一个小问题(我找不到原报错日志了): Incorrect string value: ‘\xF0\x9F\x98\x83 <…’ for column ‘summary’ at row 1 当时问我,我也不知道原因,然后经过一番搜索之后,发现原来是 MySQL 存储 emoji 时出错了,存储 emoji 的时候,不应该用 utf8,而是用 utf8mb4。 随着移动互联网的到来,emoji 的应用越来越广泛,本以为这个问题大家都知道了,所以就没有深究过,想着自己记着就行了,但是不知道为什么最近这个问题突然火了,原来不是所有人都知道这个问题,甚至有人说,在做微信开发的时候,天知道微信用户会取什么昵称,所以不得已把用户的昵称都 base64 编码一下,展示的时候再解码,我只能表示:厉害,人民群众的智慧是无穷的。 其实产生这个问题的原因也很简单,据说是 MySQL 的一个 bug: MySQL 的 utf8mb4 是真正的utf8。 MySQL 的 utf8 是一种专属的编码,它能够编码的 Unicode 字符并不多。 关于什么是编码,我相信大家应该都知道了,如果不知道请自行搜索(当年曾经给一个计算机在读博的学姐科普什么是 GBK、GB2312、GB18030、UTF-8、BIG5、ISO-8859-1 等等,也是心累),至于为什么 MySQL 的 utf8 不是真正的 utf8,这是一个历史原因,我搜索的资料是:MySQL 从 4.1 版本开始支持 UTF-8,也就是 2003 年,而今天使用的 UTF-8 标准(RFC 3629)是随后才出现的。 所以现在我们看到的很多 MySQL(或者 MySQL 的分支:MariaDB)资料依然在建议,MySQL 的编码使用 utf8,这在绝大多数的情况下,确实也没有问题,但确实是错误的,最好的选择是,选用真正的 utf8,也就是:utf8mb4。对于已经选用 utf8 需要改用 utf8mb4 的项目,这有一个指南: ...

August 4, 2018 · 1 min · 87 words · Bridge Li

MySQL : The last packet successfully received from the server was XXX milliseconds ago

14年毕业写完论文没事干的时候,自己玩微信公众号开发,当时想做一个自然语言交互,其实就是想试一下lucene,但是当时建索引的时候偶尔会报这个错,一致不知道具体原因,去网上搜索但是天下文章一大抄,你抄我来我抄他,也没找到原因,后来因为工作中也没遇到过,感觉应该是自己当时水平不行就忘了这件事,前几天 fatsjson 和 druid 的作者温少突然在一个群里面说有人通过阿里工单反馈这个问题,他给追踪了一下,找到了原因,原来还是还是有人遇到这个问题,今天记录一下,希望对遇到这个问题的小伙伴有帮助,报错的信息大概就是: Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: The last packet successfully received from the server was 20,820,001 milliseconds ago. The last packet sent successfully to the server was 20,820,002 milliseconds ago. is longer than the server configured value of &#8216;wait_timeout&#8217;. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection property &#8216;autoReconnect=true&#8217; to avoid this problem. at sun.reflect.GeneratedConstructorAccessor29.newInstance(Unknown Source) ………… 下面是温少分享的截图 ...

November 11, 2017 · 1 min · 88 words · Bridge Li

事务并发处理

前几天和同事讨论,老夫自以为对事务有了一定的了解,但当讨论的时候发现还是有些说不明白,所以周末的时间,又看了一遍带我入门北京尚学堂马士兵老师关于事务的讲解,这次做一下笔记,以供以后忘了的时候查询方便。这里默认读者对事务的ACID都有了了解,直接说事务并发时可能出现的问题和数据库的事务隔离级别 事务并发时可能出现的问题 说这个问题记得大学课堂上有一个很经典的例子就是:银行的存取款,这里也用这个例子说明(因为不知道wp博客怎么搞表格和怎么支持MD,所以就搞几张图片吧) ①. 第一类丢失更新(Lost Update) ②. dirty read脏读(读到了另一个事务在处理中还未提交的数据) ③. non-repeatable read 不可重复读 ④. second lost update problem 第二类丢失更新(不可重复读的特殊情况) ⑤. phantom read 幻读 看到这里可能会有读者对不可重复读和幻读有所迷惑,这两者有什么区别吗?不都是受另一个事务的影响,导致前后结果不一致吗?其实仔细看区别还是很明显的:幻读是关于数据库的delete和insert导致前后的数据不一致,而其他的情况都是数据的更新导致前后的数据不一致 数据库的事务隔离机制 其中在文档java.sql.Connection中有详细的说明,除了none(没有事务)之外,还有:1:read-uncommitted 2:read-committed 4:repeatable read 8:serializable(数字代表对应值)四种。 为什么取值要使用 1 2 4 8 而不是 1 2 3 4 1=0000 2=0010 4=0100 8=1000(位移计算效率高) 需要说明的是: 只要数据库支持事务,就不可能出现第一类丢失更新 read-uncommitted(允许读取未提交的数据) 会出现dirty read, phantom-read, non-repeatable read 问题 read-commited(读取已提交的数据 项目中一般都使用这个)不会出现dirty read,因为只有另一个事务提交才会读出来结果,但仍然会出现 non-repeatable read 和 phantom-read;使用read-commited机制可用悲观锁 乐观锁来解决non-repeatable read 和 phantom-read问题 repeatable read(事务执行中其他事务无法执行修改或插入操作 较安全)但仍然会出现phantom-read serializable解决一切问题(顺序执行事务 不并发,实际中很少用) ...

April 9, 2017 · 1 min · 74 words · Bridge Li

MySQL优化初步

这一段时间看了学长推荐的一本书:《高性能MySQL》,感觉还不错,今天写一篇读书笔记,供自己和需要的人参考。 一、数据库优化简介 数据库优化的目的 ①. 避免出现页面访问错误,主要是数据库连接timeout产生页面5XX错误、由于慢查询造成页面无法加载和由于阻塞造成数据无法提交; ②. 增加数据库的稳定性,很多数据库问题都是由于低效的查询引起的; ③. 优化用户体验,流畅页面的访问速度、良好的网站功能体验 从哪几个方面对数据库优化 所谓一图胜千言,所以我们先看一下,下面这张图: 从这张图上,我们可以清晰的看到,我们优化的重点和代价,另外这几个方面我们分别可以做些什么呢? ①.在SQL及索引层面,首先是我们要写出一些结构良好的SQL和建立有效的索引 ②.数据库表结构,我们应该根据数据库的一些范式,减少冗余,建立一个利于查询的库表结构 ③.系统配置,例如TCP/IP的连接数、文件的打开数等等 ④. 硬件,包括内存、CPU等,IO设备等等 二、SQL语句优化 在SQL优化之前,我们首先肯定是要查询到哪些SQL需要优化,所以下面让我们先打开MySQL的慢查询日志 MySQL慢查日志的开启方式和存储格式 我们先看几个命令: show variables like &#8216;slow_query_log&#8217;; set global slow_query_log=on; set global slow_query_log_file=&#8217;/home/mysql/sql_log/mysql-slow.log&#8217;; set global log_queries_not_using_indexes=on; set global long_query_time=1; 这几条命令相信不用说,大家都能看得懂,需要说明的是最后一条的时间单位是“秒”,本例中意思是说大于“1秒”的都认为是慢查询,当然实际情况下“1秒”很多时候也是太长了,记录下来这些信息之后我们就可以分析和查看这些日志了,分析和查看MySQL的慢查询日志有两个常见的工具,分别是mysqldumpslow和pt-query-digest,第一个是MySQL官方提供的,第二个是一个商业软件,至于他们怎么用,这里就不多说了,找到这些用问题的SQL之后,我们就可以对这些SQL进行优化了,一般有问题的SQL主要是一下这些: ①. 查询次数多且每次查询占用时间长的SQL,通常是pt-query-digest分析的前几个查询; ②. IO大的SQL,注意pt-query-digest分析中的Rows examine项 ③. 未命中索引的SQL,注意pt-query-digest分析中Rows examine和Rows Send的对比 通过explain查询和分析SQL的执行计划 explain返回各列的含义 table: 显示这一行数据是关于那张表的 type: 这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为const、eq_reg、ref、range、index和All possible_keys 显示可能应用在这张表中的索引。如果为空,没有可能的索引。 key: 实际使用的索引 key_len: 使用索引的长度,在不损失精确性的情况下,长度越短越好 ref: 显示索引的哪一列被使用了,如果可能的话,是一个常数 rows: MySQL认为必须检查的用来返回请求数据的行数 extra: 看到Using filesort和Using temporary的时候,查询就需要优化了,Using temporary需要MySQL常见一个临时表来存储结果,这通常发生在不同的列集进行ORDER BY上,而不是GROUP BY上 count()和max()的优化 max函数通常是查询最后一个数据,我们可以对max函数包含的列添加一个索引; count函数是用来计数的,我们只需要了解count(*)和count(id)的差别就好了,count(*)返回的是有多少行,count(id)返回的是id不为null的有多少行,所以这是有差别的 ...

June 14, 2015 · 1 min · 206 words · Bridge Li