服务器崩溃?MySQL高并发处理与数据库优化实战全攻略
崩溃现场:那个可怕的凌晨三点
你还记得那种感觉吗?凌晨三点,手机疯狂震动,报警短信一条接一条。你迷迷糊糊点开监控面板,好家伙——MySQL连接数直接飙到上限,CPU占用率99%,整个系统像一头被抽干了力气的老黄牛,瘫在原地起不来。
别慌,这种事几乎每个做后端的人都会经历。今天我们就来一场深度实战,把这堆乱七八糟的问题一个个拆解清楚。
一、连接数限制:服务器崩溃的第一道防线
先搞清楚,连接数是什么?
想象一下你家小区的门卫室。MySQL就像一个小区,每个访问数据库的请求就是一个来访者,而连接数就是同时能进入小区的总人数。门卫室容量有限,来的人太多,新来的人就得在外面干等着,或者被直接拒之门外。
MySQL默认的max_connections通常是151,这在小型项目里够用了。但一旦你的网站开始有流量,这个值就像个瓶颈,直接把你卡死。
实战排查:你的连接数现在是多少?
-- 查看当前连接状态
SHOW STATUS LIKE 'Threads_connected';
-- 查看最大连接数配置
SHOW VARIABLES LIKE 'max_connections';
-- 查看连接详情(找出是谁在占用连接)
SHOW PROCESSLIST;
-- 更详细的连接统计
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Connections';
SHOW GLOBAL STATUS LIKE 'Threads_cached';
解读这些数字:
Threads_connected:当前活跃连接数,如果接近Max_used_connections,说明你已经在风暴边缘了Connections:历史总连接数,如果这个数字增长极快,说明连接泄漏或者频繁创建连接Threads_cached:缓存中的空闲连接,太低意味着MySQL在频繁创建和销毁连接
连接数爆满的典型症状
Can't connect to MySQL server (1040 "Too many connections")
看到这条报错,就意味着你的连接池已经被撑爆了。
优化方案一:合理调整连接数
-- 设置最大连接数(需要重启MySQL或动态修改)
SET GLOBAL max_connections = 500;
-- 在my.cnf中永久配置
[mysqld]
max_connections = 1000
但等等,调大连接数不是万能药。连接数太多会导致:
- 内存占用飙升(每个连接都要分配Buffer)
- 上下文切换开销增加
- 磁盘IO竞争加剧
优化方案二:连接池的正确使用姿势
这是大多数开发者最容易踩坑的地方。看看这段反模式代码:
# 错误示范:每次请求都新建和关闭连接
def get_user(user_id):
conn = pymysql.connect(
host='localhost',
user='root',
password='password',
database='mydb'
)
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchone()
conn.close() # 频繁创建销毁连接,性能极差
return result
正确示范:使用连接池
import pymysql
from DBUtils.PooledDB import PooledDB
# 创建连接池(项目启动时初始化一次)
pool = PooledDB(
creator=pymysql,
maxconnections=20, # 连接池最大连接数
mincached=5, # 启动时初始化的空闲连接数
maxcached=10, # 空闲连接数上限
maxusage=100, # 单个连接最大复用次数
blocking=True, # 连接池满时是否阻塞等待
host='localhost',
user='root',
password='password',
database='mydb',
charset='utf8mb4'
)
def get_user(user_id):
# 从连接池获取连接,而不是新建
conn = pool.connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchone()
conn.close() # 这里close()是把连接还给池子,不是真的关闭
return result
// Java中使用HikariCP连接池(Spring Boot标配)
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("root");
config.setPassword("password");
// 关键配置
config.setMaximumPoolSize(20); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接
config.setIdleTimeout(30000); // 空闲超时30秒
config.setMaxLifetime(1800000); // 连接最长存活30分钟
config.setConnectionTimeout(30000); // 获取连接超时30秒
return new HikariDataSource(config);
}
}
连接泄漏检测
连接泄漏是隐形的杀手。一个连接没释放,可能当时看不出来,但几个小时之后,连接池被耗尽,系统直接瘫痪。
-- 查看是否有长时间运行的连接
SELECT
ID,
USER,
HOST,
DB,
COMMAND,
TIME AS 运行时长秒,
STATE,
LEFT(INFO, 100) AS 当前执行的SQL
FROM information_schema.PROCESSLIST
WHERE COMMAND != 'Sleep'
ORDER BY TIME DESC;
在代码层面加入超时检测:
import pymysql.cursors
def safe_query(sql, params=None, timeout=5):
"""带超时保护的查询,防止连接长时间占用"""
conn = pool.connection()
try:
cursor = conn.cursor()
# 设置会话级超时
cursor.execute(f"SET SESSION lock_wait_timeout={timeout}")
cursor.execute(sql, params or ())
result = cursor.fetchall()
return result
except pymysql.err.OperationalError as e:
# 连接异常时标记为坏连接,让池子丢弃它
conn.rollback()
raise
finally:
try:
conn.close()
except:
pass
二、慢查询优化:数据库的隐形杀手
什么是慢查询?
慢查询就是那些执行时间超过阈值的SQL。它们就像高速公路上的堵车,一旦有几辆车(查询)堵在那里,后面的全得跟着等。
-- 查看慢查询日志是否开启
SHOW VARIABLES LIKE 'slow_query%';
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒的查询记入日志
在my.cnf中配置:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1 # 记录没有使用索引的查询
慢查询分析的黄金工具:EXPLAIN
每次写SQL之前,先跑一遍EXPLAIN,就像司机出发前检查车况一样自然。
-- 最核心的分析语句
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
-- 带成本信息的EXPLAIN(MySQL 8.0+)
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE user_id = 10086 AND status = 1;
输出结果解读(逐字段拆解):
| 字段 | 含义 | 理想状态 |
|---|---|---|
id |
查询ID | 越大越先执行 |
select_type |
查询类型 | 简单查询优先 |
table |
涉及的表 | - |
partitions |
匹配的分区 | 越少越好 |
type |
连接类型 | const > ref > range > index > ALL |
possible_keys |
可能的索引 | 有值最佳 |
key |
实际使用的索引 | 有值且是预期索引 |
key_len |
索引使用长度 | 越短越好(但要看是否用全) |
ref |
索引比较的列 | 常量和列名最佳 |
rows |
扫描行数 | 越少越好 |
filtered |
过滤比例 | 越大越好 |
Extra |
额外信息 | 避免Using filesort/Using temporary |
典型慢查询案例与优化实战
案例一:最左前缀失效
假设你有一张用户订单表:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL,
amount DECIMAL(10,2) NOT NULL,
INDEX idx_user_status (user_id, status, create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
很多人会犯这种错误:
-- ❌ 危险查询:跳过了最左列user_id
SELECT * FROM orders WHERE status = 1 AND create_time > '2024-01-01';
-- ✅ 正确查询:遵循最左前缀原则
SELECT * FROM orders WHERE user_id = 10086 AND status = 1 AND create_time > '2024-01-01';
-- 如果确实需要按status和create_time查询,单独建索引
ALTER TABLE orders ADD INDEX idx_status_time (status, create_time);
运行EXPLAIN对比:
-- 优化前
EXPLAIN SELECT * FROM orders WHERE status = 1 AND create_time > '2024-01-01';
-- type=ALL, rows=1000000 ← 全表扫描,灾难
-- 优化后
EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = 1 AND create_time > '2024-01-01';
-- type=ref, rows=100 ← 走索引,瞬间完成
案例二:隐式类型转换
-- 假设user_id是BIGINT类型,但查询时传了字符串
SELECT * FROM orders WHERE user_id = '10086';
这会导致索引失效!MySQL会在查询前把字符串转成数字,这个转换发生在每一行上,索引就没法用了。
-- ✅ 正确写法:类型匹配
SELECT * FROM orders WHERE user_id = 10086;
案例三:SELECT * 的陷阱
-- ❌ 大表上用SELECT *
SELECT * FROM orders WHERE user_id = 10086;
-- ✅ 只取需要的字段
SELECT id, status, create_time, amount FROM orders WHERE user_id = 10086;
这不仅减少了网络传输,还可能触发覆盖索引优化——索引里已经有所有需要的数据,根本不用回表查整行。
-- 验证覆盖索引:Extra显示"Using index"说明命中了
EXPLAIN SELECT id, status, create_time FROM orders WHERE user_id = 10086;
-- 可能看到: Extra = Using index
案例四:分页查询的深度陷阱
-- ❌ 深度分页,数据量一大就爆炸
SELECT * FROM orders WHERE user_id = 10086 ORDER BY create_time DESC LIMIT 100000, 20;
这种写法每往后翻一页,MySQL就要扫描100020行然后丢弃前面的100000行,越往后越慢。
-- ✅ 方案一:子查询优化(先定位ID,再回表)
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 100000, 20
) AS tmp ON o.id = tmp.id;
-- ✅ 方案二:游标分页(记录上一页最大ID)
SELECT * FROM orders
WHERE user_id = 10086 AND create_time < '2024-03-15 10:30:00'
ORDER BY create_time DESC
LIMIT 20;
-- ✅ 方案三:限制最大页码
-- 业务上限制最多翻1000页,超出提示用户
索引优化的核心原则
- 区分度高的列放前面:性别字段的区分度只有2,放索引最前面是大忌
- 能走范围查询的列放索引最后面:
WHERE a=1 AND b>100 AND c='x',c应该放最前 - 短索引优先:能确定唯一性的前缀就够,不用建整个字段索引
- 避免过度索引:每个索引都有写入开销,表写入频繁时索引不是越多越好
-- 查看表的索引使用情况
SELECT
TABLE_NAME,
INDEX_NAME,
SEQ_IN_INDEX,
COLUMN_NAME,
CARDINALITY -- 区分度:值越大越好
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME = 'orders';
-- 查看索引效率:区分度 / 总行数
SELECT
TABLE_NAME,
INDEX_NAME,
COUNT(DISTINCT COLUMN_NAME) / COUNT(*) AS 区分度比例
FROM orders
GROUP BY TABLE_NAME, INDEX_NAME;
三、分库分表:当单表扛不住百万级数据
为什么要分库分表?
一张表数据量到了什么程度开始有问题?这是一个没有标准答案的问题,但我们可以这样理解:
| 数据量级 | 单表处理建议 |
|---|---|
| < 100万 | 普通单表,索引优化够用了 |
| 100万 - 1000万 | 考虑分区表或开始规划分表 |
| 1000万 - 1亿 | 必须分表,单表查询开始变慢 |
| > 1亿 | 分库分表,可能还需要读写分离 |
MySQL单表的理想行数通常认为在500万到1000万之间。超过这个范围,索引效率会明显下降,维护成本也会上升。
分表策略:怎么分?往哪里分?
横向分表(水平拆分):把一张大表按规则拆成多张小表。
-- 原始大表
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2),
status TINYINT,
create_time DATETIME,
INDEX idx_user_id (user_id)
) ENGINE=InnoDB;
-- 分成16张表(按user_id取模)
CREATE TABLE orders_00 (LIKE orders);
CREATE TABLE orders_01 (LIKE orders);
-- ... 到 orders_15
路由规则:
def get_table_name(user_id, shard_count=16):
"""根据user_id确定数据落在哪张表"""
table_index = user_id % shard_count
return f"orders_{table_index:02d}"
# 写入时
table_name = get_table_name(user_id=10086)
# table_name = "orders_06"
sql = f"INSERT INTO {table_name} (user_id, amount, status, create_time) VALUES (%s, %s, %s, %s)"
注意点:分表之后查询变了!
-- ❌ 分表后不能再这样写(会提示表不存在)
SELECT * FROM orders WHERE user_id = 10086;
-- ✅ 需要先路由到正确的表
SELECT * FROM orders_06 WHERE user_id = 10086;
如果是跨表查询(比如查所有订单),需要聚合多张表的结果:
def get_all_orders():
"""跨表查询示例"""
all_results = []
for i in range(16):
table_name = f"orders_{i:02d}"
sql = f"SELECT * FROM {table_name}"
result = db.execute(sql)
all_results.extend(result)
return all_results
垂直分表:把大字段拆出去
有些表有TEXT或LONGTEXT类型的大字段,这些字段查询频率很低,但会拖慢整个表。
-- 原表:order_detail包含大文本字段
CREATE TABLE order_detail (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
remark TEXT, -- 大字段,很少用到
product_desc LONGTEXT, -- 超大字段
create_time DATETIME
);
-- 垂直拆分:主表和扩展表分离
CREATE TABLE order_main (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME,
INDEX idx_user_id (user_id)
) ENGINE=InnoDB;
CREATE TABLE order_extend (
order_id BIGINT PRIMARY KEY,
remark TEXT,
product_desc LONGTEXT,
INDEX idx_order_id (order_id)
) ENGINE=InnoDB;
这样,日常查询只访问order_main,速度快很多。需要详情时再JOIN order_extend。
分库策略:从分表到分库
当单库也扛不住时,就升级到分库。每个库可以部署在不同的服务器上。
数据库集群架构:
┌─────────────┐ ┌─────────────┐
│ MySQL-库A │ │ MySQL-库B │
│ orders_00 │ │ orders_16 │
│ orders_01 │ │ orders_17 │
│ orders_02 │ │ orders_18 │
│ orders_03 │ │ orders_19 │
└─────────────┘ └─────────────┘
│ │
└────────┬───────────┘
▼
┌─────────────┐
│ 路由中间件 │
│ (Sharding) │
└─────────────┘
路由规则示例:
# 分库路由:根据user_id的高位决定库,低位决定表
def get_shard_info(user_id, db_count=4, table_count=16):
"""返回数据库和表的索引"""
db_index = (user_id // table_count) % db_count
table_index = user_id % table_count
return db_index, table_index
# 示例:user_id=10086, 4库16表
db_index, table_index = get_shard_info(10086, 4, 16)
# db_index=0, table_index=10
# → 从"库0"的"orders_10"表查询
分库分表的常见坑
坑一:跨库跨表查询困难
-- 分表前:一条SQL搞定的事
SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.id = 12345;
-- 分表后:可能需要两次查询,或者用ES做关联
# 第一次:确定orders落在哪张表
table_name = get_table_name(user_id=xxx)
order = SELECT * FROM {table_name} WHERE id = 12345
# 第二次:查用户信息
user = SELECT * FROM users WHERE id = {order.user_id}
坑二:分布式ID生成
分表后,自增ID在各张表里各自递增,合并后就不连续了。如果需要全局唯一ID,用雪花算法:
import time
import random
class SnowflakeID:
"""简化版雪花算法,生成全局唯一ID"""
def __init__(self, worker_id=1):
self.worker_id = worker_id
self.sequence = 0
self.last_timestamp = -1
def generate(self):
timestamp = self._current_millis()
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & 0xFFF
if self.sequence == 0:
timestamp = self._wait_until_next_ms()
else:
self.sequence = 0
self.last_timestamp = timestamp
# 拼接:时间戳(41位) + 工作机ID(10位) + 序列号(12位)
return ((timestamp & 0x1FFFFFFFFFFFF) << 22) | \
((self.worker_id & 0x3FF) << 12) | \
(self.sequence & 0xFFF)
def _current_millis(self):
return int(time.time() * 1000)
def _wait_until_next_ms(self):
while self._current_millis() <= self.last_timestamp:
pass
return self.last_timestamp
# 使用
id_generator = SnowflakeID(worker_id=1)
new_id = id_generator.generate()
坑三:运维复杂度暴增
分库分表之后,备份、监控、扩容都变得更复杂了。建议在分表前评估一下业务增长曲线,别为了未来的假设过度设计。
中间件方案:不用自己造轮子
如果自己实现分库分表太麻烦,业界已经有成熟的中间件:
ShardingSphere(推荐)
# ShardingSphere配置示例(YAML格式)
dataSources:
ds_0:
url: jdbc:mysql://localhost:3306/db_order_0
username: root
password: password
ds_1:
url: jdbc:mysql://localhost:3306/db_order_1
username: root
password: password
rules:
- !SHARDING
tables:
orders:
actualDataNodes: ds_$->{0..1}.orders_$->{0..3}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order-table-sharding
keyGenerateStrategy:
column: id
keyGeneratorName: snowflake
shardingAlgorithms:
order-table-sharding:
type: MOD
props:
sharding-count: 4
keyGenerators:
snowflake:
type: SNOWFLAKE
四、读写分离:让数据库喘口气
当数据量还不够大到需要分库分表时,读写分离是最直接的优化手段。
┌──────────┐
│ 写入 │
└────┬─────┘
▼
┌──────────┐
│ 主库Master│ ← 负责写操作
└────┬─────┘
│ 异步复制
▼
┌────────┴────────┐
▼ ▼
┌────────┐ ┌────────┐
│ 从库S1 │ │ 从库S2 │
└────────┘ └────────┘
▲ ▲
└────────┬────────┘
▼
┌──────────┐
│ 读取 │
└──────────┘
// Spring Boot中配置主从数据源
@Configuration
public class DataSourceConfig {
@Bean("masterDataSource")
@Primary
public DataSource masterDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://master:3306/mydb");
config.setUsername("root");
config.setPassword("password");
config.setMaximumPoolSize(10);
return new HikariDataSource(config);
}
@Bean("slaveDataSource")
public DataSource slaveDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://slave:3306/mydb");
config.setUsername("root");
config.setPassword("password");
config.setMaximumPoolSize(20); // 读多写少,从库连接可以更多
return new HikariDataSource(config);
}
@Bean
public DataSourceRouting dataSourceRouting(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER, master);
targetDataSources.put(DataSourceType.SLAVE, slave);
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(master);
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
// 动态数据源路由
public class DataSourceRouting extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
// 使用AOP自动切换数据源
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void setReadDataSource(ReadOnly readOnly) {
DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
}
@Before("@annotation(write)")
public void setWriteDataSource(Write write) {
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
}
}
五、监控与预警:别让问题静默发生
最好的优化是预防。建立完善的监控体系,让问题在爆发前就被发现。
-- 关键监控指标SQL
-- 1. 当前连接数和利用率
SELECT
VARIABLE_VALUE AS 当前连接数,
(VARIABLE_VALUE / @@max_connections * 100) AS 连接使用率百分比
FROM information_schema.GLOBAL_VARIABLES
WHERE VARIABLE_NAME IN ('Threads_connected', 'max_connections');
-- 2. 慢查询数量趋势
SELECT
DATE_FORMAT(event_time, '%Y-%m-%d %H:00') AS 小时,
COUNT(*) AS 慢查询数
FROM mysql.slow_log
WHERE event_time > NOW() - INTERVAL 24 HOUR
GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d %H:00')
ORDER BY 小时 DESC;
-- 3. 表大小和索引大小
SELECT
table_name AS 表名,
ROUND(data_length / 1024 / 1024, 2) AS 数据大小MB,
ROUND(index_length / 1024 / 1024, 2) AS 索引大小MB,
table_rows AS 行数
FROM information_schema.TABLES
WHERE table_schema = 'mydb'
ORDER BY (data_length + index_length) DESC;
-- 4. 缓冲池命中率(越接近100%越好)
SELECT
(1 - (Pages_dirty / (Pages_free + Pages_free))) * 100 AS buffer_hit_rate
FROM information_schema.GLOBAL_STATUS;
# 简单的监控脚本(可以用cron定时执行)
import pymysql
import requests
import os
def check_mysql_health():
conn = pymysql.connect(host='localhost', user='root', password='password', db='mysql')
cursor = conn.cursor()
# 检查连接数
cursor.execute("SHOW STATUS LIKE 'Threads_connected'")
connected = cursor.fetchone()[1]
max_conn = int(os.getenv('MAX_CONNECTIONS', 500))
if connected > max_conn * 0.8:
send_alert(f"连接数过高: {connected}/{max_conn}")
# 检查慢查询
cursor.execute("SHOW GLOBAL STATUS LIKE 'Slow_queries'")
slow_count = cursor.fetchone()[1]
if slow_count > 100:
send_alert(f"慢查询数量过多: {slow_count}")
conn.close()
def send_alert(message):
"""发送告警(钉钉/企业微信/邮件等)"""
webhook = os.getenv('ALERT_WEBHOOK')
if webhook:
requests.post(webhook, json={"msg_type": "text", "text": {"content": message}})
# 每分钟执行一次
import schedule
schedule.every().minute.do(check_mysql_health)
while True:
schedule.run_pending()
time.sleep(1)
六、实战总结:从崩溃到稳定的完整路径
当服务器因为MySQL崩溃时,你应该按这个顺序来排查和解决:
第一步:止血(立即)
- 查看当前连接数和进程列表,找出异常连接
- 如果有连接泄漏,杀掉可疑的长连接
- 临时调大
max_connections和wait_timeout
第二步:诊断(短期)
- 开启慢查询日志,抓出慢SQL
- 用EXPLAIN分析每一个慢查询
- 检查索引是否命中,是否存在全表扫描
第三步:优化(中期)
- 给慢SQL加上合适的索引
- 重构低效查询(避免SELECT *、隐式类型转换等)
- 接入连接池,复用数据库连接
第四步:扩容(长期)
- 评估数据增长趋势,规划分库分表
- 部署读写分离,分担主库压力
- 建立完善的监控告警体系
给小读者的话
数据库优化就像整理你的房间。刚开始东西少,随手一放还能找到。后来东西越来越多,找一样东西要翻半天,房间也越来越乱。
优化数据库也是一样:
- 连接池就像把常用的东西放在手边,不用每次都去柜子最深处翻
- 索引就像给书贴上标签,找书的时候直接看标签,不用一本本翻
- 分库分表就像买了更大的书架,把东西分类放,虽然整理麻烦点,但以后找起来快多了
- 读写分离就像请了个帮手,你专心收拾(写),帮手帮你整理找东西(读)
最重要的是:不要等房间乱到没法进了才想起来整理。平时多留意,发现问题马上处理,这样数据库才能一直保持健康状态。