MySQL性能调优实战:慢查询分析、索引设计与分库分表方案

MySQL慢查询定位与执行计划分析

MySQL性能调优的第一步是识别慢查询。通过开启慢查询日志捕获执行时间超过阈值的SQL语句,再使用EXPLAIN分析执行计划,定位全表扫描、临时表、文件排序等性能瓶颈。这套方法论适用于大多数数据库运维场景。

开启慢查询日志并配置阈值:

-- 查看当前慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 动态开启慢查询日志(重启后失效)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1;  -- 超过1秒的SQL记录
SET GLOBAL log_queries_not_using_indexes = 'ON';  -- 记录未使用索引的查询

-- 永久生效需写入 my.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

使用EXPLAIN分析SQL执行计划:

EXPLAIN SELECT 
    o.order_id, o.total_amount, u.username, p.product_name
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.id
WHERE o.create_time >= '2026-01-01' 
  AND o.status = 'PAID'
ORDER BY o.total_amount DESC
LIMIT 20;

EXPLAIN输出中需要重点关注以下字段:

+----+--------+--------+------------------+---------+---------+------------------+--------+----------------------------------------------+
| id | select_type| table | type             | key     | key_len | ref              | rows   | Extra                                        |
+----+--------+--------+------------------+---------+---------+------------------+--------+----------------------------------------------+
|  1 | SIMPLE | o      | ref              | idx_status_time | 130 | const            |  15000 | Using where; Using temporary; Using filesort |
|  1 | SIMPLE | u      | eq_ref           | PRIMARY | 4       | test.o.user_id   |      1 | NULL                                         |
|  1 | SIMPLE | oi     | ref              | idx_order_id | 4 | test.o.order_id  |      3 | NULL                                         |
|  1 | SIMPLE | p      | eq_ref           | PRIMARY | 4       | test.oi.product_id|     1 | NULL                                         |
+----+--------+--------+------------------+---------+---------+------------------+--------+----------------------------------------------+

type字段的值表示访问类型,性能从好到差:system > const > eq_ref > ref > range > index > ALL。上述结果中orders表出现了Using temporary和Using filesort,说明查询创建了临时表并进行了文件排序,这是需要优化的重点。

复合索引设计与覆盖索引优化

索引设计是SQL查询优化的核心。MySQL的InnoDB引擎使用B+树索引,理解最左前缀原则和覆盖索引概念对性能调优至关重要。

复合索引遵循最左前缀原则:索引(a, b, c)可以用于查询条件a、a,b、a,b,c,但不能用于b,c或c。索引列的顺序应将区分度高的列放在前面,将范围查询的列放在最后。

-- 针对上述慢查询的索引优化

-- 原始查询条件:status = 'PAID' AND create_time >= '2026-01-01'
-- 排序:ORDER BY total_amount DESC

-- 错误索引:将范围查询列放前面
-- CREATE INDEX idx_time_status ON orders(create_time, status);
-- 问题:create_time是范围查询,后面的status无法走索引

-- 正确索引:等值查询在前,范围查询在后
CREATE INDEX idx_status_time_amount ON orders(status, create_time, total_amount);

-- 如果查询只需要索引包含的列,可以使用覆盖索引避免回表
-- 覆盖索引:查询的所有列都在索引中,不需要回表查数据行
EXPLAIN SELECT order_id, total_amount 
FROM orders 
WHERE status = 'PAID' AND create_time >= '2026-01-01';
-- Extra列显示 Using index,表示使用了覆盖索引

索引创建的注意事项:单表索引数量不宜超过5-6个,过多索引会影响写入性能;索引列长度尽量短,长字符串使用前缀索引;避免在索引列上使用函数或类型转换,否则索引失效。

-- 索引失效的常见场景

-- 1. 函数操作导致索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2026;
-- 改写为范围查询:
SELECT * FROM orders 
WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01';

-- 2. 隐式类型转换
SELECT * FROM orders WHERE order_no = 123456789;
-- 如果order_no是varchar类型,应改为:
SELECT * FROM orders WHERE order_no = '123456789';

-- 3. LIKE以通配符开头
SELECT * FROM products WHERE product_name LIKE '%手机%';
-- 如果必须使用前缀模糊匹配,考虑全文索引或搜索引擎

-- 4. OR条件中有一侧无索引
SELECT * FROM orders WHERE order_id = 100 OR remark = '加急';
-- 确保OR两侧的列都有索引,或改用UNION ALL

分库分表方案与ShardingSphere实践

当单表数据量超过千万级别,B+树索引层级增加导致查询性能下降,写入性能也受影响。分库分表是解决这一问题的标准方案。ShardingSphere是目前Java生态中主流的分库分表中间件,支持声明式配置和透明化的SQL路由。

分片策略选择:水平分表按行拆分数据到多个表,适合单表数据量巨大的场景;水平分库将数据分散到不同数据库实例,能同时缓解单库连接数和磁盘IO瓶颈。分片键的选择是核心决策,应选择查询条件中频繁使用且分布均匀的列。

