电商秒杀与外卖高峰期数据库崩溃案例分库分表加读写分离优化MySQL百万级并发连接数爆满慢查询解决方案


一场突如其来的”黑色星期五”

先给你讲个真事儿。

2023年双11,某头部电商平台搞了一场”限时秒杀”活动,一款原价1999元的旗舰手机,秒杀价只要999元。直播间里主播喊到”3、2、1,上链接”的时候,预计会有10万人同时点击“立即抢购”。

结果,后台数据库在0.5秒内直接崩了。

你能想象那个画面吗?用户点击支付,页面上转圈转了10秒,然后蹦出一行冷冰冰的字:“系统繁忙,请稍后重试。” 客服微信被打爆,技术群里有人发了一个跪着的表情包。

技术负责人老张第二天早上被叫到公司,一进门就看到监控大屏上一片血红——QPS从正常的2000飙到85万,MySQL的连接数直接拉满,CPU占用率100%,慢查询日志刷得磁盘写不动。

老张说,那是他职业生涯里最绝望的8小时。


外卖午高峰的”血色订单”

同一个城市,同一天的中午11:30,另一场灾难也在上演。

某外卖平台接入了5000家餐厅,午高峰时段每单平均3分钟完成一次下单-接单-配送的全流程。当天有活动,全场免配送费,结果平台在11:45收到了12万单同时涌入

数据库连接池直接爆了。用户端显示”订单提交中”,点了半天没反应,实际后台连接数已经达到上限,新的请求根本进不来。更糟糕的是,数据库主库写入压力过大,从库同步延迟高达30秒,订单状态查询返回的是30秒前的数据——用户看到”已下单”,实际上订单根本还没写进去。

这两个案例看起来不同,但根子上是同一个问题:MySQL扛不住高并发


MySQL高并发的”死亡三件套”

咱们先把问题拆开来看。电商平台秒杀和外卖午高峰,击垮MySQL的都是这三个家伙:

第一个:连接数爆满

MySQL默认的最大连接数是151。生产环境通常会调到1000-2000,但遇到秒杀场景,每个用户请求都要建立一次连接,10万人同时点,连接数瞬间打满。

-- 查看当前连接数配置
SHOW VARIABLES LIKE 'max_connections';
-- 输出:max_connections = 2000

-- 查看当前活跃连接数
SHOW STATUS LIKE 'Threads_connected';
-- 输出:Threads_connected = 2000 (已满)

-- 查看拒绝的连接数
SHOW STATUS LIKE 'Threads_created';
SHOW STATUS LIKE 'Connection_errors_max_connections';

当连接数满的时候,MySQL会直接拒绝新连接。这时候用户看到的不是”查询慢”,而是直接连接失败——这就是为什么很多系统在高峰期不是卡顿,而是直接崩。

第二个:慢查询堆积

连接数爆满只是表象,真正致命的是慢查询。高并发下,如果SQL写得有问题,一条慢查询能hold住一个连接好几秒,连接数瞬间就被撑满。

-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- long_query_time默认10秒,生产环境建议调到0.5秒甚至0.1秒

-- 查看慢查询日志是否开启
SHOW VARIABLES LIKE 'slow_query_log';

-- 查看当前正在执行的查询(揪出慢查询元凶)
SELECT * FROM information_schema.processlist 
WHERE COMMAND != 'Sleep' 
ORDER BY TIME DESC 
LIMIT 20;

第三个:磁盘I/O打满

慢查询多 → 连接不释放 → 更多请求堆积 → MySQL开始从磁盘读数据而不是从内存读 → 磁盘I/O飙升 → 响应时间进一步拉长 → 恶性循环。


分库分表:把一座山拆成多座小山丘

为什么需要分库分表

假设你的订单表有5亿条数据,每条查询都要扫这5亿行,哪怕有索引,InnoDB的B+树深度也已经到4-5层了。更可怕的是,所有写操作都集中在一张表上,单点的写入性能是瓶颈。

