前两天有个哥们找我,说他那个电商平台的订单系统在晚上8点到9点的时候,页面转圈能转出一朵花来,后台一查,MySQL连接数直接爆满,CPU飙升到99%,吓得他半夜爬起来改配置。
这其实就是典型的高并发场景下的MySQL性能瓶颈问题。今天咱们不整那些虚头巴脑的理论,直接上干货,从现象到原理,再到实战代码,一步步带你解决这个头疼的问题。
一、先搞清楚:你的系统到底卡在哪
别急着上分库分表,先学会诊断。很多开发者一看到慢,就想着加索引、改配置、分库分表,结果搞了一堆,问题还是没解决,反而把系统搞得更复杂了。
第一步:定位瓶颈
你需要先搞清楚,你的系统卡在哪里。通常有这几个方向:
- 连接数瓶颈:客户端太多连接涌入,MySQL处理不过来
- 查询性能瓶颈:SQL写得烂,全表扫描,索引失效
- 写性能瓶颈:写入量大,锁竞争激烈
- 资源瓶颈: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);
}
}
注意:读写分离有个经典问题——数据延迟。主库写完,从库还没同步过来,这时候查可能查不到最新数据。解决办法:
- 写入后立即查主库(用
@DS("master")强制指定) - 缩短同步延迟(优化从库性能、减少网络延迟)
- 对于强一致场景,放弃读写分离,或者接受短暂不一致
三、连接池优化:别让连接成为瓶颈
很多系统卡顿,不是因为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亿条数据。随着数据量增长,会出现这些问题:
- 查询变慢:全表扫描越来越慢
- 索引效率下降:索引树变大,B+树层次增加
- 锁竞争加剧:写入时锁表范围变大
- 硬件瓶颈:单台服务器的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
