引言:理解高并发场景下的MySQL挑战

在现代互联网应用中,高并发访问已经成为常态。无论是电商平台的秒杀活动、社交媒体的热点事件,还是金融系统的交易高峰,MySQL数据库都面临着前所未有的压力。高并发场景下,数据库容易出现连接耗尽、响应延迟飙升、甚至服务崩溃等问题。本文将深入探讨MySQL高并发处理的核心策略,从架构设计到参数调优,从代码实现到监控预警,提供一套完整的实战指南,帮助您构建稳定高效的数据库系统。

一、高并发对MySQL的核心影响

1.1 连接层压力

当并发请求激增时,MySQL的连接数会快速上升。默认的最大连接数(max_connections=151)在高并发场景下很快就会被耗尽,导致新连接被拒绝。同时,大量的连接会消耗服务器内存,每个连接都需要分配thread_stack(默认256KB)和read_buffer等内存结构。

1.2 锁竞争加剧

InnoDB引擎的行锁、间隙锁在高并发写操作下会产生激烈的锁竞争。特别是热点数据更新时,大量事务会排队等待锁释放,导致响应时间线性增长。严重的锁竞争还会引发死锁,增加事务回滚率。

1.3 I/O瓶颈

高并发下的大量读写操作会迅速耗尽磁盘I/O带宽。特别是未优化的查询,如全表扫描、大量随机读等,会使磁盘I/O成为系统瓶颈。同时,redo log、binlog的频繁写入也会加剧I/O压力。

1.4 CPU和内存资源争用

复杂的查询、排序、分组操作会消耗大量CPU资源。高并发下的临时表、排序缓冲区等内存结构会频繁申请释放,增加CPU的上下文切换开销。内存不足时还会触发swap,导致性能急剧下降。

二、连接层优化策略

2.1 合理配置连接数参数

max_connections:这是MySQL允许的最大并发连接数。设置过高会耗尽系统内存,过低则会拒绝服务。建议根据服务器内存和业务特点计算:

理论最大连接数 = 可用内存 / (每个连接的内存开销)
每个连接的内存开销 ≈ 1MB (包括thread_stack、read_buffer等)

例如,32GB内存的服务器,建议设置max_connections=2000-3000。但实际还需要考虑其他内存消耗。

关键配置示例

[mysqld]
max_connections = 2000
thread_cache_size = 100  # 线程缓存,减少线程创建销毁开销
back_log = 500  # TCP连接队列长度,应对瞬时高峰

2.2 连接池技术应用

应用层必须使用连接池,避免频繁创建销毁连接。主流连接池配置要点:

HikariCP(Java)配置示例

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(100);  // 池大小,根据业务调整
config.setMinimumIdle(10);       // 最小空闲连接
config.setConnectionTimeout(30000); // 获取连接超时时间
config.setIdleTimeout(600000);    // 空闲连接超时
config.setMaxLifetime(1800000);   // 连接最大存活时间
config.setLeakDetectionThreshold(60000); // 连接泄漏检测
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
HikariDataSource dataSource = new HikariDataSource(config);

连接池大小计算公式

连接池大小 = (核心数 * 2) + 有效磁盘数

对于读密集型应用可以适当增大,写密集型应保守设置。

2.3 读写分离架构

通过主从复制实现读写分离,将读请求分发到从库,减轻主库压力:

// 简单的读写分离数据源路由
public class DataSourceRouter extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    
    @Override
    protected Object determineCurrentLookupKey() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void setMaster() {
        CONTEXT_HOLDER.set("master");
    }
    
    public static void setSlave() {
        CONTEXT_HOLDER.set("slave");
    }
    
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

// 使用AOP进行路由
@Aspect
@Component
public class DataSourceAspect {
    @Before("@annotation(master)")
    public void setMaster(JoinPoint jp, Master master) {
        DataSourceRouter.setMaster();
    }
    
    @Before("@annotation(slave)")
    public void setSlave(JoinPoint jp, Slave slave) {
        DataSourceRouter.setSlave();
    }
    
    @After("@annotation(master) || @annotation(slave)")
    public void clear(JoinPoint jp) {
        DataSourceRouter.clear();
    }
}

三、SQL与索引优化

3.1 执行计划分析

使用EXPLAIN分析SQL执行计划,重点关注type、key、rows、Extra字段:

-- 示例:分析一个慢查询
EXPLAIN SELECT o.order_id, c.name, o.amount 
FROM orders o 
JOIN customers c ON o.customer_id = c.id 
WHERE o.create_time >= '2024-01-01' 
AND o.status = 'PAID' 
ORDER BY o.amount DESC 
LIMIT 100;

-- 关键指标解读:
-- type: ALL(全表扫描)→ 需要优化
-- key: NULL(未使用索引)→ 需要添加索引
-- rows: 1000000(扫描行数过多)→ 索引选择性差
-- Extra: Using filesort(文件排序)→ 需要优化索引或查询

3.2 索引设计原则

最左前缀原则:复合索引必须遵循最左前缀匹配,否则索引失效。

-- 创建复合索引
ALTER TABLE orders ADD INDEX idx_customer_status_time (customer_id, status, create_time);

-- 有效使用索引的查询:
SELECT * FROM orders WHERE customer_id = 123; -- ✅ 使用索引第一列
SELECT * FROM orders WHERE customer_id = 123 AND status = 'PAID'; -- ✅ 使用前两列
SELECT * FROM orders WHERE customer_id = 123 AND status = 'PAID' AND create_time >= '2024-01-01'; -- ✅ 完全匹配

-- 无效使用索引的查询:
SELECT * FROM orders WHERE status = 'PAID'; -- ❌ 未使用最左列
SELECT * FROM orders WHERE create_time >= '2024-01-01'; -- ❌ 未使用最左列

索引选择性:选择性高的列适合建索引。选择性 = 不重复值数量 / 总行数。

-- 计算列的选择性
SELECT COUNT(DISTINCT status) / COUNT(*) AS selectivity FROM orders;
-- 结果 > 0.3 适合建索引,< 0.1 不适合

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

  1. 函数操作:WHERE YEAR(create_time) = 2024 → 改为 create_time BETWEEN ‘2024-01-01’ AND ‘2024-12-31’
  2. 隐式类型转换:WHERE phone = 13800138000 (phone是varchar) → 改为 WHERE phone = ‘13800138000’
  3. LIKE前缀模糊查询:WHERE name LIKE ‘%张%’ → 改为 WHERE name LIKE ‘张%’
  4. OR条件:WHERE id = 1 OR id = 2 → 改为 WHERE id IN (1,2)
  5. 负向查询:!=、NOT IN、NOT LIKE 通常无法使用索引

3.4 覆盖索引与延迟关联

覆盖索引:查询的列全部在索引中,避免回表操作。

-- 原始查询(需要回表)
SELECT order_id, customer_id, amount, status FROM orders WHERE customer_id = 123;

