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

在当今互联网应用中,高并发场景已经成为常态。无论是电商大促、社交网络热点事件,还是金融交易高峰期,MySQL数据库都面临着前所未有的压力。高并发环境下,数据库可能面临连接数耗尽、CPU飙升、I/O瓶颈、锁竞争激烈等问题,导致响应延迟甚至服务不可用。

本文将从架构优化和索引调优两个核心维度,深入探讨MySQL应对高并发流量的完整策略。我们将结合实际案例,详细说明如何通过分层架构设计、读写分离、分库分表等手段提升系统吞吐量,以及如何通过索引优化、查询优化等技术降低单点压力。这些策略不仅能帮助您应对短期流量洪峰,更能为系统长期稳定运行奠定基础。

一、架构优化:构建可扩展的数据库体系

1.1 读写分离架构设计

读写分离是应对高并发最基础且有效的架构优化手段。其核心思想是将读操作和写操作分离到不同的数据库实例,通过主从复制实现数据同步,从而分散单实例压力。

1.1.1 主从复制原理与配置

MySQL主从复制基于二进制日志(Binary Log)实现。主库将变更记录到binlog,从库通过I/O线程读取并重放这些日志。

主库配置示例(my.cnf):

[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
expire_logs_days = 7

从库配置示例:

[mysqld]
server-id = 2
relay_log = /var/log/mysql/mysql-relay-bin.log
log_bin = /var/log/mysql/mysql-bin.log
read_only = 1

创建复制用户:

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

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

START SLAVE;

1.1.2 应用层路由策略

应用层需要智能地将读写请求路由到不同实例。常见实现方式包括:

基于Spring Boot的动态数据源配置:

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource routingDataSource() {
        DynamicDataSource routingDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());
        return routingDataSource;
    }
}

// 线程上下文保持数据源类型
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static void setDataSource(String type) {
        contextHolder.set(type);
    }
    
    public static String getDataSource() {
        return contextHolder.get() == null ? "master" : contextHolder.get();
    }
    
    public static void clear() {
        contextHolder.remove();
    }
}

// AOP切面实现自动路由
@Aspect
@Component
public class DataSourceAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void before(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        if (methodName.startsWith("get") || methodName.startsWith("list") || methodName.startsWith("query")) {
            DataSourceContextHolder.setDataSource("slave");
        } else {
            DataSourceContextHolder.setDataSource("master");
        }
    }
    
    @After("execution(* com.example.service.*.*(..))")
    public void after(JoinPoint joinPoint) {
        DataSourceContextHolder.clear();
    }
}

1.1.3 读写分离的注意事项

  1. 数据延迟问题:主从复制存在延迟,对于强一致性要求的读操作(如支付后立即查询订单状态),必须强制走主库。
  2. 事务内读操作:在事务中执行的读操作不能路由到从库,否则可能导致脏读。
  3. 主从切换:需要实现故障自动转移机制,当主库宕机时,从库能快速提升为主库。

1.2 数据库分片(Sharding)策略

当单库容量或性能达到瓶颈时,分片是终极解决方案。分片将数据分布到多个物理数据库,每个分片只包含部分数据。

1.2.1 垂直分片 vs 水平分片

垂直分片:按业务模块拆分表到不同数据库。

  • 优点:业务清晰,易于维护
  • 缺点:无法解决单表数据量过大问题

水平分片:将同一张表的数据按规则拆分到多个数据库。

  • 优点:理论上无限扩展
  • 缺点:跨分片查询复杂

1.2.2 水平分片实战:按用户ID取模

假设我们有4个分片库(db0, db1, db2, db3),分片规则为 user_id % 4。

分片表结构(每个分片库相同):

CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    amount DECIMAL(10,2),
    create_time DATETIME,
    INDEX idx_user_id (user_id)
) ENGINE=InnoDB;

应用层分片路由实现:

public class ShardingRouter {
    private static final int SHARD_COUNT = 4;
    
    // 根据user_id计算分片库索引
    public static int getShardIndex(Long userId) {
        return (int) (userId % SHARD_COUNT);
    }
    
    // 获取数据源名称
    public static String getDataSourceName(Long userId) {
        return "db" + getShardIndex(userId);
    }
    
