引言:理解高并发场景下的数据库挑战

在现代互联网应用中,高并发场景已经成为常态。无论是电商秒杀、社交平台热点事件,还是金融交易系统,MySQL数据库都面临着每秒数千甚至数万请求的严峻考验。当并发请求超过数据库的处理能力时,系统响应时间急剧增加,甚至导致服务完全不可用,这就是我们常说的”系统崩溃”。

高并发对MySQL的冲击主要体现在三个方面:连接资源耗尽CPU和内存过载磁盘I/O瓶颈。想象一下,如果每个数据库连接都需要消耗内存和CPU资源,当10000个并发请求同时到达时,数据库服务器可能瞬间被压垮。更糟糕的是,不当的查询可能导致锁竞争,使得所有请求都在等待,形成雪崩效应。

本文将从架构设计MySQL配置优化SQL与索引优化缓存策略读写分离数据库拆分等多个维度,系统性地讲解如何优化MySQL以应对海量请求。我们会结合具体的配置参数、代码示例和实际案例,帮助你构建一个高可用、高性能的数据库系统。

一、架构层面的优化策略

1.1 读写分离架构

读写分离是应对高并发最有效的架构手段之一。其核心思想是将读请求写请求分离到不同的数据库实例,利用多个从库来分担读压力。

实现原理

  • 主库(Master):处理所有写操作(INSERT/UPDATE/DELETE)和部分读操作
  • 从库(Slave):处理所有读操作(SELECT),通过主从复制同步数据

代码示例:使用ShardingSphere实现读写分离

// ShardingSphere配置示例(application.yml)
spring:
  shardingsphere:
    datasource:
      names: master,slave0,slave1
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://master-host:3306/db
        username: root
        password: password
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave0-host:3306/db
        username: root
        password: password
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave1-host:3306/db
        username: root
        password: password
    
    rules:
      readwrite-splitting:
        data-sources:
          ds0:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave0,slave1
              load-balancer-name: round_robin
        load-balancers:
          round_robin:
            type: ROUND_ROBIN

配置要点

  1. 主从复制延迟:需要监控复制延迟,避免读取到过期数据
  2. 主从数据一致性:对于强一致性要求的场景,可以强制读主库
  3. 负载均衡:合理选择轮询、随机或基于权重的负载均衡策略

1.2 数据库垂直拆分

当单表字段过多(如超过50个字段)或业务逻辑清晰时,可以考虑垂直拆分。将大表按业务模块拆分成多个小表,减少单行数据大小,提高查询效率。

案例:用户表拆分

-- 拆分前:用户表包含所有字段
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    password VARCHAR(100),
    email VARCHAR(100),
    phone VARCHAR(20),
    address TEXT,
    avatar TEXT,
    profile TEXT,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    -- ... 更多字段
    INDEX idx_username (username)
);

-- 拆分后:基础信息表 + 详情表
CREATE TABLE user_base (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(100),
    email VARCHAR(100),
    phone VARCHAR(20),
    created_at TIMESTAMP,
    INDEX idx_username (username)
);

CREATE TABLE user_profile (
    user_id BIGINT PRIMARY KEY,
    address TEXT,
    avatar TEXT,
    profile TEXT,
    updated_at TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES user_base(id)
);

优势

  • 热点数据(如username/password)查询更快
  • 非频繁访问的字段(如profile)不占用主表内存
  • 更灵活的缓存策略

1.3 数据库水平拆分(分库分表)

当单表数据量超过1000万或单库连接数不足时,需要水平拆分。将数据按某种规则(如用户ID哈希)分布到多个物理表中。

分表策略示例:按用户ID取模分表

-- 分表规则:user_id % 4,共4张表
CREATE TABLE user_0 (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    username VARCHAR(50),
    -- 其他字段
    INDEX idx_user_id (user_id)
);

CREATE TABLE user_1 (
    id BIGINT PRIMARY KEY,
    user_id BIGINT,
    username VARCHAR(50),
    -- 其他字段
    INDEX idx_user_id (user_id)
);

-- ... user_2, user_3

分表路由代码实现

public class ShardingUtil {
    private static final int TABLE_COUNT = 4;
    
    // 根据user_id计算表名
    public static String getTableName(Long userId) {
        int index = (int) (userId % TABLE_COUNT);
        return "user_" + index;
    }
    
    // 动态SQL示例(MyBatis)
    // <select id="getUserById" resultType="User">
    //   SELECT * FROM ${tableName} WHERE user_id = #{userId}
    // </select>
    