-- 创建覆盖索引
ALTER TABLE orders ADD INDEX idx_customer_cover (customer_id, order_id, amount, status);

-- 优化后查询(无需回表)
SELECT order_id, customer_id, amount, status FROM orders WHERE customer_id = 123;

延迟关联:对于需要返回大量列的查询,先通过索引获取主键,再关联获取其他列。

-- 低效查询
SELECT * FROM orders WHERE customer_id = 123 AND amount > 1000;

-- 优化:延迟关联
SELECT t1.* 
FROM orders t1 
JOIN (SELECT order_id FROM orders WHERE customer_id = 123 AND amount > 1000) t2 
ON t1.order_id = t2.order_id;

四、事务与锁优化

4.1 事务设计原则

短事务原则:事务应尽可能短,减少锁持有时间。

// ❌ 错误示例:长事务
@Transactional
public void processOrder(Long orderId) {
    // 1. 查询订单(获取共享锁)
    Order order = orderMapper.selectById(orderId);
    
    // 2. 调用外部API(耗时操作,事务未提交,锁未释放)
    paymentService.verifyPayment(order);
    
    // 3. 更新状态
    order.setStatus("PAID");
    orderMapper.update(order);
}

// ✅ 正确示例:短事务
public void processOrder(Long orderId) {
    // 1. 事务外执行耗时操作
    paymentService.verifyPayment(orderId);
    
    // 2. 缩短事务范围
    try {
        transactionTemplate.execute(status -> {
            Order order = orderMapper.selectById(orderId);
            order.setStatus("PAID");
            orderMapper.update(order);
            return null;
        });
    } catch (Exception e) {
        // 处理异常
    }
}

4.2 乐观锁与悲观锁选择

乐观锁:适合读多写少场景,通过版本号避免更新丢失。

-- 表结构
CREATE TABLE product (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    stock INT,
    version INT DEFAULT 0
);

-- 更新操作
UPDATE product 
SET stock = stock - 1, version = version + 1 
WHERE id = 123 AND version = 2; -- 版本号匹配

-- Java代码实现
public boolean deductStock(Long productId, int quantity) {
    int retry = 3;
    while (retry-- > 0) {
        Product product = productMapper.selectById(productId);
        int newVersion = product.getVersion() + 1;
        int affected = productMapper.updateStock(productId, quantity, product.getVersion(), newVersion);
        if (affected > 0) {
            return true;
        }
        // 短暂休眠后重试
        Thread.sleep(50);
    }
    return false;
}

悲观锁:适合写多读少、强一致性场景。

-- 显式悲观锁
BEGIN;
SELECT * FROM inventory WHERE product_id = 123 FOR UPDATE; -- 排他锁
UPDATE inventory SET stock = stock - 1 WHERE product_id = 123;
COMMIT;

-- 避免间隙锁:使用等值查询
-- ❌ 范围查询会产生间隙锁
SELECT * FROM inventory WHERE product_id BETWEEN 100 AND 200 FOR UPDATE;
-- ✅ 等值查询减少锁范围
SELECT * FROM inventory WHERE product_id = 123 FOR UPDATE;

4.3 死锁预防与处理

死锁常见场景

  1. 交叉更新:A更新1→2,B更新2→1
  2. 索引顺序不一致:不同事务按不同顺序访问数据
  3. 间隙锁冲突:范围更新导致的死锁

预防策略

-- 1. 固定访问顺序
-- 事务1和事务2都按相同顺序访问:先orders后order_items

-- 2. 使用相同索引
-- 所有事务都使用主键或相同索引进行更新

-- 3. 减少间隙锁
-- 使用等值查询代替范围查询
-- 降低事务隔离级别(RC级别间隙锁较少)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

死锁监控与处理

-- 查看最近死锁信息
SHOW ENGINE INNODB STATUS\G

-- 在MySQL 8.0+中启用死锁监控
SET GLOBAL innodb_print_all_deadlocks = ON;