分库分表的核心思想就一句话:把一个大表拆成多个小表,把压力分散到多个节点上

水平分表:把大表切成小表

水平分表是按行拆分,同一个表结构,拆到多个物理表中。

-- 原表结构(单表5亿数据)
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    product_id INT NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status TINYINT DEFAULT 0,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 拆分成16张表(按user_id取模)
CREATE TABLE orders_00 (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    product_id INT NOT NULL,
    amount DECIMAL(10,2) NOT NULL,
    status TINYINT DEFAULT 0,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE orders_01 LIKE orders_00;
-- ... 重复到 orders_15

分片规则怎么定?

常见方案有:

  • 取模分片table_index = user_id % 16,均匀但扩容麻烦
  • 范围分片:按ID范围,比如1-1000万放表0,1000万-2000万放表1,扩容容易但容易数据倾斜
  • 一致性哈希:扩容时只影响少量数据,但实现复杂

分库:把压力分散到多台机器

光分表还不够,因为如果所有表都在同一台MySQL实例上,连接数和CPU压力还是集中在一个点。分库就是把不同的表放到不同的MySQL实例上。

┌─────────────────────────────────────────────┐
│                 应用层(ShardingSphere)       │
│                                              │
│   请求 → 路由 → 订单库0(16表)   订单库1(16表)  │
│             → 用户库0(8表)      用户库1(8表)  │
└─────────────────────────────────────────────┘

用ShardingSphere做分库分表

手写了分片规则容易出错,生产环境一般用成熟的分库分表中间件。ShardingSphere是目前国内用得最多的方案。

# sharding-jdbc 核心配置
dataSources:
  ds_0:
    url: jdbc:mysql://10.0.1.10:3306/order_db_0?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: xxx
    driverClassName: com.mysql.cj.jdbc.Driver
  ds_1:
    url: jdbc:mysql://10.0.1.11:3306/order_db_1?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: xxx
    driverClassName: com.mysql.cj.jdbc.Driver

shardingRule:
  tables:
    orders:
      actualDataNodes: ds_$->{0..1}.orders_$->{00..15}
      tableStrategy:
        standard:
          shardingColumn: user_id
          shardingAlgorithmName: orders-table-mod
      keyGenerator:
        type: SNOWFLAKE
        prop:
          worker-id: 1
  shardingAlgorithms:
    orders-table-mod:
      type: MOD
      props:
        sharding-count: 16

这样配置之后,应用层自动根据user_id路由到正确的库和表,代码完全无感知。

分库分表后的经典问题

问题1:跨分片查询怎么办?

比如要查”某个用户在所有分片中的订单”,这其实问题不大,因为user_id就是分片键,路由一次就能定位到具体哪张表。

但如果是按order_id查,或者按时间范围查,就麻烦了:

// 跨分片查询 - 广播查询(性能差,能避免就避免)
SELECT * FROM orders_00 WHERE create_time BETWEEN '2023-11-11' AND '2023-11-12'
UNION ALL
SELECT * FROM orders_01 WHERE create_time BETWEEN '2023-11-11' AND '2023-11-12'
-- ... 重复16次

// 正确做法:在业务层面拆分查询,先通过ES/Mongo做检索,再回MySQL拿详情

最佳实践:尽量避免跨分片查询。如果必须查,把查询压力转移到搜索引擎(Elasticsearch)或数据仓库上。

问题2:ID怎么生成?

分库分表后,数据库自增ID会重复。必须用分布式ID生成方案:

// 雪花算法(Snowflake)实现
public class SnowflakeIdGenerator {
    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨");
        }
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 4095; // 12位序列号,4096个
            if (sequence == 0) {
                timestamp = waitUntilNextMillis(timestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // 41位时间戳 + 10位机器ID + 12位序列号
        return ((timestamp - EPOCH) << 22) | (datacenterId << 17) | (workerId << 12) | sequence;
    }
}

41位时间戳可以用到2084年,10位机器ID支持1024个节点,12位序列号每毫秒每节点生成4096个ID。这个方案比数据库自增ID稳定多了。


读写分离:让主库专心写,从库专心读

为什么需要读写分离

秒杀场景有一个特点:读多写少。10万人在看商品详情、看库存、看评价,但真正下单的只有几百人。如果所有请求都打到主库,主库的连接数和CPU全被读请求占满了,写入请求反而进不来。

读写分离的做法很简单:主库负责写,从库负责读,通过中间件或代码层面做路由。

// 简单的读写分离路由(动态数据源方案)
@Configuration
public class DataSourceConfig {
    
    @Bean
    @Primary
    public DataSource dynamicDataSource(
        @Qualifier("masterDataSource") DataSource master,
        @Qualifier("slaveDataSource") DataSource slave) {
        
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER, master);
        targetDataSources.put(DataSourceType.SLAVE, slave);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(master);
        return dynamicDataSource;
    }
}