// Spring Boot + ShardingSphere 配置
// application.yml

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0
        username: root
        password: password
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_1
        username: root
        password: password
    
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..1}.t_order_${0..3}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-inline
            table-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: table-inline
            key-generate-strategy:
              column: order_id
              key-generator-name: snowflake
        
        sharding-algorithms:
          db-inline:
            type: INLINE
            props:
              algorithm-expression: ds${user_id % 2}
          table-inline:
            type: INLINE
            props:
              algorithm-expression: t_order_${order_id % 4}
        
        key-generators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1

分库分表后的跨库查询问题需要特别处理。分片键不相同时,Join查询需要广播表或绑定表支持。对于聚合统计类查询,ShardingSphere会合并各分片结果。复杂的跨库Join建议在应用层组装,或引入Elasticsearch等搜索引擎做查询分离。

Redis缓存策略与缓存一致性

NoSQL选型应用中Redis是最常用的缓存方案。缓存能大幅降低数据库压力,但引入了缓存与数据库一致性这一经典难题。Cache-Aside(旁路缓存)模式是生产环境最常用的缓存策略。

// Cache-Aside 模式的标准实现

@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    private static final String CACHE_PREFIX = "product:";
    private static final long CACHE_TTL = 3600; // 缓存1小时

    // 读操作:先读缓存,缓存未命中再读数据库
    public Product getProduct(Long id) {
        String key = CACHE_PREFIX + id;
        
        // 1. 查缓存
        Product product = (Product) redisTemplate.opsForValue().get(key);
        if (product != null) {
            return product;
        }
        
        // 2. 缓存未命中,查数据库
        product = productRepository.findById(id).orElse(null);
        if (product != null) {
            // 3. 写入缓存(带随机TTL防止缓存雪崩)
            long ttl = CACHE_TTL + ThreadLocalRandom.current().nextInt(300);
            redisTemplate.opsForValue().set(key, product, ttl, TimeUnit.SECONDS);
        } else {
            // 4. 空值缓存(防止缓存穿透)
            redisTemplate.opsForValue().set(key, "NULL", 60, TimeUnit.SECONDS);
        }
        
        return product;
    }
    
    // 写操作:先更新数据库,再删除缓存
    @Transactional
    public void updateProduct(Long id, ProductDTO dto) {
        // 1. 更新数据库
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new BusinessException("商品不存在"));
        product.setName(dto.getName());
        product.setPrice(dto.getPrice());
        productRepository.save(product);
        
        // 2. 删除缓存(而非更新缓存,避免并发写入导致脏数据)
        redisTemplate.delete(CACHE_PREFIX + id);
    }
}

缓存三大问题及解决方案:

// 1. 缓存穿透:大量请求查询不存在的数据
// 解决方案:布隆过滤器 + 空值缓存
public Product getProductWithBloomFilter(Long id) {
    // 布隆过滤器检查:如果不存在直接返回
    if (!bloomFilter.mightContain(id)) {
        return null;
    }
    return getProduct(id);  // 正常的Cache-Aside流程
}

// 2. 缓存击穿:热点Key过期瞬间大量请求打到数据库
// 解决方案:互斥锁(双重检查)
public Product getProductWithLock(Long id) {
    String key = CACHE_PREFIX + id;
    Product product = (Product) redisTemplate.opsForValue().get(key);
    if (product != null) {
        return product;
    }
    
    // 获取分布式锁
    String lockKey = "lock:product:" + id;
    try {
        boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        if (locked) {
            // 双重检查
            product = (Product) redisTemplate.opsForValue().get(key);
            if (product != null) {
                return product;
            }
            // 查数据库并写入缓存
            product = productRepository.findById(id).orElse(null);
            if (product != null) {
                redisTemplate.opsForValue().set(key, product, 
                    CACHE_TTL, TimeUnit.SECONDS);
            }
            return product;
        } else {
            // 等待并重试
            Thread.sleep(50);
            return getProductWithLock(id);
        }
    } finally {
        redisTemplate.delete(lockKey);
    }
}

// 3. 缓存雪崩:大量Key同时过期
// 解决方案:TTL加随机值(已在getProduct中实现)
// 数据库高可用架构层面:限流降级、多级缓存(本地缓存+Redis)

数据备份恢复方面,MySQL推荐使用Percona XtraBackup做物理热备份,结合binlog实现PITR(基于时间点的恢复)。Redis使用RDB定期快照+AOF增量日志的策略,AOF配置appendfsync everysec在性能和数据安全性之间取得平衡。分库分表环境下的备份需要各分片独立备份,恢复时注意分片间的数据一致性校验。这些数据库高可用架构的实践,是保障业务数据安全的底线方案。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/mysql-xing-neng-diao-you-shi-zhan-man-cha-xun-fen-xi-suo/

(0)
小编小编
上一篇 8小时前
下一篇 8小时前

相关推荐