前两天有个哥们找我,说他那个电商平台的订单系统在晚上8点到9点的时候,页面转圈能转出一朵花来,后台一查,MySQL连接数直接爆满,CPU飙升到99%,吓得他半夜爬起来改配置。

这其实就是典型的高并发场景下的MySQL性能瓶颈问题。今天咱们不整那些虚头巴脑的理论,直接上干货,从现象到原理,再到实战代码,一步步带你解决这个头疼的问题。

一、先搞清楚:你的系统到底卡在哪

别急着上分库分表,先学会诊断。很多开发者一看到慢,就想着加索引、改配置、分库分表,结果搞了一堆,问题还是没解决,反而把系统搞得更复杂了。

第一步:定位瓶颈

你需要先搞清楚,你的系统卡在哪里。通常有这几个方向:

  1. 连接数瓶颈:客户端太多连接涌入,MySQL处理不过来
  2. 查询性能瓶颈:SQL写得烂,全表扫描,索引失效
  3. 写性能瓶颈:写入量大,锁竞争激烈
  4. 资源瓶颈:CPU、内存、磁盘IO不够用

怎么诊断呢?我给你几个实用的命令:

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

-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';

-- 查看当前运行的查询
SHOW PROCESSLIST;

-- 查看锁等待情况
SELECT * FROM information_schema.INNODB_TRX;

有个小技巧,你可以用这个SQL实时监控连接数变化:

-- 每5秒采样一次,查看连接数趋势
SELECT 
    TABLE_NAME,
    COUNT(*) as connection_count
FROM information_schema.PROCESSLIST
GROUP BY DB
ORDER BY connection_count DESC;

二、读写分离:让读压力分流

高并发场景下,80%的流量往往是读操作,20%是写操作。如果把读和写分开处理,性能提升会非常明显。

原理很简单:主库负责写,从库负责读。主库通过binlog同步数据到从库,从库可以有多台,实现读负载均衡。

2.1 架构设计

                    ┌─────────────┐
                    │   应用层     │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
     ┌────────▼────┐ ┌────▼────┐ ┌─────▼──────┐
     │  主库(写)   │ │ 从库1   │ │   从库2    │
     │  MySQL A    │ │ MySQL B │ │   MySQL C  │
     └─────────────┘ └─────────┘ └────────────┘
              │            │            │
              └────────────┴────────────┘
                        binlog同步

2.2 代码实现:ShardingSphere读写分离

我推荐用Apache ShardingSphere,这是目前最主流的Java分布式数据库中间件。

// Maven依赖
<dependencies>
    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
        <version>5.3.2</version>
    </dependency>
</dependencies>
# 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-db:3306/ecommerce?useSSL=false&serverTimezone=UTC
        username: root
        password: your_password
        
      # 从库1配置
      slave0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave-db1:3306/ecommerce?useSSL=false&serverTimezone=UTC
        username: root
        password: your_password
        
      # 从库2配置
      slave1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://slave-db2:3306/ecommerce?useSSL=false&serverTimezone=UTC
        username: root
        password: your_password
        
    rules:
      readwrite-splitting:
        data-sources:
          ds:
            load-balancer-type: ROUND_ROBIN  # 轮询负载均衡
            write-data-source-name: master
            read-data-source-names: slave0,slave1
            
    props:
      sql-show: true  # 打印SQL,方便调试
// Java代码使用
@Service
public class OrderService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 写入订单 - 走主库
     */
    @DS("master")  // 强制走主库
    public long createOrder(Order order) {
        String sql = "INSERT INTO orders (user_id, product_id, amount, status, create_time) " +
                     "VALUES (?, ?, ?, ?, ?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            ps.setLong(1, order.getUserId());
            ps.setLong(2, order.getProductId());
            ps.setBigDecimal(3, order.getAmount());
            ps.setString(4, "PENDING");
            ps.setTimestamp(5, new Timestamp(System.currentTimeMillis()));
            return ps;
        }, keyHolder);
        return keyHolder.getKey().longValue();
    }
    
    /**
     * 查询订单 - 走从库(自动负载均衡)
     */
    public Order queryOrder(Long orderId) {
        String sql = "SELECT * FROM orders WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Order.class), orderId);
    }
    
    /**
     * 批量查询 - 走从库
     */
    public List<Order> queryOrdersByUserId(Long userId) {
        String sql = "SELECT * FROM orders WHERE user_id = ? ORDER BY create_time DESC LIMIT 20";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Order.class), userId);
    }
}