// 路由策略:通过AOP判断是读操作还是写操作
@Aspect
@Component
public class DataSourceAspect {
    
    @Before("@annotation(readOnly)")
    public void setReadOnly(JoinPoint point, ReadOnly readOnly) {
        DataSourceType.set(DataSourceType.SLAVE);
    }
    
    @Before("@annotation(writeOnly)")
    public void setWriteOnly(JoinPoint point, WriteOnly writeOnly) {
        DataSourceType.set(DataSourceType.MASTER);
    }
}

// 使用示例
@ReadOnly  // 走从库
public OrderVO getOrder(Long orderId) { ... }

@WriteOnly // 走主库
public void createOrder(OrderDTO dto) { ... }

主从同步延迟问题

读写分离最大的坑是主从同步延迟。主库写入后,从库可能需要几百毫秒甚至几秒才能同步到。这时候如果用户刚下单,立刻去查订单状态,可能查不到。

主库写入 → binlog → 从库 replay → 查询从库
            ↑
        延迟发生在这里

解决方案有几个:

方案1:写完后强制读主库

@WriteOnly
public OrderVO createOrderAndQuery(OrderDTO dto) {
    // 写入走主库
    Long orderId = orderMapper.insert(dto);
    
    // 刚写完,强制读主库避免读到旧数据
    DataSourceType.set(DataSourceType.MASTER);
    try {
        return orderMapper.selectById(orderId);
    } finally {
        DataSourceType.clear();
    }
}

方案2:设置读主库的白名单

对于核心业务(订单查询、库存扣减),直接走主库,其他业务走从库。

方案3:缩短同步延迟

用MHA或Orchestrator做主从监控,选延迟最小的从库来读:

-- 检查从库延迟
SHOW SLAVE STATUS\G
-- Slave_SQL_Running: Yes
-- Seconds_Behind_Master: 0  (理想状态)

MySQL连接数优化:从百万并发中活下来

回到老张的那个案例。连接数爆满是最直接的死因,那怎么优化?

第一步:调大max_connections(治标)

-- 临时调整(重启后失效)
SET GLOBAL max_connections = 5000;

-- 永久调整,修改my.cnf
[mysqld]
max_connections = 5000

但这条路走不通——连接数开太大,每个连接都要占用内存(大约256KB-1MB),5000个连接就是好几个GB的内存,而且MySQL处理过多空闲连接的开销也很大。

第二步:连接池(治本的关键)

应用层必须用连接池,不能让每个请求都新建连接。

// HikariCP 连接池配置(性能最好的连接池之一)
spring:
  datasource:
    hikari:
      maximum-pool-size: 50        # 每个数据源最大连接数
      minimum-idle: 10             # 最小空闲连接
      idle-timeout: 30000          # 空闲连接存活时间
      max-lifetime: 1800000        # 连接最大存活时间(30分钟)
      connection-timeout: 3000     # 获取连接超时时间
      leak-detection-threshold: 60000  # 检测连接泄漏