    // 使用示例
    public User getUserById(Long userId) {
        String tableName = ShardingUtil.getTableName(userId);
        Map<String, Object> params = new HashMap<>();
        params.put("tableName", tableName);
        params.put("userId", userId);
        return userMapper.getUserById(params);
    }
}

分库分表中间件

  • ShardingSphere:功能强大,支持多种分片算法
  • MyCat:基于代理的分库分表方案
  • Vitess:YouTube开源的分库分表方案,适合超大规模

二、MySQL配置参数优化

2.1 连接相关配置

高并发下,连接数是最容易成为瓶颈的资源。合理配置连接参数至关重要。

关键参数

# /etc/my.cnf [mysqld] 部分

# 最大连接数,默认151,高并发建议500-2000
max_connections = 1000

# 每个连接的最大请求数,防止连接泄漏
max_user_connections = 800

# 连接超时时间(秒)
connect_timeout = 10

# 等待超时时间(秒),空闲连接自动关闭
wait_timeout = 600

# 交互式超时时间(秒)
interactive_timeout = 600

# back_log队列长度,允许暂时挂起的连接数
back_log = 500

参数详解

  • max_connections:这个值不是越大越好。每个连接至少消耗1MB内存,1000个连接就是1GB。同时,操作系统对进程能打开的文件数有限制(ulimit -n),需要同步调整。
  • wait_timeout:设置过长会导致大量空闲连接占用资源,过短会导致频繁重连。600秒(10分钟)是较合理的值。

监控连接数

-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';

-- 查看最大连接数
SHOW VARIABLES LIKE 'max_connections';

-- 查看连接数使用率
SELECT 
    VARIABLE_VALUE as max_conn,
    (SELECT COUNT(*) FROM information_schema.PROCESSLIST) as current_conn,
    (SELECT COUNT(*) FROM information_schema.PROCESSLIST) / VARIABLE_VALUE * 100 as usage_rate
FROM information_schema.GLOBAL_VARIABLES 
WHERE VARIABLE_NAME = 'max_connections';

2.2 InnoDB缓冲池配置

InnoDB缓冲池(Buffer Pool)是MySQL性能的核心,它缓存数据和索引在内存中,减少磁盘I/O。

关键配置

# 缓冲池大小,通常设置为物理内存的50%-70%
innodb_buffer_pool_size = 8G

# 缓冲池实例数,多实例减少竞争
innodb_buffer_pool_instances = 8

# 缓冲池刷新策略
innodb_buffer_pool_flush_at_neighbors = 1
innodb_buffer_pool_dump_at_shutdown = 1
innodb_buffer_pool_load_at_startup = 1

# 页大小
innodb_page_size = 16K

配置建议

  • 8GB内存服务器innodb_buffer_pool_size = 4G
  • 32GB内存服务器innodb_buffer_pool_size = 24G
  • innodb_buffer_pool_instances:当缓冲池大于1GB时,建议设置为8或16,避免大内存下的锁竞争

监控缓冲池命中率

-- 缓冲池命中率应 > 99%
SELECT 
    (1 - (SUM(VARIABLE_VALUE) / @@innodb_buffer_pool_size)) * 100 AS hit_rate
FROM information_schema.GLOBAL_STATUS 
WHERE VARIABLE_NAME IN ('Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests');

-- 查看缓冲池使用情况
SHOW ENGINE INNODB STATUS\G
-- 查看BUFFER POOL AND MEMORY部分

2.3 日志文件配置

日志文件影响写入性能和数据安全性。

关键配置

# 事务日志文件大小
innodb_log_file_size = 2G

# 日志文件数量
innodb_log_files_in_group = 3

# 日志缓冲区大小
innodb_log_buffer_size = 64M

# 刷新日志的策略
innodb_flush_log_at_trx_commit = 1  # 1=每次提交刷盘(最安全),2=每秒刷盘(高性能)
innodb_flush_method = O_DIRECT      # 绕过操作系统缓存,直接写入磁盘

配置说明

  • innodb_log_file_size:总日志大小约为1-2GB,太大恢复时间长,太小导致频繁checkpoint
  • innodb_flush_log_at_trx_commit
    • 1:每次事务提交都写入磁盘,ACID完全保证,性能最低
    • 2:每秒写入磁盘,可能丢失1秒数据,性能最高
    • 0:写入操作系统缓存,不保证刷盘,风险最大

2.4 其他重要参数

# 并发线程数
innodb_thread_concurrency = 0  # 0表示不限制,让InnoDB自动管理