注意:读写分离有个经典问题——数据延迟。主库写完,从库还没同步过来,这时候查可能查不到最新数据。解决办法:

  1. 写入后立即查主库(用@DS("master")强制指定)
  2. 缩短同步延迟(优化从库性能、减少网络延迟)
  3. 对于强一致场景,放弃读写分离,或者接受短暂不一致

三、连接池优化:别让连接成为瓶颈

很多系统卡顿,不是因为MySQL本身慢,而是因为连接池配置不合理,要么连接不够用,要么连接太多拖垮数据库。

3.1 连接池工作原理

连接池就像是一个”停车场”,预先分配好一定数量的”车位”(数据库连接),需要的时候去停车,用完再还回来。

如果车位太少,车来了没地方停,只能排队等待;如果车位太多,维护成本太高,还可能把数据库拖垮。

3.2 HikariCP配置详解

HikariCP是目前性能最好的Java连接池,我来给你拆解每个参数的含义:

spring:
  datasource:
    hikari:
      # 连接池最小空闲连接数
      # 建议:根据业务低谷期的并发量设置
      minimum-idle: 10
      
      # 连接池最大连接数
      # 建议:不要超过MySQL的max_connections的70%
      # 计算公式:CPU核心数 * 2 + 有效磁盘数
      maximum-pool-size: 30
      
      # 连接最大生命周期(毫秒)
      # 建议:比MySQL的wait_timeout短一点
      max-lifetime: 1800000  # 30分钟
      
      # 连接最大存活时间(毫秒)
      # 建议:30分钟以内,防止连接长时间占用
      idle-timeout: 600000  # 10分钟
      
      # 获取连接的超时时间(毫秒)
      # 如果10秒内获取不到连接,抛出异常
      connection-timeout: 10000
      
      # 连接测试查询(性能影响小)
      connection-test-query: SELECT 1
      
      # 自动提交
      auto-commit: true
      
      # 连接名称
      pool-name: HikariPool-Order
      
      # 初始化SQL
      initialization-fail-timeout: 1

3.3 连接池性能监控

光配好还不够,你得知道连接池的运行状态:

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.hikari")
    public HikariDataSource dataSource() {
        return new HikariDataSource();
    }
    
    @Bean
    public DataSource dataSourceWrapper(DataSource dataSource) {
        return new DataSourceWrapper(dataSource);
    }
}

public class DataSourceWrapper extends DataSourceDecorator {
    
    public DataSourceWrapper(DataSource ds) {
        super(ds);
    }
    
    /**
     * 获取连接池健康状态
     */
    public Map<String, Object> getPoolStatus() {
        HikariDataSource hikariDs = (HikariDataSource) unwrap(HikariDataSource.class);
        HikariPoolMXBean pool = hikariDs.getHikariPoolMXBean();
        
        Map<String, Object> status = new HashMap<>();
        status.put("activeConnections", pool.getActiveConnections());
        status.put("idleConnections", pool.getIdleConnections());
        status.put("totalConnections", pool.getTotalConnections());
        status.put("threadsAwaitingConnection", pool.getThreadsAwaitingConnection());
        status.put("connectionTimeoutCount", pool.getConnectionTimeoutTotalCount());
        
        return status;
    }
}
// 定期监控连接池状态
@Component
public class ConnectionPoolMonitor {
    
    private static final Logger log = LoggerFactory.getLogger(ConnectionPoolMonitor.class);
    
    @Autowired
    private DataSourceWrapper dataSourceWrapper;
    
    // 每分钟检查一次连接池状态
    @Scheduled(fixedRate = 60000)
    public void checkPoolStatus() {
        Map<String, Object> status = dataSourceWrapper.getPoolStatus();
        
        Integer active = (Integer) status.get("activeConnections");
        Integer total = (Integer) status.get("totalConnections");
        Integer waiting = (Integer) status.get("threadsAwaitingConnection");
        Integer timeoutCount = (Integer) status.get("connectionTimeoutCount");
        
        log.info("连接池状态 - 活跃:{}, 总数:{}, 等待:{}, 超时累计:{}", 
                 active, total, waiting, timeoutCount);
        
        // 告警逻辑
        if (waiting > 5) {
            log.warn("连接池等待线程过多,可能存在性能瓶颈!");
        }
        if (timeoutCount > 0) {
            log.error("检测到连接获取超时,连接池配置可能需要调整!");
        }
    }
}

3.4 不同场景的连接池配置建议