连接池的核心思路:预先建立好连接,复用它们,而不是每次请求都新建。50个连接池可以支撑远高于50的并发,因为大部分连接大部分时间是空闲等待的。

第三步:降低单个连接的持有时间

很多慢查询的问题是:查询本身很慢,连接被占用了很久才释放。这时候即使连接池再大也没用。

-- 开启 performance_schema 分析慢查询
SET GLOBAL performance_schema = ON;

-- 查看最耗时的SQL
SELECT DIGEST_TEXT, 
       SUM_TIMER_WAIT/1000000000000 AS total_sec,
       COUNT_STAR,
       AVG_TIMER_WAIT/1000000000000 AS avg_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY total_sec DESC
LIMIT 20;

第四步:MySQL 8.0 的改进

MySQL 8.0引入了很多连接优化的新特性:

# my.cnf 优化配置
[mysqld]
# 连接数
max_connections = 3000

# 线程缓存,避免频繁创建销毁线程
thread_cache_size = 100

# 禁用DNS解析,加快连接建立
skip-name-resolve

# 连接超时,快速释放空闲连接
wait_timeout = 60
interactive_timeout = 60

# 减少每个连接的内存开销
max_allowed_packet = 64M

第五步:用ProxySQL做连接复用

对于超高并发场景,可以在MySQL前面加一层ProxySQL,它能把很多小请求复用成少数量对MySQL的连接:

10000个客户端请求
    ↓
ProxySQL(连接池管理,最多维持200个到MySQL的连接)
    ↓
MySQL主库/从库(连接数可控)
# ProxySQL基本配置
INSERT INTO mysql-users (username, password, default_hostgroup)
VALUES ('app_user', 'password123', 10);

-- hostgroup 10 = 写主库,20 = 读从库
INSERT INTO mysql_server (hostname, port, hostgroup, weight)
VALUES ('10.0.1.10', 3306, 10, 1);

INSERT INTO mysql_server (hostname, port, hostgroup, weight)
VALUES ('10.0.1.11', 3306, 20, 1);

慢查询优化:从毫秒到微秒的优化之路

秒杀场景的典型慢查询

-- 问题SQL:秒杀时扣减库存(没有索引,全表扫描)
UPDATE products SET stock = stock - 1 
WHERE product_id = 10086 AND stock > 0;
-- 全表扫描 + 行锁,高峰期直接卡死

-- 问题SQL:查询订单(没有联合索引,回表严重)
SELECT * FROM orders 
WHERE user_id = 12345 AND status = 1 
ORDER BY create_time DESC 
LIMIT 20;
-- 单索引效率低,回表多次

-- 问题SQL:跨表关联查询
SELECT o.*, p.name, p.image 
FROM orders o 
LEFT JOIN products p ON o.product_id = p.id 
WHERE o.user_id = 12345;
-- 大表JOIN,没有覆盖索引

优化方案一:加对的正确索引

-- 优化扣减库存:确保product_id有索引
ALTER TABLE products ADD INDEX idx_product_id (product_id);

-- 优化订单查询:用联合索引避免回表
ALTER TABLE orders ADD INDEX idx_user_status_time (user_id, status, create_time);

-- 覆盖索引优化:只查需要的字段,减少回表
SELECT id, amount, status, create_time 
FROM orders 
WHERE user_id = 12345 AND status = 1 
ORDER BY create_time DESC 
LIMIT 20;
-- 这个查询可以直接从索引树上拿到所有数据,不需要回表

优化方案二:用EXPLAIN分析执行计划

EXPLAIN FORMAT=JSON
SELECT o.id, o.amount, o.status 
FROM orders o 
WHERE o.user_id = 12345 AND o.status = 1 
ORDER BY o.create_time DESC 
LIMIT 20;

输出结果里重点关注:

