哎哟,看到这标题我忍不住想拍拍你肩膀——兄弟,服务器又炸了?日志里那些 Got signal 11、InnoDB: Fatal error,堆成山了对吧?
别慌。今天咱不整那些虚的教科书理论,就聊点真刀真枪的血泪经验。我见过太多项目,前期跑得欢,一上并发就原地升天。MySQL崩溃这事儿,归根结底就四个字:撑不住。
先搞清楚:你的MySQL到底死在哪
崩溃不是凭空来的。得先问自己几个问题:
你是内存爆了还是IO扛不住?
看你的 SHOW GLOBAL STATUS,InnoDB_buffer_pool_reads 这个值如果一直涨,说明你的 buffer pool 太小,数据都从磁盘硬读,IO 直接爆表。这时候索引再优化也没用,得先加内存。
是连接数太多还是锁冲突?
Threads_connected 接近 max_connections,Threads_running 长期高位,说明连接池没管理好。每个连接都要占内存,线程切换也有开销。我见过一个项目,max_connections 设了 5000,实际并发才 200,结果大部分连接都在空转占资源。
是死锁还是行锁竞争?
Innodb_row_lock_time 和 Innodb_row_lock_waits 这两个值要是天天往上涨,说明你的热点数据被并发修改,锁争抢严重。这时候索引优化能缓解,但根本上要改业务逻辑,把大事务拆小。
索引优化:不是越多越好,而是用得对
最常被踩的坑:盲目加索引
很多开发者有个误区:查询慢就加索引。结果索引加了一堆,插入变慢,表空间爆表,甚至把 buffer pool 都挤爆了。
正确的姿势:
先 EXPLAIN 看一下执行计划。重点看这几个字段:
type:要是ALL(全表扫描),那确实需要索引key:实际使用的索引,如果为NULL,说明没用到索引rows:预估扫描行数,越小越好Extra:要是出现Using filesort或Using temporary,说明查询效率低
实战例子:一个典型的慢查询优化
假设你有张订单表,结构如下:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
status TINYINT DEFAULT 0,
create_time DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
INDEX idx_user (user_id),
INDEX idx_status (status),
INDEX idx_create_time (create_time)
);
有个查询老是慢:
SELECT * FROM orders
WHERE user_id = 12345
AND status = 1
AND create_time > '2024-01-01'
ORDER BY create_time DESC
LIMIT 20;
问题在哪?
你现在的索引是分离的:idx_user、idx_status、idx_create_time。MySQL 只能选一个用,其他的得回来过滤。而且 ORDER BY 还要 filesort。
怎么改?
加个联合索引:
DROP INDEX idx_user ON orders;
DROP INDEX idx_status ON orders;
DROP INDEX idx_create_time ON orders;
CREATE INDEX idx_user_status_time ON orders (user_id, status, create_time);
为什么这样设计?
复合索引遵循最左前缀原则。把 user_id 放第一位,因为等值查询能过滤掉大部分数据。status 放第二位,也是等值过滤。create_time 放最后,既能过滤范围,又能避免 filesort(因为索引本身已经按 create_time 排序了)。
EXPLAIN 一下看看效果:
EXPLAIN SELECT * FROM orders
WHERE user_id = 12345
AND status = 1
AND create_time > '2024-01-01'
ORDER BY create_time DESC
LIMIT 20;
你看 type 变成 ref 了,key 用了 idx_user_status_time,Extra 里也没有 Using filesort。这就对了。
另一个坑:索引字段类型不一致
-- 表结构
CREATE TABLE users (
id BIGINT PRIMARY KEY,
phone VARCHAR(20) NOT NULL,
INDEX idx_phone (phone)
);
-- 查询时传了字符串
SELECT * FROM users WHERE phone = '13800138000'; -- 正常用索引
-- 查询时传了数字(隐式类型转换)
SELECT * FROM users WHERE phone = 13800138000; -- 索引失效!
MySQL 会把 phone 字段转成数字再比较,这导致索引没法用。开发的时候一定要注意类型匹配,别偷懒做隐式转换。
读写分离:别以为配了就能用
常见的错误认知
“读写分离就是主库写、从库读,配个中间件就完事了。”
错。读写分离最容易出现的问题是:主从延迟导致数据不一致。
你刚在从库读了数据,主库还没同步过来。用户刷新页面,发现刚才填的内容没了。这种问题排查起来能让人抓狂。
实战配置:基于 MySQL 官方的 replication
主库配置(master.cnf):
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
expire-logs-days = 7
sync-binlog = 1
innodb-flush-log-at-trx-commit = 1
从库配置(slave.cnf):
[mysqld]
server-id = 2
relay-log = mysql-relay-bin
log-slave-updates = 1
read-only = 1
关键点解释:
binlog-format = ROW:行模式,同步最精确,虽然体积大一点,但避免很多坑sync-binlog = 1:每次事务提交都刷盘,保证主库挂了数据不丢innodb-flush-log-at-trx-commit = 1:同上,事务级别的刷盘保证read-only = 1:从库只读,防止误写
搭建主从复制:
-- 主库上创建复制账号
CREATE USER 'repl'@'%' IDENTIFIED BY 'ReplicationPass123';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
-- 查看主库状态,记住 File 和 Position
SHOW MASTER STATUS;
-- 从库上配置
CHANGE MASTER TO
MASTER_HOST = 'master_ip',
MASTER_USER = 'repl',
MASTER_PASSWORD = 'ReplicationPass123',
MASTER_LOG_FILE = 'mysql-bin.000001',
MASTER_LOG_POS = 154;
START SLAVE;
-- 检查从库状态
SHOW SLAVE STATUS\G
看这两个字段:Slave_IO_Running: Yes 和 Slave_SQL_Running: Yes。要是都 YES,说明复制正常。Seconds_Behind_Master 是延迟时间,最好是 0 或者个位数。
应用层怎么接入?
别用那种傻瓜式中间件,自己写个路由逻辑更可控。
Python 示例(简单的读写分离):
import pymysql
from contextlib import contextmanager
import random
class ReadWriteSplitter:
def __init__(self, master_config, slave_configs):
self.master = master_config
self.slaves = slave_configs
@contextmanager
def get_connection(self, for_write=False):
if for_write:
conn = pymysql.connect(**self.master)
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
else:
# 从库随机选一个,简单负载均衡
slave = random.choice(self.slaves)
conn = pymysql.connect(**slave)
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
# 使用示例
db = ReadWriteSplitter(
master_config={
'host': '192.168.1.10',
'port': 3306,
'user': 'app_user',
'password': 'AppPass123',
'database': 'mydb'
},
slave_configs=[
{'host': '192.168.1.20', 'port': 3306, 'user': 'app_user', 'password': 'AppPass123', 'database': 'mydb'},
{'host': '192.168.1.21', 'port': 3306, 'user': 'app_user', 'password': 'AppPass123', 'database': 'mydb'}
]
)
# 写操作走主库
with db.get_connection(for_write=True) as conn:
with conn.cursor() as cur:
cur.execute("INSERT INTO orders (user_id, status, amount) VALUES (%s, %s, %s)", (123, 1, 99.99))
# 读操作走从库
with db.get_connection(for_write=False) as conn:
with conn.cursor() as cur:
cur.execute("SELECT * FROM orders WHERE user_id = %s", (123,))
rows = cur.fetchall()
注意: 这种简单方案有缺陷,生产环境建议用成熟的中间件比如 MyCAT、ShardingSphere,或者云厂商的读写分离服务。
解决主从延迟的关键技巧
1. 关键业务强制读主库
@contextmanager
def get_master_connection(self):
conn = pymysql.connect(**self.master)
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
# 查刚写入的数据,必须走主库
with db.get_master_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT * FROM orders WHERE id = %s", (last_insert_id,))
2. 减少从库压力
从库也要有合适的索引,否则每个查询都全表扫描,延迟只会越来越大。
3. 监控延迟
-- 定期执行,监控从库延迟
SELECT
ROUND(SYSDATE() - LAST_SEEN, 2) AS seconds_behind_master
FROM (
SELECT MAX(UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(RELAY_LOG_READ_POS) +
UNIX_TIMESTAMP(RELAY_LOG_WRITE_POS) - UNIX_TIMESTAMP(NOW())) AS LAST_SEEN
FROM mysql.slave_status
) t;
要是延迟超过 5 秒,要考虑从库配置升级,或者减少从库的查询压力。
崩溃预防:这些参数你得调
innodb_buffer_pool_size
这是最重要的参数。一般设为物理内存的 50%-70%。
-- 查看当前值
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 修改(需要重启)
SET GLOBAL innodb_buffer_pool_size = 4294967296; -- 4GB
怎么判断够不够用?
-- 看命中率
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
Innodb_buffer_pool_read_requests 是总请求数,Innodb_buffer_pool_reads 是物理读次数。要是命中率低于 99%,考虑加大 buffer pool。
max_connections
别设太大。每个连接都要占内存(约 256KB-2MB),连接太多内存直接爆。
-- 查看当前值
SHOW VARIABLES LIKE 'max_connections';
-- 推荐设置:并发数 × 1.5,最大不超过 500
SET GLOBAL max_connections = 300;
用连接池管理连接,别每次查询都新建连接。
innodb_log_file_size
日志文件越大,事务提交越快,但恢复时间也越长。
-- 推荐:总日志空间 = buffer_pool_size × 25%
-- 比如 buffer_pool 4GB,log 总空间 1GB,两个文件各 512MB
SET GLOBAL innodb_log_file_size = 536870912;
sync_binlog 和 innodb_flush_log_at_trx_commit
这两个决定数据安全性,也影响性能。
# 最安全(性能较差)
sync_binlog = 1
innodb_flush_log_at_trx-commit = 1
# 性能最好(可能丢 1 秒数据)
sync_binlog = 0
innodb_flush_log_at_trx-commit = 2
# 推荐折中方案
sync_binlog = 100
innodb_flush_log_at-trx-commit = 1
排查崩溃的实战流程
第一步:看错误日志
MySQL 的错误日志是 error.log,路径在 my.cnf 里配:
[mysqld]
log-error = /var/log/mysql/error.log
常见错误:
Got signal 11:段错误,通常是 bug 或者内存访问越界InnoDB: Unable to lock ./ibdata1:有其他进程占着文件,可能是另一个 MySQL 实例没关Out of memory:内存不够,检查innodb_buffer_pool_size和max_connections
第二步:看性能schema
-- 查看最慢的查询
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile
ORDER BY avg_timer_run;
-- 查看全表扫描的查询
SELECT * FROM sys.statements_with_full_table_scans
ORDER BY avg_timer_run;
第三步:看系统资源
# 内存
free -h
# CPU
top
# IO
iostat -x 1 5
# 网络连接
netstat -an | grep 3306 | wc -l
要是 IO wait 高,说明磁盘是瓶颈。考虑换 SSD,或者调整查询减少 IO。
第四步:看锁等待
-- 查看当前锁等待
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
要是有锁等待,找到对应的线程 ID,杀掉或者优化查询。
最后说几句掏心窝的话
MySQL 崩溃这事儿,真不是单一原因造成的。多半是:索引没优化 + 配置不合理 + 业务逻辑有缺陷,三者叠加,高并发一来,直接炸。
我见过太多项目,前期数据量少,跑得欢。一上并发,MySQL 扛不住。这时候加索引、调参数、上读写分离,一步步来。别想着一步到位,先把最痛的点解决了。
记住几个关键点:
- 索引要合理:别瞎加,复合索引要按查询模式设计
- 配置要对:buffer pool 够大,连接数别太多
- 读写分离要谨慎:主从延迟问题必须处理
- 监控要跟上:没监控就是盲人摸象
高并发下 MySQL 稳定运行,靠的不是运气,是扎实的基本功。今天这些经验,都是踩过的坑换来的。希望能帮到你,有问题随时聊。
加油,服务器不炸的那天,就在不远处。