    // 执行查询示例
    public List<Order> getOrdersByUserId(Long userId) {
        String dataSource = getDataSourceName(userId);
        // 切换到对应数据源
        DataSourceContextHolder.setDataSource(dataSource);
        
        // 执行查询(SQL中不需要包含分片条件)
        String sql = "SELECT * FROM orders WHERE user_id = ?";
        // ... 执行查询
        return orders;
    }
}

分片后的查询限制

  • 跨分片查询需要应用层合并结果
  • 分片键上的查询效率最高
  • 非分片键查询需要扫描所有分片

1.2.3 分片中间件选型

对于复杂业务,建议使用成熟的分片中间件:

  • ShardingSphere:功能强大,支持JDBC和Proxy模式
  • Vitess:YouTube开源,适合超大规模场景
  • MyCat:国产中间件,社区活跃

1.3 连接池优化

连接池是应用与数据库之间的缓冲层,合理配置能显著提升高并发下的稳定性。

1.3.1 HikariCP配置详解

HikariCP是目前性能最优的连接池,Spring Boot 2.x默认使用。

生产级配置示例(application.yml):

spring:
  datasource:
    hikari:
      # 连接池名称
      pool-name: ProductionHikariCP
      # 最小空闲连接数
      minimum-idle: 20
      # 最大连接数(根据并发量调整)
      maximum-pool-size: 100
      # 连接超时时间(毫秒)
      connection-timeout: 30000
      # 空闲连接存活时间(毫秒)
      idle-timeout: 600000
      # 连接最大生命周期(毫秒)
      max-lifetime: 1800000
      # 连接测试查询
      connection-test-query: SELECT 1
      # 是否启用缓存预编译语句
      cache-prep-statements: true
      # 预编译语句缓存大小
      prep-stmt-cache-size: 250
      # 预编译语句最大长度
      prep-stmt-cache-sql-limit: 2048

1.3.2 连接池监控指标

关键监控指标:

  • 活跃连接数:接近maximum-pool-size时需扩容
  • 等待连接数:持续大于0说明连接池不足
  • 连接获取平均时间:超过100ms需要优化

HikariCP监控代码示例:

@Component
public class ConnectionPoolMonitor {
    
    @Autowired
    private DataSource dataSource;
    
    public void printPoolStatus() {
        HikariDataSource hikariDS = (HikariDataSource) dataSource;
        HikariPoolMXBean poolMXBean = hikariDS.getHikariPoolMXBean();
        
        System.out.println("活跃连接数: " + poolMXBean.getActiveConnections());
        System.out.println("空闲连接数: " + poolMXBean.getIdleConnections());
        System.out.println("等待连接数: " + poolMXBean.getThreadsAwaitingConnection());
        System.out.println("总连接数: " + poolMXBean.getTotalConnections());
    }
}

1.4 缓存层引入

引入Redis等缓存层,减少直接访问MySQL的次数。

1.4.1 缓存策略设计

Cache-Aside模式:

public Order getOrder(Long orderId) {
    // 1. 先查缓存
    String cacheKey = "order:" + orderId;
    String cached = redisTemplate.opsForValue().get(cacheKey);
    if (cached != null) {
        return JSON.parseObject(cached, Order.class);
    }
    
    // 2. 缓存未命中,查数据库
    Order order = orderMapper.selectById(orderId);
    if (order != null) {
        // 3. 写入缓存,设置合理TTL
        redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(order), 30, TimeUnit.MINUTES);
    }
    return order;
}

public void updateOrder(Order order) {
    // 1. 更新数据库
    orderMapper.updateById(order);
    
    // 2. 删除缓存(避免脏数据)
    String cacheKey = "order:" + order.getOrderId();
    redisTemplate.delete(cacheKey);
}

1.4.2 缓存穿透与雪崩防护

布隆过滤器防止缓存穿透:

@Component
public class BloomFilterCache {
    private BloomFilter<Long> bloomFilter;
    
    @PostConstruct
    public void init() {
        // 初始化布隆过滤器,预期插入100万条数据,误判率0.01%
        bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.0001);
        