-- 应用层重试机制
public <T> T executeWithRetry(Supplier<T> operation) {
    int maxRetries = 3;
    for (int i = 0; i < maxRetries; i++) {
        try {
            return operation.get();
        } catch (DeadlockException e) {
            if (i == maxRetries - 1) throw e;
            // 指数退避
            try {
                Thread.sleep((long) Math.pow(2, i) * 100);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
    }
    throw new RuntimeException("Max retries exceeded");
}

五、查询缓存与缓冲区优化

5.1 查询缓存(Query Cache)

注意:MySQL 8.0已移除查询缓存功能,但在5.7及以下版本仍有价值。对于读多写少的静态数据,查询缓存能显著提升性能。

-- MySQL 5.7 配置
[mysqld]
query_cache_type = 1  # 0:OFF, 1:ON, 2:DEMAND
query_cache_size = 128M
query_cache_limit = 2M  # 单个查询最大缓存结果集

-- 在SQL中显式使用缓存
SELECT SQL_CACHE * FROM static_table WHERE id = 1;
SELECT SQL_NO_CACHE * FROM dynamic_table WHERE id = 1;

5.2 InnoDB缓冲池(Buffer Pool)

核心参数

[mysqld]
innodb_buffer_pool_size = 24G  # 70-80%物理内存
innodb_buffer_pool_instances = 8  # 多实例减少竞争
innodb_buffer_pool_dump_at_shutdown = 1  # 关闭时dump热数据
innodb_buffer_pool_load_at_startup = 1    # 启动时加载热数据

监控缓冲池命中率

-- 缓冲池命中率应 > 99%
SELECT 
    (1 - (SUM(VARIABLE_VALUE) / @@innodb_buffer_pool_size)) * 100 AS hit_rate
FROM performance_schema.global_status 
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads';

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

5.3 其他关键缓冲区

[mysqld]
# 读缓冲
read_buffer_size = 2M
read_rnd_buffer_size = 4M

# 排序缓冲
sort_buffer_size = 4M

# Join缓冲
join_buffer_size = 4M

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

# 线程栈大小(根据连接数调整)
thread_stack = 256K

六、高并发写入优化

6.1 批量操作

批量插入:减少网络往返和事务开销。

// ❌ 单条插入(N次网络往返)
for (Order order : orderList) {
    orderMapper.insert(order);
}

// ✅ 批量插入(1次网络往返)
public void batchInsert(List<Order> orders) {
    // JDBC批量处理
    String sql = "INSERT INTO orders (order_id, customer_id, amount, status) VALUES (?, ?, ?, ?)";
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql)) {
        
        int batchSize = 0;
        for (Order order : orders) {
            ps.setLong(1, order.getOrderId());
            ps.setLong(2, order.getCustomerId());
            ps.setBigDecimal(3, order.getAmount());
            ps.setString(4, order.getStatus());
            ps.addBatch();
            
            // 每1000条提交一次
            if (++batchSize % 1000 == 0) {
                ps.executeBatch();
            }
        }
        ps.executeBatch(); // 提交剩余
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

批量更新

-- 使用INSERT ON DUPLICATE KEY UPDATE
INSERT INTO orders (order_id, customer_id, amount) 
VALUES (1, 100, 500), (2, 101, 300), (3, 102, 800)
ON DUPLICATE KEY UPDATE 
    amount = VALUES(amount),
    update_time = NOW();

-- 使用CASE WHEN批量更新
UPDATE orders 
SET status = CASE 
    WHEN order_id = 1 THEN 'PAID'
    WHEN order_id = 2 THEN 'SHIPPED'
    WHEN order_id = 3 THEN 'CANCELLED'
END
WHERE order_id IN (1, 2, 3);

6.2 异步写入与队列

消息队列解耦:将写操作异步化,削峰填谷。

// 生产者:将写操作放入队列
public void createOrderAsync(Order order) {
    // 1. 快速返回,不等待数据库写入
    order.setStatus("PENDING");
    orderMapper.insert(order); // 先写入状态
    
    // 2. 发送消息到队列
    rabbitTemplate.convertAndSend("order.create", order);
    
    // 3. 异步处理后续逻辑
    // 消费者会处理支付、库存扣减等
}

// 消费者:批量处理队列消息
@RabbitListener(queues = "order.create")
public void processOrderCreate(Order order) {
    // 批量处理:累积一定数量后批量提交
    List<Order> batch = orderBuffer.getAndAdd(order);
    if (batch.size() >= 100) {
        batchInsert(batch);
        orderBuffer.clear();
    }
}

6.3 分区表(Partitioning)

对于大数据量表,使用分区可以提升查询性能和管理效率。

-- 按时间范围分区(适合日志、订单等时间序列数据)
CREATE TABLE orders (
    order_id BIGINT,
    customer_id BIGINT,
    amount DECIMAL(10,2),
    create_time DATETIME,
    status VARCHAR(20),
    PRIMARY KEY (order_id, create_time) -- 分区键必须包含在主键中
) PARTITION BY RANGE (YEAR(create_time)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 按哈希分区(适合均匀分布)
CREATE TABLE user_log (
    id BIGINT,
    user_id BIGINT,
    log_time DATETIME
) PARTITION BY HASH(user_id) PARTITIONS 16;

-- 查看分区信息
SELECT PARTITION_NAME, TABLE_ROWS 
FROM INFORMATION_SCHEMA.PARTITIONS 
WHERE TABLE_NAME = 'orders';

七、读写分离与分库分表

7.1 主从复制配置

主库配置(my.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  # 保证ACID

从库配置

[mysqld]
server-id = 2
relay_log = mysql-relay-bin
read_only = 1  # 只读模式,防止误写

创建复制用户

-- 在主库执行
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

-- 在从库执行
CHANGE MASTER TO
MASTER_HOST='master_ip',
MASTER_USER='repl',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=1234;

START SLAVE;
SHOW SLAVE STATUS\G  -- 检查Slave_IO_Running和Slave_SQL_Running为Yes

7.2 读写分离实现

ShardingSphere-JDBC配置

# 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:3306/mydb
        username: root
        password: password
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave0:3306/mydb
        username: root
        password: password
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave1:3306/mydb
        username: root
        password: password
    
    rules:
      readwrite-splitting:
        data-sources:
          mydb:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave0,slave1
              load-balance-type: ROUND_ROBIN

7.3 分库分表(Sharding)

垂直分库:按业务模块拆分数据库。

原库:mydb (user, order, product, payment)
拆分后:
- user_db: user, user_profile
- order_db: order, order_item
- product_db: product, category
- payment_db: payment, refund

水平分表:按数据特征拆分大表。

-- 按用户ID取模分表(16张表)
-- order_0, order_1, ..., order_15

-- ShardingSphere配置
spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: mydb.order_$->{0..15}
            table-strategy:
              standard:
                sharding-column: customer_id
                sharding-algorithm-name: mod
        sharding-algorithms:
          mod:
            type: MOD
            props:
              sharding-count: 16

八、高并发配置调优

8.1 InnoDB核心参数

[mysqld]
# 1. 缓冲池(最重要)
innodb_buffer_pool_size = 24G  # 70-80%内存
innodb_buffer_pool_instances = 8  # 多实例减少竞争

# 2. 日志文件
innodb_log_file_size = 2G      # 重做日志大小,建议1-2G
innodb_log_buffer_size = 64M   # 日志缓冲区
innodb_flush_log_at_trx_commit = 1  # 1:安全, 2:性能更好但可能丢数据

# 3. I/O相关
innodb_flush_method = O_DIRECT  # 绕过OS缓存,直接I/O
innodb_io_capacity = 2000       # SSD建议2000-5000
innodb_io_capacity_max = 4000   # 最大IOPS

# 4. 并发控制
innodb_thread_concurrency = 0   # 0:自动管理,建议0
innodb_read_io_threads = 8
innodb_write_io_threads = 8

# 5. 锁相关
innodb_lock_wait_timeout = 50   # 锁等待超时
innodb_rollback_on_timeout = 1  # 超时回滚整个事务

8.2 连接与线程参数

[mysqld]
# 连接数
max_connections = 2000
thread_cache_size = 100
back_log = 500

# 超时设置
wait_timeout = 600  # 非交互连接超时,避免连接泄漏
interactive_timeout = 600

# 内存相关
tmp_table_size = 64M
max_heap_table_size = 64M
sort_buffer_size = 4M
join_buffer_size = 4M
read_buffer_size = 2M
read_rnd_buffer_size = 4M

8.3 日志与监控参数

[mysqld]
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1  # 记录超过1秒的查询
log_queries_not_using_indexes = 1

# 错误日志
log_error = /var/log/mysql/error.log

# 通用查询日志(调试用,生产环境关闭)
general_log = 0
general_log_file = /var/log/mysql/general.log

# 性能监控
performance_schema = ON  # MySQL 5.6+ 默认开启

九、高并发场景下的监控与预警

9.1 关键监控指标

连接数监控

-- 当前连接数 vs 最大连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';

-- 计算使用率
SELECT 
    (VARIABLE_VALUE / @@max_connections) * 100 AS usage_rate
FROM performance_schema.global_status 
WHERE VARIABLE_NAME = 'Threads_connected';

QPS/TPS监控

-- 计算每秒查询数和事务数
SELECT 
    (VARIABLE_VALUE - @last_queries) / TIMESTAMPDIFF(SECOND, @last_time, NOW()) AS qps,
    (VARIABLE_VALUE - @last_committed) / TIMESTAMPDIFF(SECOND, @last_time, NOW()) AS tps
FROM performance_schema.global_status 
WHERE VARIABLE_NAME IN ('Queries', 'Com_commit');

-- 使用Prometheus + Grafana监控
# mysqld_exporter配置
--collect.global_status
--collect.info_schema.innodb_metrics
--collect.perf_schema.tablelocks
--collect.perf_schema.eventsstatements

慢查询监控

-- 查看最近慢查询
SELECT 
    DIGEST_TEXT,
    COUNT_STAR,
    AVG_TIMER_WAIT/1000000000000 AS avg_time_sec,
    MAX_TIMER_WAIT/1000000000000 AS max_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000000  -- 1秒
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;

9.2 锁监控

-- 查看当前锁等待
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

-- 查看MDL锁(元数据锁)
SELECT 
    ps.id AS processlist_id,
    ps.user,
    ps.host,
    ps.db,
    ps.command,
    ps.time,
    ps.state,
    ps.info
FROM performance_schema.metadata_locks ml
JOIN performance_schema.threads t ON ml.OWNER_THREAD_ID = t.THREAD_ID
JOIN information_schema.processlist ps ON t.PROCESSLIST_ID = ps.id
WHERE ml.OBJECT_SCHEMA = 'mydb' AND ml.OBJECT_NAME = 'orders';

9.3 性能监控工具

Percona Toolkit

# 安装
sudo apt-get install percona-toolkit

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

# 监控复制延迟
pt-heartbeat --check h=master,u=root,p=password --master-server-id=1

# 在线DDL工具
pt-online-schema-change --alter "ADD INDEX idx_status (status)" D=mydb,t=orders --execute

Prometheus + mysqld_exporter

# docker-compose.yml
version: '3'
services:
  mysqld_exporter:
    image: prom/mysqld-exporter
    environment:
      DATA_SOURCE_NAME: "user:password@(mysql:3306)/"
    ports:
      - "9104:9104"
  
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

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

10.1 问题分析

场景:10000 QPS秒杀,库存100件,热点数据更新。

问题

  1. 库存行锁竞争激烈
  2. 大量请求导致连接耗尽
  3. 数据库CPU飙升至100%

10.2 优化方案

架构层

// 1. 缓存预热与库存扣减
public class SeckillService {
    private static final String STOCK_KEY = "seckill:stock:";
    private static final String ORDER_KEY = "seckill:order:";
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private OrderMapper orderMapper;
    
    // 预热:将库存加载到Redis
    public void preloadStock(Long productId, int stock) {
        redisTemplate.opsForValue().set(STOCK_KEY + productId, String.valueOf(stock));
    }
    
    // 秒杀下单
    public Result seckill(Long productId, Long userId) {
        String lockKey = "seckill:lock:" + productId;
        
        // 1. Redis预扣库存(Lua脚本保证原子性)
        String luaScript = 
            "if redis.call('exists', KEYS[1]) == 1 then " +
            "   local stock = tonumber(redis.call('get', KEYS[1])); " +
            "   if stock > 0 then " +
            "       redis.call('decr', KEYS[1]); " +
            "       return 1; " +
            "   end; " +
            "end; " +
            "return 0;";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(STOCK_KEY + productId)
        );
        
        if (result == 0) {
            return Result.fail("库存不足");
        }
        
        // 2. 异步创建订单
        String orderId = IdUtil.randomUUID();
        rabbitTemplate.convertAndSend("seckill.order", 
            new SeckillMessage(userId, productId, orderId));
        
        return Result.success(orderId);
    }
    
    // 异步消费:批量写入数据库
    @RabbitListener(queues = "seckill.order")
    public void processSeckillOrder(List<SeckillMessage> batch) {
        // 批量插入订单
        List<Order> orders = batch.stream()
            .map(m -> new Order(m.getOrderId(), m.getUserId(), m.getProductId()))
            .collect(Collectors.toList());
        
        orderMapper.batchInsert(orders);
        
        // 批量更新库存(在业务低峰期)
        if (batch.size() >= 100) {
            updateStockBatch(batch);
        }
    }
}

数据库层

-- 1. 库存表使用乐观锁
CREATE TABLE stock (
    product_id BIGINT PRIMARY KEY,
    stock INT,
    version INT DEFAULT 0
);

-- 2. 订单表分表(按用户ID取模)
CREATE TABLE order_0 (
    order_id VARCHAR(50) PRIMARY KEY,
    user_id BIGINT,
    product_id BIGINT,
    create_time DATETIME
) PARTITION BY HASH(user_id) PARTITIONS 16;

-- 3. 关闭MySQL查询缓存(写多读少)
SET GLOBAL query_cache_size = 0;

配置优化

[mysqld]
# 针对秒杀场景优化
innodb_buffer_pool_size = 32G
innodb_log_file_size = 4G
innodb_flush_log_at_trx_commit = 2  # 性能优先,可能丢1秒数据
max_connections = 3000
thread_cache_size = 200

10.3 效果对比

指标 优化前 优化后
平均响应时间 850ms 45ms
成功率 65% 99.5%
数据库CPU 100% 35%
连接数峰值 1500 800

十一、常见误区与最佳实践

11.1 常见误区

  1. 盲目增大max_connections:不解决根本问题,反而增加内存消耗
  2. 过度使用索引:索引越多,写入越慢,维护成本越高
  3. 长事务:持有锁时间过长,阻塞其他操作
  4. *SELECT **:返回不需要的列,增加I/O和网络开销
  5. 忽视从库延迟:读写分离时从库数据可能滞后

11.2 最佳实践清单

  • [ ] 使用连接池,合理设置池大小
  • [ ] 所有查询必须使用EXPLAIN分析
  • [ ] 事务尽可能短,避免事务内调用外部服务
  • [ ] 高并发写入使用批量操作+异步队列
  • [ ] 热点数据使用Redis缓存
  • [ ] 监控慢查询,定期优化
  • [ ] 主从复制开启GTID,便于故障恢复
  • [ ] 定期备份并测试恢复流程
  • [ ] 使用Percona Toolkit定期分析
  • [ ] 建立完善的监控预警体系

十二、总结

MySQL高并发优化是一个系统工程,需要从连接层、SQL层、事务层、架构层多维度入手。核心原则是:减少资源竞争、缩短操作时间、分散系统压力

关键要点

  1. 连接优化:连接池+读写分离,避免连接耗尽
  2. SQL优化:合理索引+覆盖索引,减少I/O
  3. 事务优化:短事务+乐观锁,减少锁竞争
  4. 架构优化:缓存+队列+分库分表,分散压力
  5. 监控预警:实时监控+快速响应,防患于未然

记住,没有银弹。每个业务场景都有其特殊性,需要根据实际压力测试结果,持续监控和调优,才能构建稳定高效的MySQL高并发系统。# MySQL高并发处理策略:如何避免数据库崩溃并提升系统性能的实战指南

引言:理解高并发场景下的MySQL挑战

在现代互联网应用中,高并发访问已经成为常态。无论是电商平台的秒杀活动、社交媒体的热点事件,还是金融系统的交易高峰,MySQL数据库都面临着前所未有的压力。高并发场景下,数据库容易出现连接耗尽、响应延迟飙升、甚至服务崩溃等问题。本文将深入探讨MySQL高并发处理的核心策略,从架构设计到参数调优,从代码实现到监控预警,提供一套完整的实战指南,帮助您构建稳定高效的数据库系统。

一、高并发对MySQL的核心影响

1.1 连接层压力

当并发请求激增时,MySQL的连接数会快速上升。默认的最大连接数(max_connections=151)在高并发场景下很快就会被耗尽,导致新连接被拒绝。同时,大量的连接会消耗服务器内存,每个连接都需要分配thread_stack(默认256KB)和read_buffer等内存结构。

1.2 锁竞争加剧

InnoDB引擎的行锁、间隙锁在高并发写操作下会产生激烈的锁竞争。特别是热点数据更新时,大量事务会排队等待锁释放,导致响应时间线性增长。严重的锁竞争还会引发死锁,增加事务回滚率。

1.3 I/O瓶颈

高并发下的大量读写操作会迅速耗尽磁盘I/O带宽。特别是未优化的查询,如全表扫描、大量随机读等,会使磁盘I/O成为系统瓶颈。同时,redo log、binlog的频繁写入也会加剧I/O压力。

1.4 CPU和内存资源争用

复杂的查询、排序、分组操作会消耗大量CPU资源。高并发下的临时表、排序缓冲区等内存结构会频繁申请释放,增加CPU的上下文切换开销。内存不足时还会触发swap,导致性能急剧下降。

二、连接层优化策略

2.1 合理配置连接数参数

max_connections:这是MySQL允许的最大并发连接数。设置过高会耗尽系统内存,过低则会拒绝服务。建议根据服务器内存和业务特点计算:

理论最大连接数 = 可用内存 / (每个连接的内存开销)
每个连接的内存开销 ≈ 1MB (包括thread_stack、read_buffer等)

例如,32GB内存的服务器,建议设置max_connections=2000-3000。但实际还需要考虑其他内存消耗。

关键配置示例

[mysqld]
max_connections = 2000
thread_cache_size = 100  # 线程缓存,减少线程创建销毁开销
back_log = 500  # TCP连接队列长度,应对瞬时高峰

2.2 连接池技术应用

应用层必须使用连接池,避免频繁创建销毁连接。主流连接池配置要点:

HikariCP(Java)配置示例

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(100);  // 池大小,根据业务调整
config.setMinimumIdle(10);       // 最小空闲连接
config.setConnectionTimeout(30000); // 获取连接超时时间
config.setIdleTimeout(600000);    // 空闲连接超时
config.setMaxLifetime(1800000);   // 连接最大存活时间
config.setLeakDetectionThreshold(60000); // 连接泄漏检测
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
HikariDataSource dataSource = new HikariDataSource(config);

连接池大小计算公式

连接池大小 = (核心数 * 2) + 有效磁盘数

对于读密集型应用可以适当增大,写密集型应保守设置。

2.3 读写分离架构

通过主从复制实现读写分离,将读请求分发到从库,减轻主库压力:

// 简单的读写分离数据源路由
public class DataSourceRouter extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    
    @Override
    protected Object determineCurrentLookupKey() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void setMaster() {
        CONTEXT_HOLDER.set("master");
    }
    
    public static void setSlave() {
        CONTEXT_HOLDER.set("slave");
    }
    
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

// 使用AOP进行路由
@Aspect
@Component
public class DataSourceAspect {
    @Before("@annotation(master)")
    public void setMaster(JoinPoint jp, Master master) {
        DataSourceRouter.setMaster();
    }
    
    @Before("@annotation(slave)")
    public void setSlave(JoinPoint jp, Slave slave) {
        DataSourceRouter.setSlave();
    }
    
    @After("@annotation(master) || @annotation(slave)")
    public void clear(JoinPoint jp) {
        DataSourceRouter.clear();
    }
}

三、SQL与索引优化

3.1 执行计划分析

使用EXPLAIN分析SQL执行计划,重点关注type、key、rows、Extra字段:

-- 示例:分析一个慢查询
EXPLAIN SELECT o.order_id, c.name, o.amount 
FROM orders o 
JOIN customers c ON o.customer_id = c.id 
WHERE o.create_time >= '2024-01-01' 
AND o.status = 'PAID' 
ORDER BY o.amount DESC 
LIMIT 100;

-- 关键指标解读:
-- type: ALL(全表扫描)→ 需要优化
-- key: NULL(未使用索引)→ 需要添加索引
-- rows: 1000000(扫描行数过多)→ 索引选择性差
-- Extra: Using filesort(文件排序)→ 需要优化索引或查询

3.2 索引设计原则

最左前缀原则:复合索引必须遵循最左前缀匹配,否则索引失效。

-- 创建复合索引
ALTER TABLE orders ADD INDEX idx_customer_status_time (customer_id, status, create_time);

-- 有效使用索引的查询:
SELECT * FROM orders WHERE customer_id = 123; -- ✅ 使用索引第一列
SELECT * FROM orders WHERE customer_id = 123 AND status = 'PAID'; -- ✅ 使用前两列
SELECT * FROM orders WHERE customer_id = 123 AND status = 'PAID' AND create_time >= '2024-01-01'; -- ✅ 完全匹配

-- 无效使用索引的查询:
SELECT * FROM orders WHERE status = 'PAID'; -- ❌ 未使用最左列
SELECT * FROM orders WHERE create_time >= '2024-01-01'; -- ❌ 未使用最左列

索引选择性:选择性高的列适合建索引。选择性 = 不重复值数量 / 总行数。

-- 计算列的选择性
SELECT COUNT(DISTINCT status) / COUNT(*) AS selectivity FROM orders;
-- 结果 > 0.3 适合建索引,< 0.1 不适合

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

  1. 函数操作:WHERE YEAR(create_time) = 2024 → 改为 create_time BETWEEN ‘2024-01-01’ AND ‘2024-12-31’
  2. 隐式类型转换:WHERE phone = 13800138000 (phone是varchar) → 改为 WHERE phone = ‘13800138000’
  3. LIKE前缀模糊查询:WHERE name LIKE ‘%张%’ → 改为 WHERE name LIKE ‘张%’
  4. OR条件:WHERE id = 1 OR id = 2 → 改为 WHERE id IN (1,2)
  5. 负向查询:!=、NOT IN、NOT LIKE 通常无法使用索引

3.4 覆盖索引与延迟关联

覆盖索引:查询的列全部在索引中,避免回表操作。

-- 原始查询(需要回表)
SELECT order_id, customer_id, amount, status FROM orders WHERE customer_id = 123;

-- 创建覆盖索引
ALTER TABLE orders ADD INDEX idx_customer_cover (customer_id, order_id, amount, status);

-- 优化后查询(无需回表)
SELECT order_id, customer_id, amount, status FROM orders WHERE customer_id = 123;

延迟关联:对于需要返回大量列的查询,先通过索引获取主键,再关联获取其他列。

-- 低效查询
SELECT * FROM orders WHERE customer_id = 123 AND amount > 1000;

-- 优化:延迟关联
SELECT t1.* 
FROM orders t1 
JOIN (SELECT order_id FROM orders WHERE customer_id = 123 AND amount > 1000) t2 
ON t1.order_id = t2.order_id;

四、事务与锁优化

4.1 事务设计原则

短事务原则:事务应尽可能短,减少锁持有时间。

// ❌ 错误示例:长事务
@Transactional
public void processOrder(Long orderId) {
    // 1. 查询订单(获取共享锁)
    Order order = orderMapper.selectById(orderId);
    
    // 2. 调用外部API(耗时操作,事务未提交,锁未释放)
    paymentService.verifyPayment(order);
    
    // 3. 更新状态
    order.setStatus("PAID");
    orderMapper.update(order);
}

// ✅ 正确示例:短事务
public void processOrder(Long orderId) {
    // 1. 事务外执行耗时操作
    paymentService.verifyPayment(orderId);
    
    // 2. 缩短事务范围
    try {
        transactionTemplate.execute(status -> {
            Order order = orderMapper.selectById(orderId);
            order.setStatus("PAID");
            orderMapper.update(order);
            return null;
        });
    } catch (Exception e) {
        // 处理异常
    }
}

4.2 乐观锁与悲观锁选择

乐观锁:适合读多写少场景,通过版本号避免更新丢失。

-- 表结构
CREATE TABLE product (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100),
    stock INT,
    version INT DEFAULT 0
);

-- 更新操作
UPDATE product 
SET stock = stock - 1, version = version + 1 
WHERE id = 123 AND version = 2; -- 版本号匹配

-- Java代码实现
public boolean deductStock(Long productId, int quantity) {
    int retry = 3;
    while (retry-- > 0) {
        Product product = productMapper.selectById(productId);
        int newVersion = product.getVersion() + 1;
        int affected = productMapper.updateStock(productId, quantity, product.getVersion(), newVersion);
        if (affected > 0) {
            return true;
        }
        // 短暂休眠后重试
        Thread.sleep(50);
    }
    return false;
}

悲观锁:适合写多读少、强一致性场景。

-- 显式悲观锁
BEGIN;
SELECT * FROM inventory WHERE product_id = 123 FOR UPDATE; -- 排他锁
UPDATE inventory SET stock = stock - 1 WHERE product_id = 123;
COMMIT;

-- 避免间隙锁:使用等值查询
-- ❌ 范围查询会产生间隙锁
SELECT * FROM inventory WHERE product_id BETWEEN 100 AND 200 FOR UPDATE;
-- ✅ 等值查询减少锁范围
SELECT * FROM inventory WHERE product_id = 123 FOR UPDATE;

4.3 死锁预防与处理

死锁常见场景

  1. 交叉更新:A更新1→2,B更新2→1
  2. 索引顺序不一致:不同事务按不同顺序访问数据
  3. 间隙锁冲突:范围更新导致的死锁

预防策略

-- 1. 固定访问顺序
-- 事务1和事务2都按相同顺序访问:先orders后order_items

-- 2. 使用相同索引
-- 所有事务都使用主键或相同索引进行更新

-- 3. 减少间隙锁
-- 使用等值查询代替范围查询
-- 降低事务隔离级别(RC级别间隙锁较少)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

死锁监控与处理

-- 查看最近死锁信息
SHOW ENGINE INNODB STATUS\G

-- 在MySQL 8.0+中启用死锁监控
SET GLOBAL innodb_print_all_deadlocks = ON;

-- 应用层重试机制
public <T> T executeWithRetry(Supplier<T> operation) {
    int maxRetries = 3;
    for (int i = 0; i < maxRetries; i++) {
        try {
            return operation.get();
        } catch (DeadlockException e) {
            if (i == maxRetries - 1) throw e;
            // 指数退避
            try {
                Thread.sleep((long) Math.pow(2, i) * 100);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
    }
    throw new RuntimeException("Max retries exceeded");
}

五、查询缓存与缓冲区优化

5.1 查询缓存(Query Cache)

注意:MySQL 8.0已移除查询缓存功能,但在5.7及以下版本仍有价值。对于读多写少的静态数据,查询缓存能显著提升性能。

-- MySQL 5.7 配置
[mysqld]
query_cache_type = 1  # 0:OFF, 1:ON, 2:DEMAND
query_cache_size = 128M
query_cache_limit = 2M  # 单个查询最大缓存结果集

-- 在SQL中显式使用缓存
SELECT SQL_CACHE * FROM static_table WHERE id = 1;
SELECT SQL_NO_CACHE * FROM dynamic_table WHERE id = 1;

5.2 InnoDB缓冲池(Buffer Pool)

核心参数

[mysqld]
innodb_buffer_pool_size = 24G  # 70-80%物理内存
innodb_buffer_pool_instances = 8  # 多实例减少竞争
innodb_buffer_pool_dump_at_shutdown = 1  # 关闭时dump热数据
innodb_buffer_pool_load_at_startup = 1    # 启动时加载热数据

监控缓冲池命中率

-- 缓冲池命中率应 > 99%
SELECT 
    (1 - (SUM(VARIABLE_VALUE) / @@innodb_buffer_pool_size)) * 100 AS hit_rate
FROM performance_schema.global_status 
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads';

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

5.3 其他关键缓冲区

[mysqld]
# 读缓冲
read_buffer_size = 2M
read_rnd_buffer_size = 4M

# 排序缓冲
sort_buffer_size = 4M

# Join缓冲
join_buffer_size = 4M

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

# 线程栈大小(根据连接数调整)
thread_stack = 256K

六、高并发写入优化

6.1 批量操作

批量插入:减少网络往返和事务开销。

// ❌ 单条插入(N次网络往返)
for (Order order : orderList) {
    orderMapper.insert(order);
}

// ✅ 批量插入(1次网络往返)
public void batchInsert(List<Order> orders) {
    // JDBC批量处理
    String sql = "INSERT INTO orders (order_id, customer_id, amount, status) VALUES (?, ?, ?, ?)";
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql)) {
        
        int batchSize = 0;
        for (Order order : orders) {
            ps.setLong(1, order.getOrderId());
            ps.setLong(2, order.getCustomerId());
            ps.setBigDecimal(3, order.getAmount());
            ps.setString(4, order.getStatus());
            ps.addBatch();
            
            // 每1000条提交一次
            if (++batchSize % 1000 == 0) {
                ps.executeBatch();
            }
        }
        ps.executeBatch(); // 提交剩余
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }
}

批量更新

-- 使用INSERT ON DUPLICATE KEY UPDATE
INSERT INTO orders (order_id, customer_id, amount) 
VALUES (1, 100, 500), (2, 101, 300), (3, 102, 800)
ON DUPLICATE KEY UPDATE 
    amount = VALUES(amount),
    update_time = NOW();

-- 使用CASE WHEN批量更新
UPDATE orders 
SET status = CASE 
    WHEN order_id = 1 THEN 'PAID'
    WHEN order_id = 2 THEN 'SHIPPED'
    WHEN order_id = 3 THEN 'CANCELLED'
END
WHERE order_id IN (1, 2, 3);

6.2 异步写入与队列

消息队列解耦:将写操作异步化,削峰填谷。

// 生产者:将写操作放入队列
public void createOrderAsync(Order order) {
    // 1. 快速返回,不等待数据库写入
    order.setStatus("PENDING");
    orderMapper.insert(order); // 先写入状态
    
    // 2. 发送消息到队列
    rabbitTemplate.convertAndSend("order.create", order);
    
    // 3. 异步处理后续逻辑
    // 消费者会处理支付、库存扣减等
}

// 消费者:批量处理队列消息
@RabbitListener(queues = "order.create")
public void processOrderCreate(Order order) {
    // 批量处理:累积一定数量后批量提交
    List<Order> batch = orderBuffer.getAndAdd(order);
    if (batch.size() >= 100) {
        batchInsert(batch);
        orderBuffer.clear();
    }
}

6.3 分区表(Partitioning)

对于大数据量表,使用分区可以提升查询性能和管理效率。

-- 按时间范围分区(适合日志、订单等时间序列数据)
CREATE TABLE orders (
    order_id BIGINT,
    customer_id BIGINT,
    amount DECIMAL(10,2),
    create_time DATETIME,
    status VARCHAR(20),
    PRIMARY KEY (order_id, create_time) -- 分区键必须包含在主键中
) PARTITION BY RANGE (YEAR(create_time)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 按哈希分区(适合均匀分布)
CREATE TABLE user_log (
    id BIGINT,
    user_id BIGINT,
    log_time DATETIME
) PARTITION BY HASH(user_id) PARTITIONS 16;

-- 查看分区信息
SELECT PARTITION_NAME, TABLE_ROWS 
FROM INFORMATION_SCHEMA.PARTITIONS 
WHERE TABLE_NAME = 'orders';

七、读写分离与分库分表

7.1 主从复制配置

主库配置(my.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  # 保证ACID

从库配置

[mysqld]
server-id = 2
relay_log = mysql-relay-bin
read_only = 1  # 只读模式,防止误写

创建复制用户

-- 在主库执行
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

-- 在从库执行
CHANGE MASTER TO
MASTER_HOST='master_ip',
MASTER_USER='repl',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=1234;

START SLAVE;
SHOW SLAVE STATUS\G  -- 检查Slave_IO_Running和Slave_SQL_Running为Yes

7.2 读写分离实现

ShardingSphere-JDBC配置

# 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:3306/mydb
        username: root
        password: password
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave0:3306/mydb
        username: root
        password: password
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave1:3306/mydb
        username: root
        password: password
    
    rules:
      readwrite-splitting:
        data-sources:
          mydb:
            type: Static
            props:
              write-data-source-name: master
              read-data-source-names: slave0,slave1
              load-balance-type: ROUND_ROBIN

7.3 分库分表(Sharding)

垂直分库:按业务模块拆分数据库。

原库:mydb (user, order, product, payment)
拆分后:
- user_db: user, user_profile
- order_db: order, order_item
- product_db: product, category
- payment_db: payment, refund

水平分表:按数据特征拆分大表。

-- 按用户ID取模分表(16张表)
-- order_0, order_1, ..., order_15

-- ShardingSphere配置
spring:
  shardingsphere:
    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: mydb.order_$->{0..15}
            table-strategy:
              standard:
                sharding-column: customer_id
                sharding-algorithm-name: mod
        sharding-algorithms:
          mod:
            type: MOD
            props:
              sharding-count: 16

八、高并发配置调优

8.1 InnoDB核心参数

[mysqld]
# 1. 缓冲池(最重要)
innodb_buffer_pool_size = 24G  # 70-80%内存
innodb_buffer_pool_instances = 8  # 多实例减少竞争

# 2. 日志文件
innodb_log_file_size = 2G      # 重做日志大小,建议1-2G
innodb_log_buffer_size = 64M   # 日志缓冲区
innodb_flush_log_at_trx_commit = 1  # 1:安全, 2:性能更好但可能丢数据

# 3. I/O相关
innodb_flush_method = O_DIRECT  # 绕过OS缓存,直接I/O
innodb_io_capacity = 2000       # SSD建议2000-5000
innodb_io_capacity_max = 4000   # 最大IOPS

# 4. 并发控制
innodb_thread_concurrency = 0   # 0:自动管理,建议0
innodb_read_io_threads = 8
innodb_write_io_threads = 8

# 5. 锁相关
innodb_lock_wait_timeout = 50   # 锁等待超时
innodb_rollback_on_timeout = 1  # 超时回滚整个事务

8.2 连接与线程参数

[mysqld]
# 连接数
max_connections = 2000
thread_cache_size = 100
back_log = 500

# 超时设置
wait_timeout = 600  # 非交互连接超时,避免连接泄漏
interactive_timeout = 600

# 内存相关
tmp_table_size = 64M
max_heap_table_size = 64M
sort_buffer_size = 4M
join_buffer_size = 4M
read_buffer_size = 2M
read_rnd_buffer_size = 4M

8.3 日志与监控参数

[mysqld]
# 慢查询日志
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1  # 记录超过1秒的查询
log_queries_not_using_indexes = 1

# 错误日志
log_error = /var/log/mysql/error.log

# 通用查询日志(调试用,生产环境关闭)
general_log = 0
general_log_file = /var/log/mysql/general.log

# 性能监控
performance_schema = ON  # MySQL 5.6+ 默认开启

九、高并发场景下的监控与预警

9.1 关键监控指标

连接数监控

-- 当前连接数 vs 最大连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';

-- 计算使用率
SELECT 
    (VARIABLE_VALUE / @@max_connections) * 100 AS usage_rate
FROM performance_schema.global_status 
WHERE VARIABLE_NAME = 'Threads_connected';

QPS/TPS监控

-- 计算每秒查询数和事务数
SELECT 
    (VARIABLE_VALUE - @last_queries) / TIMESTAMPDIFF(SECOND, @last_time, NOW()) AS qps,
    (VARIABLE_VALUE - @last_committed) / TIMESTAMPDIFF(SECOND, @last_time, NOW()) AS tps
FROM performance_schema.global_status 
WHERE VARIABLE_NAME IN ('Queries', 'Com_commit');

-- 使用Prometheus + Grafana监控
# mysqld_exporter配置
--collect.global_status
--collect.info_schema.innodb_metrics
--collect.perf_schema.tablelocks
--collect.perf_schema.eventsstatements

慢查询监控

-- 查看最近慢查询
SELECT 
    DIGEST_TEXT,
    COUNT_STAR,
    AVG_TIMER_WAIT/1000000000000 AS avg_time_sec,
    MAX_TIMER_WAIT/1000000000000 AS max_time_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE AVG_TIMER_WAIT > 1000000000000  -- 1秒
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;

9.2 锁监控

-- 查看当前锁等待
SELECT 
    r.trx_id waiting_trx_id,
    r.trx_mysql_thread_id waiting_thread,
    r.trx_query waiting_query,
    b.trx_id blocking_trx_id,
    b.trx_mysql_thread_id blocking_thread,
    b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

-- 查看MDL锁(元数据锁)
SELECT 
    ps.id AS processlist_id,
    ps.user,
    ps.host,
    ps.db,
    ps.command,
    ps.time,
    ps.state,
    ps.info
FROM performance_schema.metadata_locks ml
JOIN performance_schema.threads t ON ml.OWNER_THREAD_ID = t.THREAD_ID
JOIN information_schema.processlist ps ON t.PROCESSLIST_ID = ps.id
WHERE ml.OBJECT_SCHEMA = 'mydb' AND ml.OBJECT_NAME = 'orders';

9.3 性能监控工具

Percona Toolkit

# 安装
sudo apt-get install percona-toolkit

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

# 监控复制延迟
pt-heartbeat --check h=master,u=root,p=password --master-server-id=1

# 在线DDL工具
pt-online-schema-change --alter "ADD INDEX idx_status (status)" D=mydb,t=orders --execute

Prometheus + mysqld_exporter

# docker-compose.yml
version: '3'
services:
  mysqld_exporter:
    image: prom/mysqld-exporter
    environment:
      DATA_SOURCE_NAME: "user:password@(mysql:3306)/"
    ports:
      - "9104:9104"
  
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

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

10.1 问题分析

场景:10000 QPS秒杀,库存100件,热点数据更新。

问题

  1. 库存行锁竞争激烈
  2. 大量请求导致连接耗尽
  3. 数据库CPU飙升至100%

10.2 优化方案

架构层

// 1. 缓存预热与库存扣减
public class SeckillService {
    private static final String STOCK_KEY = "seckill:stock:";
    private static final String ORDER_KEY = "seckill:order:";
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private OrderMapper orderMapper;
    
    // 预热:将库存加载到Redis
    public void preloadStock(Long productId, int stock) {
        redisTemplate.opsForValue().set(STOCK_KEY + productId, String.valueOf(stock));
    }
    
    // 秒杀下单
    public Result seckill(Long productId, Long userId) {
        String lockKey = "seckill:lock:" + productId;
        
        // 1. Redis预扣库存(Lua脚本保证原子性)
        String luaScript = 
            "if redis.call('exists', KEYS[1]) == 1 then " +
            "   local stock = tonumber(redis.call('get', KEYS[1])); " +
            "   if stock > 0 then " +
            "       redis.call('decr', KEYS[1]); " +
            "       return 1; " +
            "   end; " +
            "end; " +
            "return 0;";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(STOCK_KEY + productId)
        );
        
        if (result == 0) {
            return Result.fail("库存不足");
        }
        
        // 2. 异步创建订单
        String orderId = IdUtil.randomUUID();
        rabbitTemplate.convertAndSend("seckill.order", 
            new SeckillMessage(userId, productId, orderId));
        
        return Result.success(orderId);
    }
    
    // 异步消费:批量写入数据库
    @RabbitListener(queues = "seckill.order")
    public void processSeckillOrder(List<SeckillMessage> batch) {
        // 批量插入订单
        List<Order> orders = batch.stream()
            .map(m -> new Order(m.getOrderId(), m.getUserId(), m.getProductId()))
            .collect(Collectors.toList());
        
        orderMapper.batchInsert(orders);
        
        // 批量更新库存(在业务低峰期)
        if (batch.size() >= 100) {
            updateStockBatch(batch);
        }
    }
}

数据库层

-- 1. 库存表使用乐观锁
CREATE TABLE stock (
    product_id BIGINT PRIMARY KEY,
    stock INT,
    version INT DEFAULT 0
);

-- 2. 订单表分表(按用户ID取模)
CREATE TABLE order_0 (
    order_id VARCHAR(50) PRIMARY KEY,
    user_id BIGINT,
    product_id BIGINT,
    create_time DATETIME
) PARTITION BY HASH(user_id) PARTITIONS 16;

-- 3. 关闭MySQL查询缓存(写多读少)
SET GLOBAL query_cache_size = 0;

配置优化

[mysqld]
# 针对秒杀场景优化
innodb_buffer_pool_size = 32G
innodb_log_file_size = 4G
innodb_flush_log_at_trx_commit = 2  # 性能优先,可能丢1秒数据
max_connections = 3000
thread_cache_size = 200

10.3 效果对比

指标 优化前 优化后
平均响应时间 850ms 45ms
成功率 65% 99.5%
数据库CPU 100% 35%
连接数峰值 1500 800

十一、常见误区与最佳实践

11.1 常见误区

  1. 盲目增大max_connections:不解决根本问题,反而增加内存消耗
  2. 过度使用索引:索引越多,写入越慢,维护成本越高
  3. 长事务:持有锁时间过长,阻塞其他操作
  4. *SELECT **:返回不需要的列,增加I/O和网络开销
  5. 忽视从库延迟:读写分离时从库数据可能滞后

11.2 最佳实践清单

  • [ ] 使用连接池,合理设置池大小
  • [ ] 所有查询必须使用EXPLAIN分析
  • [ ] 事务尽可能短,避免事务内调用外部服务
  • [ ] 高并发写入使用批量操作+异步队列
  • [ ] 热点数据使用Redis缓存
  • [ ] 监控慢查询,定期优化
  • [ ] 主从复制开启GTID,便于故障恢复
  • [ ] 定期备份并测试恢复流程
  • [ ] 使用Percona Toolkit定期分析
  • [ ] 建立完善的监控预警体系

十二、总结

MySQL高并发优化是一个系统工程,需要从连接层、SQL层、事务层、架构层多维度入手。核心原则是:减少资源竞争、缩短操作时间、分散系统压力

关键要点

  1. 连接优化:连接池+读写分离,避免连接耗尽
  2. SQL优化:合理索引+覆盖索引,减少I/O
  3. 事务优化:短事务+乐观锁,减少锁竞争
  4. 架构优化:缓存+队列+分库分表,分散压力
  5. 监控预警:实时监控+快速响应,防患于未然

记住,没有银弹。每个业务场景都有其特殊性,需要根据实际压力测试结果,持续监控和调优,才能构建稳定高效的MySQL高并发系统。