# 读入缓冲区
read_buffer_size = 2M

# 排序缓冲区
sort_buffer_size = 4M

# 临时表内存大小
tmp_table_size = 64M
max_heap_table_size = 64M

# 查询缓存(MySQL 8.0已移除)
# query_cache_type = 0
# query_cache_size = 0

# 慢查询日志
slow_query_log = 1
long_query_time = 1
log_queries_not_using_indexes = 1

三、SQL与索引优化

3.1 索引设计原则

索引是SQL性能优化的重中之重。好的索引能让查询速度提升几个数量级。

索引设计黄金法则

  1. 最左前缀原则:复合索引必须从最左列开始匹配
  2. 选择性高:区分度大的列适合建索引(如user_id, email)
  3. 覆盖索引:查询列全部在索引中,避免回表
  4. 避免冗余索引:定期检查并删除无用索引

案例:用户表索引优化

-- 原始表结构
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    email VARCHAR(100),
    status TINYINT,
    created_at TIMESTAMP,
    age INT,
    city VARCHAR(50)
);

-- 优化前:全表扫描
SELECT * FROM user WHERE email = 'user@example.com';

-- 优化后:添加索引
ALTER TABLE user ADD INDEX idx_email (email);

-- 复合索引示例:查询活跃用户
SELECT id, username FROM user 
WHERE status = 1 AND created_at > '2024-01-01';

-- 创建复合索引(注意顺序)
ALTER TABLE user ADD INDEX idx_status_created (status, created_at);

-- 覆盖索引示例:避免回表
SELECT id, username, email FROM user 
WHERE username LIKE 'john%';

-- 创建覆盖索引
ALTER TABLE user ADD INDEX idx_username_cover (username, id, email);

索引使用分析

-- 使用EXPLAIN分析执行计划
EXPLAIN SELECT * FROM user WHERE email = 'user@example.com';
-- 关注type: ref(索引查找) vs ALL(全表扫描)
-- 关注rows: 扫描行数越少越好
-- 关注Extra: Using index(覆盖索引)vs Using where(回表)

-- 查看索引使用情况
SELECT * FROM sys.schema_unused_indexes;  -- 未使用索引
SELECT * FROM sys.schema_redundant_indexes; -- 冗余索引

3.2 避免索引失效的常见场景

反面案例

-- 1. 索引列参与计算
SELECT * FROM user WHERE YEAR(created_at) = 2024;  -- 失效
-- 优化:SELECT * FROM user WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';

-- 2. 隐式类型转换
SELECT * FROM user WHERE phone = 12345678901;  -- phone是VARCHAR,会失效
-- 优化:SELECT * FROM user WHERE phone = '12345678901';

-- 3. 前缀模糊查询
SELECT * FROM user WHERE username LIKE '%john';  -- 失效
-- 优化:SELECT * FROM user WHERE username LIKE 'john%';  -- 前缀匹配有效

-- 4. OR条件(部分列无索引)
SELECT * FROM user WHERE email = 'a@b.com' OR age = 25;  -- age无索引导致全表扫描
-- 优化:UNION ALL 或分别查询

-- 5. 范围查询后无法使用后续索引
SELECT * FROM user WHERE status > 0 AND created_at = '2024-01-01';  
-- 复合索引(status, created_at)中,created_at无法使用
-- 优化:调整索引顺序或拆分查询

3.3 SQL语句优化技巧

*1. 避免SELECT **

-- 反例:查询所有列,可能无法使用覆盖索引
SELECT * FROM user WHERE id = 1;

-- 正例:只查询需要的列
SELECT id, username, email FROM user WHERE id = 1;

2. 分页优化

-- 反例:深度分页性能差
SELECT * FROM user ORDER BY id LIMIT 1000000, 20;

-- 正例1:延迟关联
SELECT u.* FROM user u 
INNER JOIN (SELECT id FROM user ORDER BY id LIMIT 1000000, 20) tmp 
ON u.id = tmp.id;

-- 正例2:记录上次id
SELECT * FROM user WHERE id > 1000000 ORDER BY id LIMIT 20;

3. 批量操作

-- 反例:逐条插入
INSERT INTO user (username, email) VALUES ('user1', 'a@b.com');
INSERT INTO user (username, email) VALUES ('user2', 'c@d.com');
-- ... 1000次

-- 正例:批量插入
INSERT INTO user (username, email) VALUES 
('user1', 'a@b.com'),
('user2', 'c@d.com'),
-- ... 1000条
('user1000', 'z@y.com');