{
  "query_block": {
    "select_id": 1,
    "ordering_operation": {
      "using_filesort": false,  -- 必须为false,否则说明用了文件排序
      "sorting_environment": {
        "sort_buffer_size": "262144",
        "sort_mode": "<sort_buffer, additional_rows>"
      }
    },
    "table": {
      "table_name": "orders",
      "access_type": "ref",  -- 必须是ref或range,不能是ALL
      "possible_keys": ["idx_user_status_time"],
      "key": "idx_user_status_time",
      "used_key_parts": ["user_id", "status"],
      "rows": 152,  -- 扫描的行数,越少越好
      "filtered": 100.00
    }
  }
}

常见EXPLAIN问题诊断

access_type 含义 是否OK
system 表只有一行 ✅ 完美
const 按主键/唯一索引查 ✅ 完美
ref 按普通索引查 ✅ 可以
range 索引范围扫描 ✅ 可以接受
index 全索引扫描 ⚠️ 有点慢
ALL 全表扫描 ❌ 必须优化

优化方案三:秒杀场景的特殊优化

秒杀和正常业务不一样,不能用常规的”查数据库→扣库存→写订单”流程,否则数据库根本扛不住。

经典秒杀架构

用户请求
    ↓
┌────────────────────────────────────────────┐
│  第1层:Nginx限流(每秒最多1000请求)          │
│  limit_req zone=seckill burst=500 nodelay;   │
└────────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────────┐
│  第2层:Redis预扣库存                         │
│  1. Lua脚本原子扣减:                         │
│     redis.call('DECRBY', key, count)         │
│  2. 库存不足 → 直接返回"已售罄"               │
│  3. 库存充足 → 进入消息队列                   │
└────────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────────┐
│  第3层:消息队列削峰(Kafka/RocketMQ)         │
│  把突发请求变成匀速消费                       │
└────────────────────────────────────────────┘
    ↓
┌────────────────────────────────────────────┐
│  第4层:MySQL异步落库                         │
│  消费者匀速写入数据库                         │
└────────────────────────────────────────────┘
// Redis Lua脚本扣减库存(原子操作)
String script = 
    "local key = KEYS[1] " +
    "local count = tonumber(ARGV[1]) " +
    "local stock = tonumber(redis.call('get', key)) " +
    "if stock < count then " +
    "    return 0 " +
    "end " +
    "redis.call('decrby', key, count) " +
    "return 1";

// 调用
Long result = redisTemplate.execute(
    new DefaultRedisScript<>(script, Long.class),
    Collections.singletonList("seckill:stock:" + productId),
    String.valueOf(quantity)
);

if (result == 0) {
    return Result.fail("库存不足");
}

// 扣减成功,发送到消息队列
kafkaTemplate.send("seckill-order", orderDTO);

这样99%的请求在Redis层面就被拦截了,只有1%真正到MySQL。数据库的连接数和QPS直接下降两个数量级。

优化方案四:分页查询优化

外卖平台查订单列表,如果用的是传统的LIMIT 100000, 20,MySQL需要扫描10万行再扔掉99980行,非常慢。

-- 慢查询:深分页
SELECT * FROM orders 
WHERE user_id = 12345 
ORDER BY create_time DESC 
LIMIT 100000, 20;
-- 扫描10万行,性能极差

-- 优化方案1:延迟关联
SELECT o.* FROM orders o
INNER JOIN (
    SELECT id FROM orders 
    WHERE user_id = 12345 
    ORDER BY create_time DESC 
    LIMIT 100000, 20
) AS tmp ON o.id = tmp.id;
-- 子查询只扫描索引,主查询只回表20行

-- 优化方案2:游标分页(推荐)
SELECT * FROM orders 
WHERE user_id = 12345 
AND create_time < '2023-11-10 12:30:00'  -- 上一页最后一条的时间
ORDER BY create_time DESC 
LIMIT 20;
-- 用where条件替代offset,性能稳定

完整架构升级方案

