MySQL
MySQL 是面向在线事务处理的关系型数据库。它把结构化数据、约束、索引、事务、并发控制、日志、复制和恢复组合成一个客户端/服务端系统,适合订单、账户、商品、库存、权限等需要明确数据关系和一致性边界的核心业务。
一、是什么
1. MySQL 的准确定位
MySQL 首先是关系型数据库管理系统,不是缓存、搜索引擎、消息队列或面向分析的列式数仓。数据通过表、列、主键、外键和约束描述,使用 SQL 查询和修改,由事务保证一组操作共同成功或共同失败。
MySQL Server 以独立进程运行。应用通过 TCP 或 Unix Socket 连接服务端,完成认证后提交 SQL。服务端负责解析、权限判断、查询优化和执行,存储引擎负责记录、索引、锁、事务和持久化。生产业务通常使用默认的 InnoDB。
同一实例内可以包含多个数据库,但数据库不是资源隔离单元。它们共享进程、CPU、内存、磁盘、日志和故障域。需要独立扩缩容、独立恢复目标或严格租户隔离时,应拆分实例,而不是只执行 CREATE DATABASE。
2. 官方资源、版本和许可
| 项目 | 地址 | 用途 |
|---|---|---|
| 官网 | mysql.com | 产品、支持和生态入口 |
| 8.4 手册 | MySQL 8.4 Reference Manual | LTS 基线的权威语义 |
| 发布说明 | MySQL Release Notes | 补丁版本、修复和行为变化 |
| 下载 | MySQL Community Downloads | Server、Shell、Router 和连接器 |
| 源码 | mysql/mysql-server | Community Server 源码 |
| 容器镜像 | Docker Hub mysql | 官方镜像与标签说明 |
| 许可 | MySQL Legal | GPL 与商业许可边界 |
MySQL 采用 LTS 与 Innovation 两条发布线。LTS 强调稳定功能集合和较长维护周期,适合生产;Innovation 更快引入能力,适合提前验证。生产不能只写 mysql:latest,应固定大版本和补丁版本,并在升级前阅读跨版本发布说明。
示例以 MySQL 8.4 LTS 为运行基线。官方发布页同时提供后续 LTS 和 Innovation 版本时,选型依据不是数字更大,而是驱动、备份工具、复制拓扑、SQL 行为和维护周期是否已验证。受支持的跨大版本升级需要沿官方路径逐级进行,不能跳过中间 LTS。
MySQL Community Edition 使用 GPL;Oracle 也提供商业版本和支持。连接器、备份工具、监控组件及云厂商托管版可能使用不同许可,交付前需要分别确认。
查看正在运行的精确版本:
SELECT VERSION() AS version,
@@version_comment AS edition,
@@version_compile_machine AS architecture;预期返回实际补丁版本和 MySQL Community Server - GPL 等说明。升级判断应使用该结果,而不是仅凭安装包文件名。
3. 一次请求如何执行
一次查询依次经过连接、认证、SQL 解析、语义检查、查询优化、执行器和存储引擎。优化器根据索引、统计信息、连接顺序和成本选择计划;执行器通过存储引擎接口读写;InnoDB 再处理页、索引、锁、MVCC 和日志。
应用/驱动
│ TCP、Unix Socket、TLS
▼
连接管理与认证
▼
解析器 ── 权限与语义检查
▼
优化器 ── 统计信息、访问路径、连接顺序
▼
执行器
▼
InnoDB ── Buffer Pool、B+Tree、锁、MVCC
├── Redo Log:崩溃恢复
├── Undo Log:回滚与一致性读
├── Binlog:复制与时间点恢复
└── 数据文件、Doublewrite、临时文件连接建立并不免费。认证、TLS 握手、线程和会话状态都会消耗资源,所以应用应使用有边界的连接池。连接池总量超过数据库可并行处理能力后,线程切换、锁竞争和内存消耗会共同放大延迟。
4. InnoDB、页、索引和缓存
InnoDB 以页为基本读写单位,默认页通常为 16 KiB。表数据和索引组织为 B+Tree。主键索引叶子节点保存整行,因此又称聚簇索引;二级索引叶子节点保存索引列和主键值,查询其他列时通常需要按主键再次访问聚簇索引。
主键应稳定、非空、尽量短并且通常递增。过长的随机主键会放大所有二级索引,随机写还会增加页分裂和缓存离散。业务编号可以建立唯一索引,不必承担物理聚簇键职责。
Buffer Pool 缓存数据页和索引页。读取命中时避免磁盘随机 I/O;修改先作用于内存页并标记为脏页,随后由后台线程刷盘。innodb_buffer_pool_size 是专用主机的重要容量参数,但不能挤占操作系统、连接缓冲、排序、临时表、监控和备份所需内存。
SHOW VARIABLES WHERE Variable_name IN (
'innodb_page_size',
'innodb_buffer_pool_size'
);
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Innodb_buffer_pool_read_requests',
'Innodb_buffer_pool_reads',
'Innodb_buffer_pool_pages_dirty'
);Innodb_buffer_pool_reads 持续快速增长说明需要从磁盘读取页面,但不能仅凭瞬时命中率扩容,还要结合工作集、磁盘时延、查询计划和剩余内存判断。
5. 事务、MVCC、Undo 和锁
事务具有原子性、一致性、隔离性和持久性。默认自动提交意味着每条独立 DML 都是一个事务;业务需要多条语句共同成功时必须显式开启事务,并在异常时回滚。
InnoDB 使用 MVCC 支持一致性读。旧版本信息保存在 Undo 中,读视图决定当前事务可见哪些版本。长事务会长时间保留旧版本,阻止清理,导致 Undo 历史增长、查询变慢和磁盘压力。
普通 SELECT 通常是一致性读,不主动锁住读取行。SELECT ... FOR UPDATE 和 SELECT ... FOR SHARE 是锁定读。更新会对命中的索引记录加锁;范围条件在可重复读下还可能涉及间隙锁或临键锁。没有合适索引时,扫描和加锁范围可能远大于业务预期。
SELECT @@transaction_isolation;
SELECT trx_id, trx_mysql_thread_id, trx_started,
trx_state, trx_rows_locked, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;6. Redo、Binlog 与提交
Redo Log 是 InnoDB 的物理恢复日志,用于崩溃后重做已提交但尚未写回数据文件的修改。Undo 用于事务回滚和一致性读。Binlog 记录服务端层面的逻辑变更,主要用于复制和时间点恢复。
开启 Binlog 后,一次提交需要协调 InnoDB Redo 与 Binlog。MySQL 通过内部两阶段提交保持恢复和复制语义一致。innodb_flush_log_at_trx_commit=1 配合 sync_binlog=1 提供较强的单机提交持久性,但不能代替跨节点确认、备份和恢复演练。
SHOW VARIABLES WHERE Variable_name IN (
'innodb_flush_log_at_trx_commit',
'sync_binlog',
'log_bin',
'binlog_format',
'gtid_mode'
);ROW 格式是复制和恢复的常见选择。Binlog 保留时间必须覆盖故障发现、备份恢复和审计窗口。
7. 复制与高可用
异步复制由 Source 写入 Binlog,Replica 的 I/O 线程接收日志,Applier 线程重放变更。GTID 为每个已提交事务提供全局标识,使拓扑切换和自动定位更可靠。异步复制不等于零丢失:Source 返回成功时,事务可能尚未到达 Replica。
半同步复制要求至少一个副本确认收到事件后再返回,能缩小丢失窗口,但不保证副本已经应用,也不能替代一致性路由和旧主隔离。
Group Replication 使用组成员协议、事务认证和多数派形成复制组。InnoDB Cluster 由 MySQL Shell 管理 Group Replication,MySQL Router 根据集群元数据路由读写。高可用完整链路还必须包括故障检测、唯一主节点、旧主隔离、客户端重连、复制延迟策略和恢复演练。
8. MySQL 的边界
MySQL 擅长具有明确关系、索引访问模式和事务边界的在线业务。它不适合把海量日志当搜索引擎使用,不适合以全表扫描和大聚合作为主要负载,也不能通过增加逻辑库获得资源隔离。
缓存热点读取应由 Redis 等缓存承担;全文检索更适合 Elasticsearch/OpenSearch;大规模聚合更适合 ClickHouse 等分析系统;嵌入式场景可考虑 SQLite;需要更丰富 SQL、类型或扩展能力时可评估 PostgreSQL。组合系统中 MySQL 通常承担事实数据源,缓存、索引和数仓必须设计可重建链路。
二、为什么
1. 什么时候使用 MySQL
| 场景 | 适合原因 | 仍需补充的设计 |
|---|---|---|
| 订单、支付、库存 | 事务、约束、行级并发控制成熟 | 幂等键、状态机、对账和补偿 |
| 用户、权限、组织 | 关系明确,唯一约束可防重复 | 最小权限、审计和敏感字段保护 |
| 商品和配置后台 | CRUD、索引和分页成熟 | 搜索量大时建立专用搜索索引 |
| SaaS 业务 | 生态成熟,便于分库分实例 | 租户隔离、容量和迁移策略 |
| Java 微服务 | Connector/J、连接池、迁移工具成熟 | 超时、事务边界和连接预算 |
是否使用 MySQL 应从一致性、关系复杂度、读写模式、容量、恢复目标和团队能力判断。如果主要任务是跨年数据扫描聚合,OLAP 系统通常更合适;如果只是进程内单文件数据,独立服务反而增加成本。
2. 什么时候不能只靠 MySQL
单表数据量、总容量和 QPS 没有适用于所有业务的固定阈值。相同的一亿行,窄表主键点查和宽表无索引聚合完全不同,必须用真实数据分布、SQL、并发和硬件压测。
MySQL 不能自动解决跨服务事务。订单库提交成功、消息发送失败时,需要本地消息表、事务消息、Outbox 或可重试补偿。不能用一个跨越 HTTP 调用的超长数据库事务假装获得分布式原子性。
复制也不是备份。错误删除、错误更新和恶意操作会被复制到所有副本;备份提供独立时间点,Binlog 提供增量恢复链路,二者缺一不可。
3. 与常见产品如何选择
| 产品 | 优势 | 相对 MySQL 的主要区别 |
|---|---|---|
| PostgreSQL | 标准 SQL、复杂类型、扩展和高级查询丰富 | 生态和运维习惯不同,需按业务验证 |
| MariaDB | MySQL 衍生生态,部分语法和工具相近 | 已独立演进,版本号相近不代表可直接替换 |
| SQLite | 零服务、单文件、嵌入式 | 不提供独立服务和同等级服务端并发模型 |
| Redis | 内存数据结构和低延迟 | 不替代关系约束、复杂查询和主数据持久化 |
| ClickHouse | 列式分析和大规模聚合 | 不以高并发短事务为核心 |
| Elasticsearch | 倒排索引、全文检索 | 不宜作为订单和账户的唯一事实数据源 |
4. 版本和架构决策
生产优先选择支持期内的 LTS 并固定补丁版本。升级前验证驱动、SQL 模式、保留字、认证、字符集、复制、备份恢复和监控。Innovation 适合验证新特性,但维护和升级节奏更紧。
| 问题 | 需要得到的量化答案 |
|---|---|
| 数据规模 | 当前数据、日增量、索引比例、Binlog 增量和三年容量 |
| 访问模式 | 点查、范围查、排序、聚合、写入峰值和热点键 |
| 一致性 | 哪些操作必须同一事务,哪些允许最终一致 |
| 可用性 | RPO、RTO、跨可用区要求和人工介入时间 |
| 连接预算 | 应用实例数、池大小、后台任务和运维连接 |
| 恢复能力 | 全量恢复时长、Binlog 窗口、演练频率 |
| 安全 | 网络边界、TLS、账户、密钥、审计和脱敏 |
总连接预算 =
应用实例数 × 每实例最大连接数
+ 定时任务连接
+ 管理和监控连接
+ 故障切换余量总预算必须低于数据库允许连接数并留出管理入口。把 max_connections 调得很大只会把拒绝连接变成内存耗尽和整体抖动。
5. 可用性不能只看副本数量
RPO 描述可接受的数据丢失量,RTO 描述可接受的恢复时间。异步副本改善读取扩展和恢复速度,但 RPO 取决于日志是否传达和应用;半同步缩小传达窗口;Group Replication 提供成员和多数派;备份与 Binlog 决定灾难恢复的时间点范围。
架构评估需要分别验证主机故障、磁盘损坏、网络分区、误删除、证书过期、容量耗尽和升级失败。只演示一次自动切换,不能证明误操作恢复和数据正确性。
三、怎么做
1. 准备 Linux 环境
实验环境建议至少 2 个 CPU、4 GiB 内存和 20 GiB 可用磁盘。生产容量必须按数据、索引、Redo、Binlog、临时文件、备份工作区和增长余量计算。
uname -a
cat /etc/os-release
free -h
df -h
sudo ss -lntp | grep ':3306'磁盘接近 80% 时不要直接开始导入。端口命令没有输出表示当前没有进程监听 3306;已有输出时确认是否是需要保留的实例。
2. Docker Compose 部署
mkdir -p mysql-lab/{conf.d,data,secrets,tls}
cd mysql-lab
printf '%s' 'Change-This-Root-Password' > secrets/root_password
chmod 600 secrets/root_passwordcompose.yaml 固定 LTS 小版本。上线前在官方镜像页确认标签并记录镜像摘要。
services:
mysql:
image: mysql:8.4.11
container_name: mysql84
restart: unless-stopped
ports:
- "127.0.0.1:3306:3306"
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/root_password
TZ: Asia/Shanghai
secrets:
- root_password
volumes:
- ./data:/var/lib/mysql
- ./conf.d:/etc/mysql/conf.d:ro
- ./tls:/etc/mysql/tls:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
interval: 10s
timeout: 5s
retries: 12
secrets:
root_password:
file: ./secrets/root_passworddocker compose up -d
docker compose ps
docker compose logs --tail=100 mysql健康状态应变为 healthy,日志不应持续出现初始化失败、权限错误或崩溃重启。此处无凭据的 ping 只证明服务端能够响应,不证明业务账户可用,认证要由下一步单独验证。一个容器不等于生产高可用。
docker compose exec mysql \
mysql -uroot --password \
-e "SELECT VERSION(), @@hostname, @@port;"预期返回版本、容器主机名和 3306。后续应使用受限账户,避免密码长期留在命令历史。
3. 使用官方仓库安装 systemd 服务
Ubuntu、Debian、RHEL、Rocky Linux 和 AlmaLinux 的仓库配置、GPG Key 与支持版本会变化,应先按官方安装文档配置 MySQL APT 或 Yum Repository。
Ubuntu/Debian 在官方仓库配置完成后:
apt-cache policy mysql-community-server
sudo apt update
sudo apt install -y mysql-community-server候选来源应指向 MySQL 官方仓库。RHEL 系:
sudo dnf module disable mysql -y
sudo dnf install -y mysql-community-server
sudo systemctl enable --now mysqld
sudo systemctl status mysqld --no-pager状态应为 active (running)。发行版可能使用 mysql 服务名,应以安装包提供的 unit 为准。
4. 配置服务器
容器保存为 conf.d/server.cnf;systemd 通常在 /etc/my.cnf.d/ 或 /etc/mysql/mysql.conf.d/ 创建独立文件。
[mysqld]
bind-address = 0.0.0.0
port = 3306
character-set-server = utf8mb4
collation-server = utf8mb4_0900_ai_ci
default-time-zone = +08:00
ssl_ca = /etc/mysql/tls/ca.pem
ssl_cert = /etc/mysql/tls/server-cert.pem
ssl_key = /etc/mysql/tls/server-key.pem
require_secure_transport = ON
max_connections = 300
table_open_cache = 4000
thread_cache_size = 100
innodb_buffer_pool_size = 2G
innodb_flush_log_at_trx_commit = 1
innodb_flush_method = O_DIRECT
server_id = 101
log_bin = mysql-bin
binlog_format = ROW
gtid_mode = ON
enforce_gtid_consistency = ON
sync_binlog = 1
binlog_expire_logs_seconds = 604800
slow_query_log = ON
long_query_time = 1
performance_schema = ON2G 只是 4 GiB 实验机示例。修改后验证并重启:
TLS 路径是启动前置条件;证书尚未按下一节安装时,不要先重启加载这份配置。systemd 实例在文件齐备后执行:
sudo mysqld --validate-config
sudo systemctl restart mysqld
sudo journalctl -u mysqld -n 100 --no-pagerCompose 实例在 ./tls/ 文件齐备后执行:
docker compose exec mysql mysqld --validate-config
docker compose restart mysql
docker compose logs --tail=100 mysqlSHOW VARIABLES WHERE Variable_name IN (
'bind_address',
'max_connections',
'innodb_buffer_pool_size',
'log_bin',
'gtid_mode',
'binlog_format'
);5. 账户、权限和 TLS
生产证书应由组织 CA 签发,服务端证书的 SAN 必须包含客户端实际使用的 DNS 名称。先检查证书,再以仅 MySQL 用户可读的权限安装:
openssl verify -CAfile ca.pem server-cert.pem
openssl x509 -in server-cert.pem -noout -subject -issuer \
-ext subjectAltName
sudo install -d -m 750 -o mysql -g mysql /etc/mysql/tls
sudo install -m 640 -o mysql -g mysql \
ca.pem server-cert.pem server-key.pem /etc/mysql/tls/这是 systemd 主机的安装路径;Compose 环境把同一组文件放进已挂载的 ./tls/,并确保容器内 mysql 用户可读。输出中的 SAN 应包含 DNS:db.example.internal,openssl verify 应返回 OK。私钥不能进入镜像、代码仓库或普通用户可读目录。安装后执行前一节的配置校验和重启命令,并确认:
SHOW VARIABLES WHERE Variable_name IN (
'have_ssl', 'require_secure_transport',
'ssl_ca', 'ssl_cert', 'ssl_key'
);have_ssl=YES、require_secure_transport=ON 且三个路径与部署值一致,才进入账户验证。
CREATE DATABASE shop
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'shop_migrate'@'10.%'
IDENTIFIED BY 'Replace-Migrate-Password';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER,
INDEX, DROP, REFERENCES
ON shop.* TO 'shop_migrate'@'10.%';
CREATE USER 'shop_app'@'10.%'
IDENTIFIED BY 'Replace-App-Password';
GRANT SELECT, INSERT, UPDATE, DELETE
ON shop.* TO 'shop_app'@'10.%';SHOW GRANTS FOR 'shop_migrate'@'10.%';
SHOW GRANTS FOR 'shop_app'@'10.%';生产密码由密钥系统注入。限制 Host 后仍需防火墙和 TLS。
SHOW VARIABLES LIKE 'have_ssl';
SHOW VARIABLES LIKE 'require_secure_transport';启用 require_secure_transport=ON 后验证:
mysql \
--host=db.example.internal \
--user=shop_app --password \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
-e "SHOW STATUS LIKE 'Ssl_cipher';"Ssl_cipher 应有非空值。证书不可信、过期或主机名不匹配时应拒绝连接,不能退回明文绕过。
mysql CLI 的安全使用边界
mysql CLI 随 MySQL 客户端工具发行。交互登录不要把密码写进参数或 MYSQL_PWD,可以让客户端提示输入,或用 mysql_config_editor 创建受当前操作系统账户保护的加密登录路径:
mysql_config_editor set \
--login-path=shop \
--host=db.example.internal \
--user=shop_app \
--password
mysql --login-path=shop \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--safe-updates.mylogin.cnf 仍需依靠操作系统账户和文件权限保护,不能代替集中密钥系统。--safe-updates 会拒绝没有键条件或 LIMIT 的高风险 UPDATE、DELETE,但不能代替事务、评审和备份。
交互终端默认可能把语句写入历史。临时敏感会话可以禁用持久历史;--histignore 可追加不记录的模式,默认模式已经包含 *IDENTIFIED* 和 *PASSWORD*:
export MYSQL_HISTFILE=/dev/null
mysql --login-path=shop \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--histignore='*TOKEN*:*SECRET*'自动化读取使用 --batch --raw 获得稳定的制表符分隔输出,并检查退出码。--force 会在 SQL 错误后继续执行,迁移、恢复和修复任务不得启用它,否则可能把失败伪装成部分成功。
mysql --login-path=shop \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--batch --raw \
-e "SELECT USER(), CURRENT_USER(), @@hostname;"需要图形化查询、Visual Explain、建模或导入导出时,参见 MySQL Workbench。
6. 表、字段和索引
金额使用 DECIMAL 或最小货币单位整数,不使用浮点数;时间统一时区语义;文本明确长度和字符集。
把下面内容保存为 src/main/resources/db/migration/V1__init.sql;直接做 SQL 实验时也可以逐条执行:
USE shop;
CREATE TABLE customer (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
email VARCHAR(254) NOT NULL,
display_name VARCHAR(100) NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_customer_email (email)
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
order_no VARCHAR(32) NOT NULL,
customer_id BIGINT UNSIGNED NOT NULL,
status TINYINT UNSIGNED NOT NULL,
total_amount DECIMAL(18,2) NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (id),
UNIQUE KEY uk_orders_order_no (order_no),
KEY idx_orders_customer_created (customer_id, created_at, id),
KEY idx_orders_status_created (status, created_at, id),
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customer(id),
CONSTRAINT chk_orders_amount CHECK (total_amount >= 0)
) ENGINE=InnoDB;
CREATE TABLE inventory (
sku_id BIGINT UNSIGNED NOT NULL,
available INT UNSIGNED NOT NULL,
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (sku_id),
CONSTRAINT chk_inventory_available CHECK (available >= 0)
) ENGINE=InnoDB;SHOW CREATE TABLE orders\G外键列和被引用列类型、符号位必须一致。高并发系统是否使用外键要结合删除流程和分片边界判断,但唯一性、非空和金额范围等约束不应全部下放应用。
联合索引按过滤、连接、排序和分页共同设计:
EXPLAIN ANALYZE
SELECT id, order_no, status, total_amount, created_at
FROM orders
WHERE customer_id = 1001
AND (created_at, id) < (CURRENT_TIMESTAMP, 900000)
ORDER BY created_at DESC, id DESC
LIMIT 50;关注实际行数、循环次数、访问方式和耗时。目标索引被使用且扫描行数接近返回行数,才说明路径合理。
7. 事务和并发
库存扣减使用条件更新,避免先查后改竞态:
START TRANSACTION;
UPDATE inventory
SET available = available - 1,
updated_at = CURRENT_TIMESTAMP(6)
WHERE sku_id = 2001
AND available >= 1;
SELECT ROW_COUNT() AS affected_rows;
COMMIT;affected_rows=1 表示成功,0 表示库存不足或记录不存在。订单、库存和幂等记录在同库时应放入同一事务;外部支付和消息采用可恢复的跨系统流程。
事务应固定资源访问顺序并保持短小。应用只对死锁和可判定瞬时错误执行有限退避重试。
8. Java、HikariCP 和 Flyway
Maven 依赖版本应由项目依赖治理锁定:
先确认项目使用 JDK 17 或更高版本以及可用的 Maven:
java -version
mvn -version两条命令都应正常返回版本;Maven 显示的 Java Home 应指向准备运行应用的 JDK。
<dependencies>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.5.0</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>6.3.1</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>11.13.0</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>11.13.0</version>
</dependency>
</dependencies>package example;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public final class DatabaseConfig {
private DatabaseConfig() {}
public static HikariDataSource open(
String userEnvironment,
String passwordEnvironment) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(
"jdbc:mysql://db.example.internal:3306/shop"
+ "?sslMode=VERIFY_IDENTITY"
+ "&serverTimezone=Asia/Shanghai");
config.setUsername(System.getenv(userEnvironment));
config.setPassword(System.getenv(passwordEnvironment));
config.addDataSourceProperty(
"trustCertificateKeyStoreUrl",
"file:/etc/ssl/mysql/truststore.p12");
config.addDataSourceProperty(
"trustCertificateKeyStorePassword",
System.getenv("MYSQL_TRUSTSTORE_PASSWORD"));
config.addDataSourceProperty("connectTimeout", "3000");
config.addDataSourceProperty("socketTimeout", "5000");
config.setMaximumPoolSize(20);
config.setMinimumIdle(2);
config.setConnectionTimeout(3000);
config.setValidationTimeout(1000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1740000);
return new HikariDataSource(config);
}
}HikariConfig.connectionTimeout 只限制从连接池等待连接的时间;Connector/J 的 connectTimeout 和 socketTimeout 才限制建连与网络读取。池上限乘以应用实例数后必须低于数据库预算。事务代码:
package example;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.sql.DataSource;
public final class InventoryRepository {
private final DataSource dataSource;
public InventoryRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
public void deduct(long skuId, int quantity) throws SQLException {
if (quantity <= 0) {
throw new IllegalArgumentException("扣减数量必须大于 0");
}
String sql = """
UPDATE inventory
SET available = available - ?
WHERE sku_id = ? AND available >= ?
""";
try (Connection connection = dataSource.getConnection()) {
connection.setAutoCommit(false);
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setQueryTimeout(3);
statement.setInt(1, quantity);
statement.setLong(2, skuId);
statement.setInt(3, quantity);
if (statement.executeUpdate() != 1) {
throw new IllegalStateException("库存不足");
}
connection.commit();
} catch (SQLException | RuntimeException exception) {
connection.rollback();
throw exception;
}
}
}
}Flyway 迁移在应用放流前执行:
package example;
import javax.sql.DataSource;
import org.flywaydb.core.Flyway;
public final class SchemaMigration {
private SchemaMigration() {}
public static void migrate(DataSource dataSource) {
Flyway.configure()
.dataSource(dataSource)
.locations("classpath:db/migration")
.load()
.migrate();
}
}应用入口必须关闭连接池;迁移完成后再构造仓储:
package example;
import com.zaxxer.hikari.HikariDataSource;
public final class Application {
private Application() {}
public static void main(String[] args) throws Exception {
try (HikariDataSource migrationDataSource = DatabaseConfig.open(
"MYSQL_MIGRATE_USER", "MYSQL_MIGRATE_PASSWORD")) {
SchemaMigration.migrate(migrationDataSource);
}
try (HikariDataSource appDataSource = DatabaseConfig.open(
"MYSQL_APP_USER", "MYSQL_APP_PASSWORD")) {
try (var connection = appDataSource.getConnection()) {
if (!connection.isValid(2)) {
throw new IllegalStateException("数据库连接验证失败");
}
}
System.out.println("数据库迁移与连接验证通过");
}
}
}mvn -q compile
mvn -q -Dexec.mainClass=example.Application \
org.codehaus.mojo:exec-maven-plugin:3.5.0:java全新数据库也应输出“数据库迁移与连接验证通过”。迁移数据源使用 shop_migrate,应用数据源使用 shop_app,不能让业务连接继承 DDL 权限。真实扣减由业务请求携带 skuId 和正数 quantity 后调用 InventoryRepository,不能在应用启动时写入硬编码库存。
最小集成测试使用隔离数据库,覆盖成功扣减、库存不足回滚和非法负数。测试仍通过同一连接池再次查询,能同时发现事务未回滚和连接未归还:
package example;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public final class InventoryIntegrationTest {
private InventoryIntegrationTest() {}
public static void main(String[] args) throws Exception {
try (HikariDataSource migrationDataSource = DatabaseConfig.open(
"MYSQL_MIGRATE_USER", "MYSQL_MIGRATE_PASSWORD")) {
SchemaMigration.migrate(migrationDataSource);
}
try (HikariDataSource appDataSource = DatabaseConfig.open(
"MYSQL_APP_USER", "MYSQL_APP_PASSWORD")) {
try (Connection connection = appDataSource.getConnection();
PreparedStatement statement = connection.prepareStatement("""
INSERT INTO inventory(sku_id, available) VALUES (2001, 5)
ON DUPLICATE KEY UPDATE available = 5
""")) {
statement.executeUpdate();
}
InventoryRepository repository = new InventoryRepository(appDataSource);
repository.deduct(2001L, 2);
requireAvailable(appDataSource, 3);
try {
repository.deduct(2001L, 10);
throw new AssertionError("库存不足必须失败");
} catch (IllegalStateException expected) {
requireAvailable(appDataSource, 3);
}
try {
repository.deduct(2001L, -1);
throw new AssertionError("负数必须失败");
} catch (IllegalArgumentException expected) {
requireAvailable(appDataSource, 3);
}
}
}
private static void requireAvailable(HikariDataSource dataSource, int expected)
throws Exception {
try (Connection connection = dataSource.getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT available FROM inventory WHERE sku_id = 2001");
ResultSet result = statement.executeQuery()) {
if (!result.next() || result.getInt(1) != expected) {
throw new AssertionError("库存不符合预期");
}
}
}
}mvn -q compile
mvn -q -Dexec.mainClass=example.InventoryIntegrationTest \
org.codehaus.mojo:exec-maven-plugin:3.5.0:java进程退出码应为 0;任一断言失败都会返回非零。测试库账户、CA 和密码通过环境变量注入,不能指向生产实例。
结构变更要兼容新旧应用同时运行。先增加兼容字段或索引,再发布读取逻辑,最后清理旧结构。
9. Python 和 Node.js 接入
python3 -m venv .venv
. .venv/bin/activate
pip install "mysql-connector-python==9.4.0"import os
import mysql.connector
connection = mysql.connector.connect(
host=os.environ["MYSQL_HOST"],
database="shop",
user=os.environ["MYSQL_USER"],
password=os.environ["MYSQL_PASSWORD"],
connection_timeout=3,
ssl_ca="/etc/ssl/mysql/ca.pem",
ssl_verify_identity=True,
)
try:
connection.start_transaction()
with connection.cursor(dictionary=True) as cursor:
cursor.execute(
"SELECT id, status FROM orders "
"WHERE order_no = %s FOR UPDATE",
("ORDER-DEMO-001",),
)
if cursor.fetchone() is None:
raise ValueError("订单不存在")
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()npm install mysql2import mysql from "mysql2/promise";
const pool = mysql.createPool({
host: process.env.MYSQL_HOST,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: "shop",
connectionLimit: 10,
waitForConnections: true,
queueLimit: 100,
connectTimeout: 3000,
ssl: { ca: process.env.MYSQL_CA_PEM },
});
const connection = await pool.getConnection();
try {
await connection.beginTransaction();
const [rows] = await connection.execute(
"SELECT id, status FROM orders WHERE order_no = ? FOR UPDATE",
["ORDER-DEMO-001"],
);
if (rows.length !== 1) throw new Error("订单不存在");
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}所有语言都必须使用参数绑定,设置连接、查询和请求总超时,异常时回滚并归还连接。
10. 慢 SQL、计划和监控
SHOW VARIABLES WHERE Variable_name IN (
'slow_query_log',
'slow_query_log_file',
'long_query_time'
);
SELECT digest_text,
count_star,
ROUND(sum_timer_wait / 1e12, 2) AS total_seconds,
ROUND(avg_timer_wait / 1e9, 2) AS avg_ms,
sum_rows_examined,
sum_rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC
LIMIT 20;先按总消耗排序,再结合单次延迟、调用量和业务重要性。优化顺序通常是减少扫描、补索引、改写 SQL、修正统计信息,再评估参数和硬件。
ANALYZE TABLE orders;
EXPLAIN ANALYZE
SELECT id, order_no
FROM orders
WHERE status = 1
ORDER BY created_at DESC, id DESC
LIMIT 50;EXPLAIN ANALYZE 会真实执行查询,不能直接对不可控大查询使用。
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Threads_connected',
'Threads_running',
'Connections',
'Aborted_connects',
'Questions',
'Slow_queries',
'Created_tmp_disk_tables'
);
SHOW ENGINE INNODB STATUS\Gpidstat -p "$(pidof mysqld)" 1
iostat -xz 1
vmstat 1
free -h
df -h监控应覆盖请求率、错误率、延迟、连接、Buffer Pool、脏页、Redo、锁等待、死锁、临时磁盘表、磁盘、复制延迟和恢复结果。
11. 容量管理
SELECT table_schema,
table_name,
ROUND(data_length / 1024 / 1024, 1) AS data_mb,
ROUND(index_length / 1024 / 1024, 1) AS index_mb,
table_rows
FROM information_schema.tables
WHERE table_schema NOT IN (
'mysql', 'information_schema', 'performance_schema', 'sys'
)
ORDER BY data_length + index_length DESC
LIMIT 30;table_rows 对 InnoDB 通常是估算值。容量模型还要包含 Binlog、Undo、Redo、临时目录、DDL 临时空间、备份和恢复副本。每连接排序缓冲会随并发累加,不应盲目全局调大。
12. 逻辑备份和恢复
CREATE USER 'backup'@'10.%'
IDENTIFIED BY 'Replace-Backup-Password'
REQUIRE SSL;
GRANT SELECT, SHOW VIEW, TRIGGER, EVENT,
RELOAD, REPLICATION CLIENT
ON *.* TO 'backup'@'10.%';set -euo pipefail
backup_name="mysql-full-$(date +%F-%H%M).sql.gz"
backup_tmp="${backup_name}.part"
mysqldump \
--host=db.example.internal \
--user=backup --password \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--single-transaction \
--no-tablespaces \
--source-data=2 \
--set-gtid-purged=ON \
--routines --events --triggers \
--hex-blob \
--all-databases \
| gzip > "$backup_tmp"
gzip -t "$backup_tmp"
mv -- "$backup_tmp" "$backup_name"
sha256sum "$backup_name" > "${backup_name}.sha256"--single-transaction 为 InnoDB 提供一致性视图,但长备份会延长旧版本保留,DDL 仍可能干扰。这个全实例基线用 --source-data=2 以注释保存一致性快照对应的 Binlog 文件和 Position,并用 --set-gtid-purged=ON 保存源端已执行 GTID。只导出 shop 的迁移文件不能直接冒充全实例 PITR 或 GTID Replica 基线。大库优先采用经过验证的物理备份工具或 MySQL Shell Dump。
BACKUP_FILE="${BACKUP_FILE:?export the validated backup file path}"
sha256sum -c "${BACKUP_FILE}.sha256"
zgrep -m1 '^-- CHANGE REPLICATION SOURCE TO' "$BACKUP_FILE"
zgrep -m1 'GTID_PURGED' "$BACKUP_FILE"校验和应返回 OK,后两条命令都应找到元数据。还要记录源端版本和 SHOW BINARY LOG STATUS 输出。恢复必须使用空的隔离实例:
set -euo pipefail
BACKUP_FILE="${BACKUP_FILE:?export the validated backup file path}"
sha256sum -c "${BACKUP_FILE}.sha256"
gzip -dc "$BACKUP_FILE" \
| mysql --host=restore-db --user=root --password \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--binary-modeCHECK TABLE shop.customer, shop.orders;
SELECT COUNT(*) FROM shop.customer;
SELECT COUNT(*) FROM shop.orders;行数只是基础证据,还应验证关键业务汇总、随机样本、约束、索引和应用只读查询。
13. MySQL Shell 并行迁移
mysqlsh \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--uri backup@db.example.internal:3306 \
--js --execute \
'util.dumpInstance("/backup/mysql-full", {threads: 8, consistent: true})'mysqlsh \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--uri root@restore-db:3306 \
--js --execute \
'util.loadDump("/backup/mysql-full", {threads: 8, updateGtidSet: "append", skipBinlog: true})'开始前确认目标版本兼容、磁盘、权限和目标实例为空。Load Dump 默认保存进度并在重试时继续;不要在续传时设置 resetProgress: true,它会从头加载且不会去重。只有确认已删除目标中先前加载的全部对象后才允许重置进度。updateGtidSet 只用于实例或 Schema Dump 的 GTID 初始化,且非托管 MySQL 需要同时设置 skipBinlog: true;Group Replication 已运行时不得这样修改 GTID。进度支持继续不代表数据已经验收,恢复后仍需业务校验。
14. Binlog 时间点恢复
SHOW BINARY LOGS;
SHOW BINARY LOG STATUS;
CREATE USER 'binlog_reader'@'10.%'
IDENTIFIED BY 'Replace-Binlog-Password'
REQUIRE SSL;
GRANT REPLICATION SLAVE ON *.*
TO 'binlog_reader'@'10.%';远程读取 Binlog 需要 REPLICATION SLAVE,不能复用只有 REPLICATION CLIENT 的逻辑备份账户。先还原早于故障点的全量基线,从备份头读取起始文件和 Position;再用 mysqlbinlog --base64-output=DECODE-ROWS -vv 查明误操作事务的 GTID 与 end_log_pos。下面示例假设起点和停止点位于两个连续日志文件;中间文件必须按顺序完整加入:
BASE_POS="${BASE_POS:?export the position recorded by the full backup}"
STOP_POS="${STOP_POS:?export the end position before the bad transaction}"
mysqlbinlog \
--read-from-remote-server \
--host=db.example.internal \
--user=binlog_reader --password \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--start-position="$BASE_POS" \
mysql-bin.000421 \
> recover.sql
mysqlbinlog \
--read-from-remote-server \
--host=db.example.internal \
--user=binlog_reader --password \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--stop-position="$STOP_POS" \
mysql-bin.000422 \
>> recover.sqlless recover.sql
mysql --host=restore-db --user=root --password \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--binary-mode < recover.sql时间点只用于缩小搜索范围,受时区和事务边界影响。执行前确认第一条恢复事务紧接备份坐标、最后一条早于误操作;恢复后核对 @@global.gtid_executed、关键业务汇总和应用只读查询。精确恢复使用已确认的文件与 Position 或 GTID,并始终在隔离实例验证。
15. GTID 异步复制
Replica 配置:
[mysqld]
server_id = 102
log_bin = mysql-bin
relay_log = relay-bin
gtid_mode = ON
enforce_gtid_consistency = ON
read_only = ON
super_read_only = ONSource 创建账户:
CREATE USER 'repl'@'10.%'
IDENTIFIED BY 'Replace-Repl-Password'
REQUIRE SSL;
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.%';从包含源端 GTID 元数据的一致性全量备份、MySQL Shell Dump 或 Clone 初始化 Replica。若使用 Shell Dump,必须在 Group Replication 启动前按上一节设置 updateGtidSet;若手工导入,确认备份中的 GTID_PURGED 已成功写入空目标。然后验证源端快照 GTID 是 Replica 已执行集合的子集,再启动自动定位:
SELECT GTID_SUBSET(
'source-uuid:1-12345',
@@GLOBAL.gtid_executed
) AS snapshot_is_present;把第一个参数替换为备份时记录的真实 GTID 集,结果必须为 1。之后:
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = 'mysql-source.internal',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'Replace-Repl-Password',
SOURCE_AUTO_POSITION = 1,
SOURCE_SSL = 1,
SOURCE_SSL_CA = '/etc/mysql/ca.pem',
SOURCE_SSL_VERIFY_SERVER_CERT = 1;
START REPLICA;
SHOW REPLICA STATUS\GI/O 和 SQL 线程应为 Yes,错误字段为空。Seconds_Behind_Source 只能辅助判断,还要结合接收 GTID、执行 GTID和业务时间戳。
16. InnoDB Cluster
至少准备三个分布在独立故障域、网络可达、版本一致、时钟同步的实例。三个节点必须使用不同的 server_id,证书 SAN 必须分别匹配 mysql-1.internal、mysql-2.internal 和 mysql-3.internal。使用具备创建账户和配置实例权限的临时引导账户,对每个节点运行配置;clusterAdminPassword 留空时 MySQL Shell 会交互提示,不把密码写进脚本:
mysqlsh \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--uri bootstrap_admin@mysql-1.internal:3306const mysql1 = {
scheme: "mysql", user: "bootstrap_admin",
host: "mysql-1.internal", port: 3306,
"ssl-mode": "VERIFY_IDENTITY", "ssl-ca": "/etc/mysql/ca.pem"
}
const mysql2 = { ...mysql1, host: "mysql-2.internal" }
const mysql3 = { ...mysql1, host: "mysql-3.internal" }
dba.configureInstance(
mysql1,
{ clusterAdmin: "cluster_admin@10.%", restart: true }
)
dba.configureInstance(
mysql2,
{ clusterAdmin: "cluster_admin@10.%", restart: true }
)
dba.configureInstance(
mysql3,
{ clusterAdmin: "cluster_admin@10.%", restart: true }
)三个调用都应报告实例已可用于 InnoDB Cluster。重连种子节点,明确建立后续 dba.createCluster() 使用的全局会话:
shell.connect({ ...mysql1, user: "cluster_admin" })
const cluster = dba.createCluster("shopCluster", {
communicationStack: "MYSQL",
memberSslMode: "VERIFY_IDENTITY",
replicationAllowedHost: "10.%"
})
cluster.addInstance(
{ ...mysql2, user: "cluster_admin" },
{ recoveryMethod: "clone" }
)
cluster.addInstance(
{ ...mysql3, user: "cluster_admin" },
{ recoveryMethod: "clone" }
)
cluster.status({ extended: 1 })应显示一个 PRIMARY 和其余 SECONDARY,成员为 ONLINE。应用通过 Router 连接:
sudo mysqlrouter \
--bootstrap cluster_admin@mysql-1.internal:3306 \
--directory /etc/mysqlrouter-shop \
--user=mysqlrouter \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--client-ssl-mode=REQUIRED \
--server-ssl-mode=REQUIRED \
--server-ssl-verify=VERIFY_IDENTITY \
--server-ssl-ca=/etc/mysql/ca.pem
sudo -u mysqlrouter /etc/mysqlrouter-shop/start.sh在第二个故障域重复 bootstrap 和启动,形成两个独立 Router。写连接使用 6446,只读连接使用 6447;先分别执行 SELECT @@hostname, @@read_only,再停止当前 PRIMARY,验证写端口切到唯一新主、旧主不可写、两个 Router 都更新路由且连接池能够重连。生产由 systemd 或编排平台管理 Router,客户端到 Router 的证书如果需要身份校验,还应为两个 Router 部署各自匹配 SAN 的证书,而不是依赖自动生成证书。
17. 安全与审计
数据库端口只对应用、管理和复制网段开放。账户按用途拆分,禁止应用拥有 SUPER、FILE、CREATE USER 等管理权限。
SELECT user, host, account_locked, password_expired
FROM mysql.user
ORDER BY user, host;
SELECT user, host
FROM mysql.user
WHERE user = '';TLS 证书、密码和备份密钥需要轮换。审计日志发送到独立存储;通用查询日志开销和敏感信息风险都较高,不宜长期在生产开启。
18. 升级、迁移和停止
SELECT VERSION();
SHOW PLUGINS;
SHOW VARIABLES;
SHOW REPLICA STATUS\Gmysqlsh \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--uri upgrade_admin@db.example.internal:3306 \
-- util check-for-server-upgrade所有错误在升级前处理。先恢复生产备份到隔离环境,升级副本并回放真实 SQL,再按拓扑逐节点升级。跨大版本回退通常不能复用升级后的数据目录,可靠回退依赖升级前备份和逻辑迁移。
停止实验环境但保留数据:
docker compose down
du -sh data删除数据是不可逆操作,不把数据清理与停止服务合并。
四、问题处理
1. MySQL 无法启动
现象
服务失败、容器反复重启、3306 没有监听。
影响
所有新连接失败;单节点系统完全不可用。
常见根因
配置错误、数据目录权限错误、磁盘满、端口占用、升级不兼容。
定位顺序
先看服务和日志,再验证配置、端口、磁盘和目录权限。
定位命令
sudo systemctl status mysqld --no-pager
sudo journalctl -u mysqld -n 200 --no-pager
sudo mysqld --validate-config
sudo ss -lntp | grep ':3306'
df -h输出判断
unknown variable 是配置错误;Permission denied 是权限;No space left 是容量;其他 PID 监听 3306 是冲突。
解决步骤
回退最近配置或改正错误项,恢复目录属主,扩容或安全清理日志,处理冲突进程。升级失败按官方路径和备份恢复,不能强删系统表。
验证
sudo systemctl restart mysqld
mysqladmin -h 127.0.0.1 -u monitor -p ping返回 mysqld is alive 且日志无新错误。
预防
变更前校验配置,监控磁盘,在隔离环境验证升级并保留可恢复备份。
2. 连接被拒绝或超时
现象
客户端返回 Connection refused 或连接超时。
影响
应用不能建立新连接,连接池请求堆积。
常见根因
服务未监听、绑定地址错误、防火墙未放行、DNS 错误或端口错误。
定位顺序
从服务监听开始,再检查本机连接、跨机 TCP、DNS 和网络策略。
定位命令
sudo ss -lntp | grep ':3306'
mysqladmin -h 127.0.0.1 -u monitor -p ping
nc -vz db.example.internal 3306
dig +short db.example.internal输出判断
没有监听是实例问题;本机成功但远程失败是网络问题;DNS 返回旧地址是解析问题。
解决步骤
启动服务,修正绑定地址,按最小来源放通 3306,更新 DNS 或客户端地址。不要暴露到公网。
验证
mysql -h db.example.internal -u shop_app -p \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
-e "SELECT 1;"返回 1 即恢复。
预防
持续探测 DNS、TCP、TLS 和业务查询。
3. 认证或 TLS 失败
现象
出现 Access denied、证书验证失败或禁止非安全传输。
影响
特定账户或全部强制 TLS 客户端无法连接。
常见根因
账户 Host 不匹配、密码错误、账户锁定、权限缺失、CA 不可信、证书过期或主机名不符。
定位顺序
确认实际来源和目标,再查账户、授权、TLS 和证书链。
定位命令
SELECT user, host, account_locked, password_expired
FROM mysql.user WHERE user = 'shop_app';
SHOW GRANTS FOR 'shop_app'@'10.%';openssl s_client -connect db.example.internal:3306 \
-starttls mysql \
-CAfile /etc/mysql/ca.pem \
-verify_hostname db.example.internal输出判断
没有匹配 Host 会拒绝;Verify return code: 0 表示证书链通过。
解决步骤
修正最小范围账户和授权,轮换密码或证书,分发正确 CA。不能关闭 TLS 绕过。
验证
SHOW STATUS LIKE 'Ssl_cipher';
SELECT CURRENT_USER(), USER();加密算法非空且授权账户符合预期。
预防
监控证书到期,定期审计账户和授权。
4. 连接数耗尽
现象
应用收到 Too many connections。
影响
新请求失败,运维也可能无法登录。
常见根因
连接泄漏、池总量失控、慢 SQL、长事务或突发流量。
定位顺序
确认上限和当前数,再按用户、主机、状态和时间定位。
定位命令
SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SELECT user, host, command, state, COUNT(*) AS connections
FROM information_schema.processlist
GROUP BY user, host, command, state
ORDER BY connections DESC;输出判断
大量长期 Sleep 指向池过大或泄漏;大量执行中连接指向慢 SQL、锁或流量。
解决步骤
限流并修复泄漏、慢 SQL 或锁等待,缩小连接池并保留管理连接。评估内存后才调整上限。
验证
连接和运行线程回落,请求恢复且不再持续增长。
预防
建立全局连接预算,监控池等待和连接生命周期。
5. SQL 突然变慢
现象
接口高分位延迟升高,慢日志出现目标 SQL。
影响
请求积压、连接增加并拖慢同实例业务。
常见根因
数据分布变化、索引缺失、统计陈旧、返回过多、类型转换或资源争用。
定位顺序
确认 SQL 和影响,再看真实计划、扫描行数、等待和系统资源。
定位命令
EXPLAIN ANALYZE
SELECT id, order_no FROM orders
WHERE customer_id = 1001
ORDER BY created_at DESC, id DESC LIMIT 50;iostat -xz 1
pidstat -p "$(pidof mysqld)" 1输出判断
扫描远大于返回行数是路径低效;I/O 等待高是扫描或缓存问题。
解决步骤
补合适索引、改写查询、减少返回、更新统计。先在真实数据副本验证。
验证
对比扫描行数、执行时间、慢日志和业务 P95。
预防
保存核心 SQL 基线,升级和数据增长前做回放。
6. 锁等待超时
现象
返回 Lock wait timeout exceeded。
影响
事务失败,请求等待并占用连接。
常见根因
长事务、事务内远程调用、大批更新或条件无索引。
定位顺序
找等待者和阻塞者,再看阻塞 SQL、开始时间和锁对象。
定位命令
SELECT * FROM sys.innodb_lock_waits
ORDER BY wait_age_secs DESC;
SELECT trx_id, trx_mysql_thread_id, trx_started, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;输出判断
blocking_pid 是阻塞连接;很早开始或当前 Sleep 常表示遗留事务。
解决步骤
确认业务后终止异常会话,修复事务边界、索引和批量大小。增大超时不是根治。
验证
等待链消失,事务成功,错误率恢复。
预防
设置事务上限,禁止事务内远程调用,监控长事务。
7. 发生死锁
现象
事务收到 Deadlock found。
影响
InnoDB 回滚其中一个事务,业务请求可能失败。
常见根因
不同顺序访问相同记录、范围锁交叉、索引不足或事务过大。
定位顺序
读取最近死锁,还原 SQL、索引、锁顺序和事务边界。
定位命令
SHOW ENGINE INNODB STATUS\G
SELECT * FROM performance_schema.data_lock_waits;输出判断
LATEST DETECTED DEADLOCK 列出双方事务、等待锁和回滚事务。
解决步骤
统一访问顺序,缩短事务,增加精确索引,拆小批次;应用仅有限退避重试。
验证
并发回归中结果正确,死锁率下降。
预防
统一写顺序,保留死锁日志,发布前做并发测试。
8. 长事务和 Undo 增长
现象
磁盘增长、查询变慢,History List Length 持续上升。
影响
旧版本无法清理,影响存储、查询、备份和 DDL。
常见根因
事务不提交、只读事务遗留、批处理过大。
定位顺序
查看最早事务,再确认会话、应用和 Undo 状态。
定位命令
SELECT trx_mysql_thread_id, trx_started, trx_state, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
SHOW ENGINE INNODB STATUS\G输出判断
早期事务持续存在且 History List Length 很大说明清理受阻。
解决步骤
确认后提交或回滚异常事务,修复连接生命周期,把批处理拆成小事务。
验证
长事务消失,History List Length 下降,磁盘和延迟稳定。
预防
监控事务年龄,设置请求和事务上限。
9. 磁盘空间不足
现象
出现 No space left on device,写入失败或实例退出。
影响
事务无法提交,Binlog 和临时文件无法写入。
常见根因
数据增长、Binlog 未过期、日志暴涨、DDL 临时文件或备份写入数据盘。
定位顺序
先看文件系统容量和可创建文件数量,再按目录、表、Binlog 定位。
定位命令
df -h
df -i
sudo du -xhd1 /var/lib/mysql | sort -h
sudo du -xhd1 /var/log | sort -hSHOW BINARY LOGS;输出判断
容量耗尽但文件数量资源充足,通常是大文件增长;容量仍有余量但无法创建文件,通常是小文件过多;Binlog 列表异常长则说明保留链路有问题。
解决步骤
停止非必要写入并扩容;只用 PURGE BINARY LOGS 清理确认不再需要的日志;禁止直接删除 MySQL 文件。
验证
恢复安全余量,写事务成功,日志无新空间错误。
预防
按增长率告警,配置保留期,隔离备份和数据盘。
10. 临时表大量落盘
现象
Created_tmp_disk_tables 快速增长,排序聚合变慢。
影响
临时目录 I/O 增加,查询相互争用磁盘。
常见根因
大排序分组、缺索引、结果列不适合内存临时表或阈值不合理。
定位顺序
确认计数增速,再找产生临时表的语句和计划。
定位命令
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
SELECT digest_text, sum_created_tmp_disk_tables
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_created_tmp_disk_tables DESC
LIMIT 20;输出判断
少数 SQL 占多数落盘时优先优化 SQL;全局增大阈值会放大并发内存。
解决步骤
补适合排序分组的索引,减少列和行,拆分大查询;评估内存预算后再调参数。
验证
计划改善,落盘增速和磁盘时延下降。
预防
监控落盘率,评审大排序,容量模型包含临时目录。
11. Replica I/O 线程停止
现象
Replica_IO_Running: No。
影响
副本停止接收日志,切换 RPO 持续扩大。
常见根因
网络、复制账户、TLS、Source 地址或所需 Binlog 被清理。
定位顺序
查看复制错误,再检查 DNS、端口、账户、TLS 和 Source 日志。
定位命令
SHOW REPLICA STATUS\Gnc -vz mysql-source.internal 3306
dig +short mysql-source.internal输出判断
Last_IO_Error 指明认证、网络或日志缺失;日志不存在时不能简单跳过。
解决步骤
恢复网络、账户或证书;日志丢失时从可信一致性备份重建。
验证
I/O 线程为 Yes,接收 GTID 推进,延迟收敛。
预防
监控 I/O 线程、Binlog 窗口和证书有效期。
12. Replica SQL 线程停止
现象
Replica_SQL_Running: No。
影响
副本不再应用变更,数据逐渐落后。
常见根因
副本误写、DDL 不兼容、对象缺失、版本或 SQL 模式差异。
定位顺序
读取最后错误,定位事务和对象,再比较 Source 与 Replica。
定位命令
SHOW REPLICA STATUS\G
SELECT *
FROM performance_schema.replication_applier_status_by_worker
WHERE LAST_ERROR_NUMBER <> 0;输出判断
重复键或记录不存在通常是数据漂移;DDL 错误是发布或版本不一致。
解决步骤
停止路由到不一致副本,确认差异并修复或重建。不要盲目跳过事务。
验证
SQL 线程恢复,GTID 推进,关键表校验一致。
预防
保持 super_read_only=ON,统一版本和 DDL 发布链路。
13. 复制延迟升高
现象
副本应用速度追不上 Source,读到旧数据。
影响
只读数据陈旧,切换可能遗漏尚未应用的状态。
常见根因
大事务、缺主键、慢磁盘、锁等待、备份或查询争用。
定位顺序
比较接收和执行进度,再查 Applier、长事务、磁盘和查询负载。
定位命令
SHOW REPLICA STATUS\G
SELECT * FROM performance_schema.replication_applier_status_by_worker;iostat -xz 1
vmstat 1输出判断
已接收未执行是应用瓶颈;接收也落后是网络或 Source 发送问题。
解决步骤
暂停重查询和备份争用,修复大事务和无主键表,评估并行复制和磁盘。
验证
执行 GTID 与 Source 收敛,业务时间戳回到目标范围。
预防
限制事务大小,为复制表建立主键,按真实延迟告警。
14. GTID 分叉或缺失
现象
自动定位失败、GTID_PURGED 冲突或节点各有独有事务。
影响
副本无法安全加入,错误切换造成数据分叉。
常见根因
日志过早清理、副本被写、错误设置 GTID、旧主未隔离。
定位顺序
冻结拓扑变更,采集每个节点的 GTID、只读状态和角色。
定位命令
SELECT @@server_uuid,
@@global.gtid_executed,
@@global.gtid_purged,
@@read_only,
@@super_read_only;输出判断
两个候选主都有独有 GTID 表示分叉,不能直接互相复制。
解决步骤
停止写入,确定权威数据集,核对独有事务;从权威节点可信备份重建其他节点。
验证
GTID 集合关系正确,副本追赶且只有唯一写入口。
预防
强制副本只读,延长日志窗口,切换集成隔离和 GTID 校验。
15. 备份无法恢复
现象
导入报错或恢复后数据不完整。
影响
故障时不能达到 RTO/RPO,备份实际不可用。
常见根因
备份损坏、对象缺失、版本不兼容、空间不足或从未演练。
定位顺序
检查文件和日志,再在隔离实例恢复并核对对象和业务数据。
定位命令
gzip -t shop-backup.sql.gz
gzip -dc shop-backup.sql.gz | head -n 30
df -h输出判断
压缩校验失败是文件损坏;空间不足会导致恢复中断。
解决步骤
选择校验通过的完整备份,准备兼容版本和空间,按工具要求恢复。
验证
表检查、对象清单、关键汇总和应用只读验收通过。
预防
记录退出码和校验和,定期恢复到隔离环境。
16. 时间点恢复越过误操作
现象
恢复实例仍包含误删除或错误更新。
影响
恢复结果不可信,覆盖生产会扩大损失。
常见根因
停止时间不准、时区错误、事务跨边界或选错 Position。
定位顺序
停止重放,确认误操作 GTID、文件、Position 和时间戳,再重建。
定位命令
mysqlbinlog --base64-output=DECODE-ROWS -vv \
mysql-bin.000422 | less输出判断
目标事务的 GTID、end_log_pos 和表变更共同确定边界。
解决步骤
重新恢复全量备份,在目标事务前的精确 Position 或 GTID 停止。
验证
误操作未出现,故障前关键事务存在,业务对账通过。
预防
持续归档 Binlog,记录时区,演练按 Position/GTID 恢复。
17. 切换后出现双写
现象
新主开放写入后,旧主仍接受业务写入。
影响
产生无法自动合并的数据分叉。
常见根因
隔离缺失、DNS 或连接池仍指向旧主、网络分区造成双主。
定位顺序
停止入口写流量,枚举节点角色、只读状态、GTID 和连接去向。
定位命令
SELECT @@hostname, @@server_uuid,
@@read_only, @@super_read_only,
@@global.gtid_executed;dig +short mysql-write.example.internal输出判断
多个节点可写或拥有独有 GTID 即为分叉。
解决步骤
从网络和进程层隔离旧主,确定唯一权威节点;对独有事务做业务核对和补偿。
验证
新连接进入唯一主,旧主不可写,GTID 拓扑和业务对账正确。
预防
切换必须包含 fencing、路由更新和连接池重建,演练网络分区。
18. 崩溃恢复时间过长
现象
启动后长时间处于恢复状态。
影响
RTO 超标,反复重启会进一步延长恢复。
常见根因
脏页和 Redo 较多、磁盘慢、文件损坏或反复强杀。
定位顺序
保持运行并观察日志,再检查磁盘时延、容量和恢复进度。
定位命令
sudo journalctl -u mysqld -f
iostat -xz 1
df -h输出判断
LSN 持续推进表示仍在恢复;重复 I/O 或页校验错误指向存储损坏。
解决步骤
不要反复强杀;保障空间和 I/O。确认损坏时保留原始副本,优先从备份和 Binlog 恢复。
验证
服务监听,日志显示恢复完成,关键表和业务查询通过。
预防
监控容量和磁盘,验证崩溃恢复时长,保持独立备份。
19. 升级后不兼容
现象
出现语法、保留字、认证、驱动或计划问题。
影响
业务失败或性能明显回退。
常见根因
跳过升级检查、驱动不兼容、SQL 模式变化、未测试真实流量或路径不受支持。
定位顺序
确认服务端和驱动版本,再看应用错误、发布说明、检查结果和计划。
定位命令
SELECT VERSION(), @@sql_mode;mysqlsh \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pem \
--uri upgrade_admin@db.example.internal:3306 \
-- util check-for-server-upgrade输出判断
检查器错误必须解决;计划变化以实际扫描和延迟为准。
解决步骤
暂停扩大升级,修复 SQL、驱动或配置。数据目录已不可逆升级时按升级前备份恢复。
验证
核心接口、事务、复制、备份和性能基线通过。
预防
遵循官方路径,在生产副本做检查和流量回放,先升级副本。