-- 批量更新
UPDATE user SET status = 1 WHERE id IN (1,2,3,...,1000);

4. 使用UNION ALL替代UNION

-- 如果不需要去重,UNION ALL性能更好
SELECT id FROM user WHERE status = 1
UNION ALL
SELECT id FROM user WHERE status = 2;

5. 小表驱动大表

-- 反例:大表驱动小表
SELECT * FROM big_table b 
JOIN small_table s ON b.id = s.big_id 
WHERE s.type = 'A';

-- 正例:小表驱动大表(MySQL优化器会自动处理,但显式控制更好)
SELECT * FROM small_table s 
JOIN big_table b ON s.big_id = b.id 
WHERE s.type = 'A';

3.4 慢查询分析与优化

开启慢查询日志

-- 临时开启
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 1;

-- 永久配置(my.cnf)
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = 1

使用pt-query-digest分析

# 安装Percona Toolkit
sudo apt-get install percona-toolkit

# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log > slow_report.txt

# 输出示例:
# Query 1: 12.5 QPS, 95% latency 250ms
# SELECT * FROM user WHERE email = ?
# Time range: 2024-01-01 00:00:00 to 01:01:00
# Attribute    pct   total     min     max     avg     95%  stddev  median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count          45    4500
# Exec time      67   1125s    200ms      2s    250ms    450ms    180ms    220ms
# Rows sent       3   9000       2       2       2       2       0       2
# Rows examine   89  45.0M   10.0K   10.0K   10.0K   10.0K       0   10.0K
# Bytes sent    100  1.2M     272     272     272     272       0     272
# ============ === ======= ======= ======= ======= ======= ======= =======
# Profile
# Rank Query ID           Response time    Calls R/Call V/M   Item
# ==== ================== ================ ===== ====== ===== ===========
#    1 0xF9A57DD5A4C...  1125.0452 67.0%  4500 0.2500  0.02 SELECT user

四、缓存策略优化

4.1 多级缓存架构

高并发下,纯数据库查询无法支撑,必须引入缓存。

典型架构

客户端 → CDN → Nginx → 应用缓存 → Redis → MySQL

缓存策略

  1. 本地缓存:Caffeine/Guava Cache,响应快但容量有限
  2. 分布式缓存:Redis/Memcached,容量大但网络开销
  3. 多级缓存:本地缓存 + 分布式缓存组合

4.2 Redis缓存实战

缓存读写模式

// 1. Cache Aside模式(最常用)
public User getUserById(Long id) {
    // 1. 先读缓存
    String key = "user:" + id;
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        return JSON.parseObject(cached, User.class);
    }
    
    // 2. 缓存未命中,读数据库
    User user = userMapper.selectById(id);
    if (user != null) {
        // 3. 写入缓存,设置过期时间
        redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
    }
    return user;
}

// 4. 更新缓存(更新数据库后删除缓存)
public void updateUser(User user) {
    userMapper.updateById(user);
    // 删除缓存,让下次查询重新加载
    String key = "user:" + user.getId();
    redisTemplate.delete(key);
}

缓存穿透防护

// 布隆过滤器防止缓存穿透
public User getUserWithBloom(Long id) {
    String key = "user:" + id;
    
    // 1. 布隆过滤器快速判断key是否存在
    if (!bloomFilter.mightContain(key)) {
        // 不存在,直接返回null,不查数据库
        return null;
    }
    
    // 2. 正常缓存逻辑
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        return JSON.parseObject(cached, User.class);
    }
    
    // 3. 查询数据库
    User user = userMapper.selectById(id);
    
    // 4. 如果数据库也不存在,缓存空值(防止同一key反复查询)
    if (user == null) {
        redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES); // 缓存空值5分钟
        return null;
    }
    
    redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
    return user;
}

缓存雪崩防护

// 设置随机过期时间,避免同时失效
public void setWithRandomExpire(String key, Object value) {
    int expire = 1800 + new Random().nextInt(600); // 30-40分钟随机
    redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}

// 热点数据永不过期 + 后台异步更新
public void setHotData(String key, Object value) {
    // 设置过期时间很长(如7天)
    redisTemplate.opsForValue().set(key, value, 7, TimeUnit.DAYS);
    
    // 启动定时任务异步更新
    scheduleHotDataRefresh(key);
}

4.3 缓存与数据库一致性

最终一致性方案