把上面所有方案拼在一起,就是一个完整的秒杀/高并发数据库优化架构:

                          ┌──────────────┐
                          │   Nginx      │
                          │  限流+缓存    │
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │   Redis      │
                          │  缓存+预扣库存  │
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │   MQ         │
                          │  削峰+异步    │
                          └──────┬───────┘
                                 │
                    ┌────────────┼────────────┐
                    │            │            │
              ┌─────▼─────┐ ┌───▼────┐ ┌────▼─────┐
              │  Sharding │ │  读从库 │ │  写主库   │
              │  Sphere   │ │(3台)    │ │ (1台)    │
              └─────┬─────┘ └───┬────┘ └────┬─────┘
                    │           │            │
              ┌─────▼─────┐ ┌───▼────┐ ┌────▼─────┐
              │  16库×16   │ │  同步   │ │  binlog  │
              │  表=256表  │ │  延迟<1s│ │  复制     │
              └───────────┘ └────────┘ └──────────┘

关键配置清单

# Spring Boot + MySQL 生产环境配置
spring:
  datasource:
    hikari:
      maximum-pool-size: 50        # 每个分片50个连接
      minimum-idle: 10
      connection-timeout: 3000
      idle-timeout: 30000
      max-lifetime: 1800000
    type: com.zaxxer.hikari.HikariDataSource

# ShardingSphere配置
sharding:
  sharding-algorithms:
    database-mod:
      type: MOD
      props:
        sharding-count: 16
    table-mod:
      type: MOD
      props:
        sharding-count: 16
  binding-tables: 
    - orders,order_detail  # 绑定表避免跨库JOIN
  broadcast-tables: 
    - sys_config  # 广播表,所有库都有一份
-- MySQL生产环境my.cnf关键参数
[mysqld]
max_connections = 3000
thread_cache_size = 200
table_open_cache = 16384
innodb_buffer_pool_size = 8G          # 尽量占物理内存的70%
innodb_log_file_size = 512M
innodb_flush_log_at_trx_commit = 2    # 性能优先时设为2
innodb_io_capacity = 2000
innodb_flush_method = O_DIRECT
sync_binlog = 1
binlog_format = ROW
binlog_cache_size = 4M
max_binlog_cache_size = 512M
skip-name-resolve

从老张的故事说起

说回开头的老张。双11那天之后,他们团队用了三周时间做了全面的数据库架构升级:

  1. 分库分表:订单表按user_id分16库×16表,共256张表
  2. 读写分离:1主3从,查询走从库,写入走主库
  3. 连接池优化:HikariCP配置调优,单实例连接数控制在50
  4. Redis预扣库存:秒杀场景99%的请求在Redis层就被拦住了
  5. MQ削峰:Kafka把突发流量变成匀速写入
  6. 慢查询治理:上线前用pt-query-digest分析了全量慢查询日志,优化了TOP20慢SQL

第二次双11,QPS飙到120万的时候,老张看着监控大屏,连接数稳定在800(上限3000),MySQL CPU占用65%,慢查询每秒不到10条。

他没发跪着的表情包,发了个”稳了”。


总结一下

高并发下MySQL崩溃,从来不是单一原因造成的。它是一个链条:

坏SQL → 慢查询 → 连接占用时间长 → 连接数打满 → 新请求被拒 → 系统崩溃

要打破这个链条,从任何一个环节入手都可以:

  • 治标:调大连接数、优化慢查询、加索引
  • 治本:分库分表分散压力、读写分离减轻主库负担、Redis+MQ在数据库前面挡掉大部分请求

分库分表+读写分离是架构层面的硬解法,但前提是每一层的SQL都要写得规范。再大的集群,也救不了满篇SELECT *和没有索引的表。

优化数据库,本质上是优化对数据的访问方式。理解数据的流向,找到瓶颈在哪一环,然后在那一环上分流、减速、或者干脆不让它到达——这就是解决高并发数据库问题的核心思路。

如果你正在面临类似的数据库崩溃问题,不用慌。先从慢查询日志入手,找到最耗资源的那几条SQL,一条一条优化,往往就能把系统的承载力提升一个数量级。剩下的,才是架构层面的活儿。