        // 加载现有ID到布隆过滤器
        List<Long> allIds = orderMapper.getAllIds();
        allIds.forEach(bloomFilter::put);
    }
    
    public Order getOrder(Long orderId) {
        // 先判断布隆过滤器
        if (!bloomFilter.mightContain(orderId)) {
            return null; // 肯定不存在
        }
        
        // 再走正常缓存流程
        return getFromCacheOrDB(orderId);
    }
}

二、索引优化:让查询飞起来

2.1 索引设计原则

索引是提升查询性能的核心武器,但不当的索引会成为写操作的负担。

2.1.1 最左前缀原则

对于复合索引 (a, b, c),以下查询能用到索引:

-- 能用到索引
SELECT * FROM table WHERE a = 1;
SELECT * FROM table WHERE a = 1 AND b = 2;
SELECT * FROM table WHERE a = 1 AND b = 2 AND c = 3;

-- 能用到部分索引(a列)
SELECT * FROM table WHERE a = 1 AND c = 3;

-- 无法使用索引
SELECT * FROM table WHERE b = 2;
SELECT * FROM table WHERE b = 2 AND c = 3;

2.1.2 覆盖索引优化

覆盖索引指查询所需的所有列都包含在索引中,无需回表查询。

案例:订单表查询优化

-- 原始查询(需要回表)
SELECT order_id, user_id, amount, status FROM orders 
WHERE user_id = 12345 AND status = 'PAID';

-- 创建覆盖索引
CREATE INDEX idx_user_status_cover ON orders(user_id, status, order_id, amount);

-- 执行计划显示Using index,性能提升10倍以上
EXPLAIN SELECT order_id, user_id, amount, status FROM orders 
WHERE user_id = 12345 AND status = 'PAID';

2.1.3 索引选择性原则

选择性 = 不重复的索引值 / 总行数。选择性越高,索引效果越好。

-- 计算列的选择性
SELECT 
    COUNT(DISTINCT status) / COUNT(*) AS status_selectivity,
    COUNT(DISTINCT user_id) / COUNT(*) AS user_id_selectivity,
    COUNT(DISTINCT create_time) / COUNT(*) AS time_selectivity
FROM orders;

-- 结果示例:
-- status_selectivity: 0.05 (低选择性,不适合单独建索引)
-- user_id_selectivity: 0.8 (高选择性,适合建索引)
-- time_selectivity: 0.95 (高选择性,适合建索引)

2.2 慢查询分析与优化

2.2.1 慢查询日志配置

my.cnf配置:

[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
min_examined_row_limit = 1000  # 至少检查1000行才记录

2.2.2 使用pt-query-digest分析

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

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

# 输出示例:
# Overall: 123 total, 5.23 avg, 0.12s avg time
# Rank Query ID           Response time    Calls R/Call V/M   Item
# ==== ================== ================ ===== ====== ===== ====
#    1 0xF9A57DD5A4C...   45.2345 36.8%   12   3.7695  0.12 SELECT orders
#    2 0xB4C27D5A4C...    12.3456 10.0%    8   1.5432  0.08 SELECT users

2.2.3 慢查询优化实战

案例:电商订单列表查询优化

优化前(慢查询):

-- 原始SQL,执行时间3.2秒
SELECT 
    o.order_id,
    o.order_no,
    u.user_name,
    o.amount,
    o.status,
    o.create_time
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE o.status IN ('PAID', 'SHIPPED', 'COMPLETED')
  AND o.create_time >= '2024-01-01 00:00:00'
  AND o.create_time < '2024-01-31 23:59:59'
ORDER BY o.create_time DESC
LIMIT 0, 20;

-- 执行计划
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| 1  | SIMPLE      | o     | ALL  | NULL          | NULL | NULL    | NULL | 1234567 | Using where |
| 1  | SIMPLE      | u     | eq_ref | PRIMARY     | PRIMARY | 8     | o.user_id | 1 | NULL |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+

优化步骤:

  1. 分析问题:全表扫描,没有索引,排序消耗大
  2. 创建索引
-- 创建复合索引(包含WHERE条件和ORDER BY字段)
CREATE INDEX idx_status_create_time ON orders(status, create_time);

-- 如果查询字段较多,创建覆盖索引
CREATE INDEX idx_status_create_time_cover ON orders(
    status, 
    create_time,
    order_id,
    order_no,
    user_id,
    amount
);
  1. 优化SQL
-- 优化后SQL,执行时间0.05秒
SELECT 
    o.order_id,
    o.order_no,
    u.user_name,
    o.amount,
    o.status,
    o.create_time
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE o.status IN ('PAID', 'SHIPPED', 'COMPLETED')
  AND o.create_time >= '2024-01-01 00:00:00'
  AND o.create_time < '2024-01-31 23:59:59'
ORDER BY o.create_time DESC
LIMIT 0, 20;

-- 执行计划
+----+-------------+-------+-------+----------------------------------+----------------------------+---------+------+---------+-------------+
| id | select_type | table | type  | possible_keys                    | key                        | key_len | ref  | rows    | Extra       |
+----+-------------+-------+-------+----------------------------------+----------------------------+---------+------+---------+-------------+
| 1  | SIMPLE      | o     | range | idx_status_create_time_cover     | idx_status_create_time_cover | 15      | NULL | 5000    | Using where; Using index; Using filesort |
| 1  | SIMPLE      | u     | eq_ref | PRIMARY                          | PRIMARY                    | 8       | o.user_id | 1 | NULL |
+----+-------------+-------+-------+----------------------------------+----------------------------+---------+------+---------+-------------+
  1. 进一步优化:消除filesort
-- 如果create_time是唯一确定顺序的,可以省略status字段
CREATE INDEX idx_create_time_cover ON orders(
    create_time,
    order_id,
    order_no,
    user_id,
    amount,
    status
);

-- 修改查询条件顺序匹配索引
WHERE o.create_time >= '2024-01-01 00:00:00'
  AND o.create_time < '2024-01-31 23:59:59'
  AND o.status IN ('PAID', 'SHIPPED', 'COMPLETED')

2.3 索引维护与监控

2.3.1 索引使用情况分析

-- 查看索引使用频率(需要开启统计信息)
SELECT 
    OBJECT_SCHEMA,
    OBJECT_NAME,
    INDEX_NAME,
    COUNT_FETCH,
    COUNT_INSERT,
    COUNT_UPDATE,
    COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'your_database'
ORDER BY COUNT_FETCH DESC;

-- 查找未使用的索引(可能可以删除)
SELECT 
    OBJECT_SCHEMA,
    OBJECT_NAME,
    INDEX_NAME
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE INDEX_NAME IS NOT NULL
  AND COUNT_STAR = 0
  AND OBJECT_SCHEMA != 'mysql';

2.3.2 索引碎片整理

-- 查看表碎片率
SELECT 
    table_name,
    ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_mb,
    ROUND(data_free / 1024 / 1024, 2) AS free_mb,
    ROUND(data_free / (data_length + index_length) * 100, 2) AS fragment_ratio
FROM information_schema.tables
WHERE table_schema = 'your_database'
  AND data_free > 1024 * 1024 * 10; -- 超过10MB碎片

-- 重建索引(在线DDL,MySQL 5.6+)
ALTER TABLE orders ENGINE=InnoDB;

-- 或者单独重建索引(MySQL 5.7+)
ALTER TABLE orders DROP INDEX idx_old, ADD INDEX idx_new (column);

三、查询优化:SQL语句的艺术

3.1 避免索引失效的常见陷阱

3.1.1 函数和计算导致索引失效

-- 错误示例:索引失效
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-15';
SELECT * FROM orders WHERE amount / 100 > 100;

-- 正确示例:使用范围查询
SELECT * FROM orders 
WHERE create_time >= '2024-01-15 00:00:00' 
  AND create_time < '2024-01-16 00:00:00';

SELECT * FROM orders WHERE amount > 10000; -- 假设amount单位是分

3.1.2 隐式类型转换

-- 假设user_id是VARCHAR类型,但索引在user_id上
-- 错误:索引失效
SELECT * FROM users WHERE user_id = 12345; -- 数字传入

-- 正确:使用正确类型
SELECT * FROM users WHERE user_id = '12345';

3.1.3 LIKE查询优化

-- 全模糊查询无法使用索引
SELECT * FROM users WHERE name LIKE '%张%'; -- 索引失效

-- 前缀模糊查询可以使用索引
SELECT * FROM users WHERE name LIKE '张%'; -- 索引有效

-- 后缀模糊查询解决方案:反转字段+索引
ALTER TABLE users ADD COLUMN name_reverse VARCHAR(100);
UPDATE users SET name_reverse = REVERSE(name);
CREATE INDEX idx_name_reverse ON users(name_reverse);

-- 查询后缀"张"
SELECT * FROM users WHERE name_reverse LIKE REVERSE('%张') || '%';

3.2 大表查询优化技巧

3.2.1 分页查询优化

传统分页的问题:

-- 当offset很大时,性能极差
SELECT * FROM orders ORDER BY create_time DESC LIMIT 1000000, 20;

优化方案1:延迟关联

-- 先查主键,再关联详情
SELECT o.*
FROM orders o
INNER JOIN (
    SELECT order_id
    FROM orders
    ORDER BY create_time DESC
    LIMIT 1000000, 20
) AS tmp ON o.order_id = tmp.order_id;

优化方案2:记录上次位置

-- 第一页
SELECT * FROM orders 
WHERE create_time <= '2024-01-31 23:59:59'
ORDER BY create_time DESC 
LIMIT 20;

-- 第二页(传入上一页最后一条的时间)
SELECT * FROM orders 
WHERE create_time < '2024-01-31 12:00:00' -- 上一页最后一条的时间
ORDER BY create_time DESC 
LIMIT 20;

3.2.2 IN vs EXISTS

-- 小表驱动大表用IN
SELECT * FROM orders 
WHERE user_id IN (SELECT user_id FROM users WHERE status = 'VIP');

-- 大表驱动小表用EXISTS
SELECT * FROM orders o
WHERE EXISTS (
    SELECT 1 FROM users u 
    WHERE u.user_id = o.user_id 
    AND u.status = 'VIP'
);

3.3 执行计划分析

3.3.1 EXPLAIN详解

EXPLAIN SELECT o.*, u.user_name 
FROM orders o 
INNER JOIN users u ON o.user_id = u.user_id 
WHERE o.status = 'PAID' 
ORDER BY o.create_time DESC;

-- 输出解读:
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1  | SIMPLE      | o     | ref  | idx_status    | idx_status | 1     | const | 5000 | Using where; Using filesort |
| 1  | SIMPLE      | u     | eq_ref | PRIMARY      | PRIMARY    | 8     | o.user_id | 1 | NULL |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+

关键字段解读:

  • type:访问类型(ALL > index > range > ref > eq_ref > const > system)
  • possible_keys:可能使用的索引
  • key:实际使用的索引
  • rows:预估扫描行数
  • Extra:额外信息(Using index, Using filesort, Using temporary)

3.3.2 使用EXPLAIN ANALYZE(MySQL 8.0+)

-- MySQL 8.0+提供实际执行统计
EXPLAIN ANALYZE SELECT * FROM orders WHERE status = 'PAID';

-- 输出示例:
-> Filter: (o.status = 'PAID')  (cost=123.45 rows=5000) (actual time=0.123..45.678 rows=5000 loops=1)
    -> Index range scan on o using idx_status  (cost=123.45 rows=5000) (actual time=0.123..34.567 rows=5000 loops=1)

四、高级优化策略

4.1 事务与锁优化

4.1.1 减少锁持有时间

// 优化前:长事务,锁持有时间长
@Transactional
public void updateOrder(Long orderId) {
    // 1. 查询订单(加锁)
    Order order = orderMapper.selectById(orderId);
    
    // 2. 调用外部服务(耗时)
    paymentService.verifyPayment(order);
    
    // 3. 更新状态
    order.setStatus("PAID");
    orderMapper.update(order);
}

// 优化后:拆分事务,减少锁时间
public void updateOrder(Long orderId) {
    // 1. 不在事务内调用外部服务
    paymentService.verifyPayment(orderId);
    
    // 2. 短事务内只做数据库操作
    updateOrderStatus(orderId, "PAID");
}

@Transactional
public void updateOrderStatus(Long orderId, String status) {
    orderMapper.updateStatus(orderId, status);
}

4.1.2 乐观锁避免死锁

-- 表结构增加版本号
CREATE TABLE inventory (
    product_id BIGINT PRIMARY KEY,
    stock INT,
    version INT DEFAULT 0
);

-- 更新时使用乐观锁
UPDATE inventory 
SET stock = stock - 1, version = version + 1
WHERE product_id = 123 AND version = 5; -- 版本号必须匹配

-- 检查影响行数,如果为0说明被其他事务修改,需要重试

4.2 统计信息优化

4.2.1 手动更新统计信息

-- 对于数据变化大的表,手动更新统计信息
ANALYZE TABLE orders;

-- 查看统计信息
SELECT 
    table_name,
    table_rows,
    avg_row_length,
    data_length,
    index_length
FROM information_schema.tables
WHERE table_schema = 'your_database';

4.2.2 索引直方图(MySQL 8.0+)

-- 为倾斜数据创建直方图
ANALYZE TABLE orders UPDATE HISTOGRAM ON status WITH 1024 BUCKETS;

-- 查看直方图
SELECT 
    column_name,
    histogram
FROM information_schema.column_statistics
WHERE table_name = 'orders';

4.3 并发控制与限流

4.3.1 数据库层面限流

-- 使用MySQL的GET_LOCK实现应用级限流
SELECT GET_LOCK('order_create_12345', 0); -- 立即返回,不等待

-- 业务逻辑处理
-- ...

SELECT RELEASE_LOCK('order_create_12345');

4.3.2 令牌桶算法限流(应用层)

@Component
public class DatabaseRateLimiter {
    private final RateLimiter rateLimiter = RateLimiter.create(100.0); // 每秒100个请求
    
    public void executeWithLimit(Runnable task) {
        if (rateLimiter.tryAcquire()) {
            task.run();
        } else {
            throw new RateLimitException("数据库请求过于频繁,请稍后重试");
        }
    }
}

五、监控与告警体系

5.1 关键监控指标

5.1.1 性能指标

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

-- 查看慢查询数量
SHOW STATUS LIKE 'Slow_queries';

-- 查看InnoDB缓冲池命中率(应>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';

5.1.2 资源使用指标

# 监控MySQL CPU使用率
mysqladmin -u root -p extended-status | grep -E "Threads_running|Queries_per_second"

# 监控磁盘I/O
iostat -x 1 | grep -E "sda|sdb"

5.2 自动化告警

5.2.1 Prometheus + Grafana监控

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"

关键告警规则:

# alert_rules.yml
groups:
- name: mysql_alerts
  rules:
  - alert: MySQLTooManyConnections
    expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections * 100 > 80
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "MySQL连接数过高"
      
  - alert: MySQLSlowQueries
    expr: rate(mysql_global_status_slow_queries[5m]) > 5
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "MySQL慢查询激增"

六、实战案例:应对秒杀场景

6.1 秒杀场景特点

  • 瞬时高并发:QPS可达数十万
  • 库存扣减:需要强一致性
  • 读多写少:大量查询请求

6.2 完整解决方案

6.2.1 架构设计

用户请求 → Nginx → 应用服务 → Redis缓存 → 消息队列 → MySQL

6.2.2 核心代码实现

@Service
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    private static final String STOCK_KEY = "seckill:stock:";
    private static final String ORDER_KEY = "seckill:order:";
    
    /**
     * 秒杀下单
     */
    public Result seckill(Long productId, Long userId) {
        // 1. 参数校验
        if (userId == null || productId == null) {
            return Result.error("参数错误");
        }
        
        // 2. 预扣减库存(Redis原子操作)
        String stockKey = STOCK_KEY + productId;
        Long stock = redisTemplate.opsForValue().decrement(stockKey);
        
        if (stock == null || stock < 0) {
            redisTemplate.opsForValue().increment(stockKey); // 回滚
            return Result.error("库存不足");
        }
        
        // 3. 判断是否已下单
        String orderKey = ORDER_KEY + userId + ":" + productId;
        if (redisTemplate.hasKey(orderKey)) {
            return Result.error("您已参与过秒杀");
        }
        
        // 4. 发送消息到队列,异步创建订单
        SeckillMessage message = new SeckillMessage(userId, productId);
        rabbitTemplate.convertAndSend("seckill.order", message);
        
        // 5. 标记已下单(防止重复)
        redisTemplate.opsForValue().set(orderKey, "1", 30, TimeUnit.MINUTES);
        
        return Result.success("秒杀成功,订单处理中");
    }
    
    /**
     * 异步处理订单(消费者)
     */
    @RabbitListener(queues = "seckill.order")
    public void processSeckillOrder(SeckillMessage message) {
        try {
            // 1. 创建订单(数据库操作)
            Order order = new Order();
            order.setOrderId(generateOrderId());
            order.setUserId(message.getUserId());
            order.setProductId(message.getProductId());
            order.setStatus("CREATED");
            order.setCreateTime(new Date());
            
            orderMapper.insert(order);
            
            // 2. 记录秒杀结果
            redisTemplate.opsForValue().set("seckill:result:" + message.getUserId() + ":" + message.getProductId(), 
                                           order.getOrderId(), 1, TimeUnit.HOURS);
            
        } catch (Exception e) {
            // 异常处理:库存回滚
            redisTemplate.opsForValue().increment(STOCK_KEY + message.getProductId());
            log.error("秒杀订单处理失败", e);
        }
    }
}

6.2.3 数据库表设计优化

-- 秒杀商品表
CREATE TABLE seckill_product (
    product_id BIGINT PRIMARY KEY,
    stock INT NOT NULL,
    start_time DATETIME,
    end_time DATETIME,
    version INT DEFAULT 0,  -- 乐观锁版本
    INDEX idx_time (start_time, end_time)
) ENGINE=InnoDB;

-- 秒杀订单表(分表)
CREATE TABLE seckill_order_0 (
    order_id BIGINT PRIMARY KEY,
    user_id BIGINT,
    product_id BIGINT,
    amount DECIMAL(10,2),
    status TINYINT,
    create_time DATETIME,
    INDEX idx_user_product (user_id, product_id)
) ENGINE=InnoDB;

-- 按user_id分表,共10张表
-- seckill_order_0 ~ seckill_order_9

6.2.4 压测与调优

# 使用JMeter进行压测
# 配置线程组:500线程,循环100次
# 预期结果:QPS > 5000,错误率 < 0.1%

# 监控MySQL指标
mysqladmin -u root -p ext -i1 | grep -E "Threads_running|Queries|Slow_queries"

七、总结与最佳实践

7.1 高并发处理 checklist

架构层面:

  • [ ] 是否实现读写分离?
  • [ ] 是否需要分库分表?
  • [ ] 连接池配置是否合理?
  • [ ] 是否引入缓存层?
  • [ ] 是否有降级预案?

索引层面:

  • [ ] 核心查询是否有合适索引?
  • [ ] 索引选择性是否足够?
  • [ ] 是否存在索引失效?
  • [ ] 是否使用覆盖索引?
  • [ ] 定期分析索引使用情况?

SQL层面:

  • [ ] 避免SELECT *
  • [ ] 避免大事务
  • [ ] 避免在索引列上使用函数
  • [ ] 合理使用JOIN
  • [ ] 优化分页查询

7.2 持续优化建议

  1. 建立性能基线:记录日常性能指标,便于对比分析
  2. 定期慢查询分析:每周分析慢查询日志,持续优化
  3. 灰度发布:SQL变更先在从库测试,再同步到主库
  4. 容量规划:根据业务增长预测,提前扩容
  5. 应急预案:准备降级、熔断、限流方案

7.3 工具推荐

  • 慢查询分析:pt-query-digest, Percona Toolkit
  • 监控告警:Prometheus + Grafana, Zabbix
  • 性能测试:JMeter, sysbench
  • 索引优化:MySQL Workbench, Navicat
  • 日志分析:ELK Stack

通过以上策略的综合运用,可以系统性地提升MySQL在高并发场景下的性能和稳定性。记住,没有银弹,需要根据实际业务场景选择合适的组合方案,并持续监控和优化。