// 使用Canal监听MySQL binlog,同步更新缓存
// Canal Server配置
canal.instance.filter.regex=db\\.user
canal.instance.filter.black.field=user:status

// 消费binlog示例
@Component
public class CanalClient {
    @Autowired
    private RedisTemplate redisTemplate;
    
    public void handleUpdate(String table, String data) {
        if ("user".equals(table)) {
            // 解析binlog数据
            User user = parseUser(data);
            // 删除或更新缓存
            String key = "user:" + user.getId();
            redisTemplate.delete(key);
        }
    }
}

五、数据库连接池优化

5.1 HikariCP配置

HikariCP是目前性能最好的Java连接池。

配置示例

spring:
  datasource:
    hikari:
      # 连接池名称
      pool-name: MasterDBPool
      # 最大连接数
      maximum-pool-size: 50
      # 最小空闲连接
      minimum-idle: 10
      # 连接超时时间(毫秒)
      connection-timeout: 30000
      # 空闲连接存活时间(毫秒)
      idle-timeout: 600000
      # 连接最大生命周期(毫秒)
      max-lifetime: 1800000
      # 连接测试查询
      connection-test-query: SELECT 1
      # 是否自动提交
      auto-commit: true
      # 连接预热
      initialization-fail-timeout: 1
      #  leakDetectionThreshold: 60000  # 开发环境开启泄漏检测

参数调优建议

  • maximum-pool-size:根据CPU核心数和业务类型计算
    • CPU密集型:CPU核心数 + 1
    • IO密集型:CPU核心数 * 2
    • 混合型:CPU核心数 * 2 ~ CPU核心数 * 4
  • minimum-idle:通常设置为maximum-pool-size的1/3到1/2
  • max-lifetime:建议小于MySQL的wait_timeout,避免被服务端关闭

5.2 连接池监控

// HikariCP监控指标
@Component
public class DataSourceMonitor {
    @Autowired
    private HikariDataSource dataSource;
    
    @Scheduled(fixedRate = 60000)
    public void monitor() {
        HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
        
        System.out.println("当前活跃连接: " + poolMXBean.getActiveConnections());
        System.out.println("空闲连接: " + poolMXBean.getIdleConnections());
        System.out.println("总连接: " + poolMXBean.getTotalConnections());
        System.out.println("等待连接的线程数: " + poolMXBean.getThreadsAwaitingConnection());
        
        // 如果等待线程数持续>0,说明连接池不足,需要扩容
        if (poolMXBean.getThreadsAwaitingConnection() > 10) {
            // 告警或扩容
            alert("连接池不足,需要扩容");
        }
    }
}

六、高级优化策略

6.1 分区表优化

当单表数据量巨大但访问有明显时间特征时,使用分区表。

-- 按时间分区(RANGE分区)
CREATE TABLE log (
    id BIGINT,
    log_time DATETIME,
    content TEXT,
    INDEX idx_time (log_time)
) PARTITION BY RANGE (YEAR(log_time) * 100 + MONTH(log_time)) (
    PARTITION p202401 VALUES LESS THAN (202402),
    PARTITION p202402 VALUES LESS THAN (202403),
    PARTITION p202403 VALUES LESS THAN (202404),
    PARTITION p_max VALUES LESS THAN MAXVALUE
);

-- 查询时自动分区裁剪
SELECT * FROM log WHERE log_time BETWEEN '2024-02-01' AND '2024-02-28';
-- 只扫描p202402分区

6.2 并发控制

应用层限流

// 使用RateLimiter限制数据库写入频率
RateLimiter writeLimiter = RateLimiter.create(1000.0); // 每秒1000次

public void writeData(Data data) {
    writeLimiter.acquire(); // 获取许可,如果没有则阻塞
    // 执行数据库写入
    dataMapper.insert(data);
}

数据库层锁优化

-- 使用乐观锁(适合读多写少)
UPDATE user SET 
    balance = balance - 100,
    version = version + 1
WHERE id = 1 AND version = 5;  -- 版本号校验

-- 使用悲观锁(适合写多读少)
SELECT * FROM user WHERE id = 1 FOR UPDATE;  -- 加排他锁

6.3 监控与告警

Prometheus + Grafana监控MySQL

# mysqld_exporter配置
# /etc/default/mysqld_exporter
DATA_SOURCE_NAME="user:password@(localhost:3306)/"

# Prometheus配置
scrape_configs:
  - job_name: 'mysql'
    static_configs:
      - targets: ['localhost:9104']