场景 minimum-idle maximum-pool-size 说明
低并发(<100 QPS) 5 15 保守配置,节省资源
中等并发(100-500 QPS) 10 30 平衡性能和资源
高并发(>500 QPS) 20 50-100 需要配合分库分表
写入密集型 10 30 写操作需要更多连接
读取密集型 20 50 读操作可以多开连接

四、分库分表:解决单库性能天花板

当单库扛不住的时候,就该分库分表上场了。这是解决高并发最彻底的方式,但也是最复杂的。

4.1 为什么要分库分表

想象一下,你有一个订单表,每天有100万条新增,一年就是3.65亿条数据。随着数据量增长,会出现这些问题:

  1. 查询变慢:全表扫描越来越慢
  2. 索引效率下降:索引树变大,B+树层次增加
  3. 锁竞争加剧:写入时锁表范围变大
  4. 硬件瓶颈:单台服务器的CPU、内存、IO都有上限

4.2 分片策略选择

分片策略是核心,选错了后面全是坑。

/**
 * 分片策略枚举
 */
public enum ShardingStrategy {
    
    /**
     * 按ID取模分片
     * 优点:均匀分布,实现简单
     * 缺点:扩容困难,需要重新分片
     */
    HASH_ID {
        @Override
        public int calculateShard(Long id, int shardCount) {
            return Math.abs(id.intValue()) % shardCount;
        }
    },
    
    /**
     * 按用户ID分片
     * 优点:同一用户的订单在同一库,查询友好
     * 缺点:大V用户数据集中,可能倾斜
     */
    HASH_USER_ID {
        @Override
        public int calculateShard(Long userId, int shardCount) {
            return Math.abs(userId.intValue()) % shardCount;
        }
    },
    
    /**
     * 按时间范围分片
     * 优点:适合按时间查询,历史数据归档方便
     * 缺点:热点数据可能集中在某段时间
     */
    RANGE_TIME {
        @Override
        public int calculateShard(Date createTime, int shardCount) {
            // 简单实现:按月份取模
            Calendar cal = Calendar.getInstance();
            cal.setTime(createTime);
            int month = cal.get(Calendar.MONTH);
            return month % shardCount;
        }
    },
    
    /**
     * 一致性哈希分片
     * 优点:扩容时数据迁移最少
     * 缺点:实现复杂,需要虚拟节点
     */
    CONSISTENT_HASH {
        @Override
        public int calculateShard(Long key, int shardCount) {
            // 简化版一致性哈希
            int hash = key.hashCode();
            int position = Math.abs(hash) % (shardCount * 160); // 160个虚拟节点
            int shardIndex = position / 160;
            return shardIndex;
        }
    };
    
    public abstract int calculateShard(Object key, int shardCount);
}

4.3 ShardingSphere分库分表示例

# 分库分表配置
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://db-host0:3306/ecommerce_db0
        username: root
        password: your_password
        
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://db-host1:3306/ecommerce_db1
        username: root
        password: your_password
        
    rules:
      sharding:
        # 订单表分库分表策略
        tables:
          orders:
            # 实际数据节点
            actual-data-nodes: ds$->{0..1}.orders$->{0..3}
            
            # 分库策略:按用户ID取模
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: user-db-sharding
            
            # 分表策略:按订单ID取模
            table-strategy:
              standard:
                sharding-column: id
                sharding-algorithm-name: order-table-sharding
        
        # 分片算法配置
        sharding-algorithms:
          user-db-sharding:
            type: HASH_MOD  # 取模算法
            props:
              sharding-count: 2  # 2个数据库
              
          order-table-sharding:
            type: HASH_MOD
            props:
              sharding-count: 4  # 每个库4张表,共8张表
              
        # 主键生成策略:分布式ID
        key-generators:
          snowflake:
            type: SNOWFLAKE
            props:
              worker-id: 1  # 每个节点唯一ID
            
    props:
      sql-show: true  # 打印SQL

”`java /**

  • 订单Service - 分库分表场景 */ @Service public class OrderShardingService {

    @Autowired private JdbcTemplate jdbcTemplate;

    /**

    • 创建订单 - 自动路由到正确的库和表 */ public long createOrder(Order order) { String sql = “INSERT INTO orders (id, user_id, product_id, amount, status, create_time) ” +

               "VALUES (?, ?, ?, ?, ?, ?)";
      

      // 使用雪花算法生成全局唯一ID long orderId = IdGenerator.nextId();

      KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> {

      PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
      ps.setLong(1, orderId);
      ps.setLong(2