关键监控指标

  • QPS:Queries per second
  • TPS:Transactions per second
  • 连接数使用率:Threads_connected / max_connections
  • 慢查询数:Slow_queries
  • InnoDB缓冲池命中率:(1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100
  • 复制延迟:Seconds_Behind_Master

告警规则示例

# 连接数超过80%告警
- alert: MySQLHighConnections
  expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections * 100 > 80
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "MySQL连接数过高"
    description: "当前连接数 {{ $value }}%,超过80%阈值"

# 慢查询告警
- alert: MySQLSlowQueries
  expr: increase(mysql_global_status_slow_queries[5m]) > 10
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "MySQL慢查询激增"
    description: "5分钟内新增 {{ $value }} 个慢查询"

七、实战案例:秒杀系统优化

7.1 问题场景

原始方案

-- 秒杀扣减库存
UPDATE item SET stock = stock - 1 WHERE id = 1 AND stock > 0;

问题

  • 高并发下大量请求阻塞在UPDATE语句
  • 数据库连接池耗尽
  • 系统响应时间从10ms飙升到5s+

7.2 优化方案

1. 预减库存(Redis)

public boolean seckill(Long itemId) {
    String key = "item:stock:" + itemId;
    
    // 1. Redis预减库存
    Long stock = redisTemplate.opsForValue().decrement(key);
    if (stock < 0) {
        // 库存不足,回滚
        redisTemplate.opsForValue().increment(key);
        return false;
    }
    
    // 2. 发送消息到队列
    rabbitTemplate.convertAndSend("seckill.order", itemId);
    return true;
}

// 异步消费队列,最终扣减数据库
@RabbitListener(queues = "seckill.order")
public void createOrder(Long itemId) {
    // 3. 数据库最终扣减(单线程,无竞争)
    itemMapper.reduceStock(itemId);
    
    // 4. 创建订单
    orderMapper.createOrder(itemId);
}

2. 数据库优化

-- 1. stock字段加唯一索引,防止超卖
ALTER TABLE item ADD UNIQUE INDEX idx_stock (stock);

-- 2. 使用乐观锁
UPDATE item SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND stock > 0 AND version = #{version};

-- 3. 分库分表(item表按id取模分4张表)

3. 限流熔断

// 令牌桶限流
RateLimiter limiter = RateLimiter.create(5000.0); // 每秒5000个请求

public boolean seckill(Long itemId) {
    if (!limiter.tryAcquire()) {
        throw new RateLimitException("请求过于频繁,请稍后重试");
    }
    // ... 业务逻辑
}

// 熔断器(Hystrix)
@HystrixCommand(
    fallbackMethod = "seckillFallback",
    commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
    }
)
public boolean seckill(Long itemId) {
    // ... 业务逻辑
}

public boolean seckillFallback(Long itemId) {
    return false; // 降级返回
}

7.3 效果对比

指标 优化前 优化后
QPS 500 15000
平均响应时间 2000ms 50ms
99%响应时间 5000ms 200ms
数据库连接数 100+ <20
是否超卖

八、总结与最佳实践

8.1 优化优先级

  1. SQL与索引优化(成本最低,效果最明显)
  2. 缓存策略(大幅提升QPS)
  3. 读写分离(解决读压力)
  4. 连接池调优(避免资源耗尽)
  5. 分库分表(终极方案,成本高)

8.2 日常维护 checklist

  • [ ] 每天检查慢查询日志
  • [ ] 监控连接数使用率
  • [ ] 检查主从复制延迟
  • [ ] 定期分析索引使用情况
  • [ ] 监控缓冲池命中率(>99%)
  • [ ] 检查磁盘空间和I/O
  • [ ] 定期备份并测试恢复

8.3 避免的坑

  1. 不要盲目增加max_connections:可能导致OOM
  2. 不要在数据库做复杂计算:应用层处理
  3. 不要忽略主从延迟:可能导致数据不一致
  4. 不要滥用索引:每个索引都有维护成本
  5. 不要忽略监控:优化是持续的过程

8.4 性能优化哲学

“先测量,再优化”:使用性能分析工具(Percona Toolkit, pt-query-digest)找到真正的瓶颈,而不是凭感觉猜测。

“二八法则”:80%的性能问题来自20%的慢查询,集中精力优化这些查询。

“架构演进”:不要一开始就设计复杂的架构,根据业务增长逐步演进。

通过以上策略的综合运用,MySQL完全可以支撑每秒数万甚至数十万的并发请求。记住,优化是一个持续的过程,需要根据业务发展和监控数据不断调整策略。