引言:理解高并发场景下的MySQL挑战
在当今互联网应用中,高并发访问已经成为常态。无论是电商平台的秒杀活动、社交媒体的热点事件,还是金融系统的交易处理,MySQL数据库都面临着前所未有的压力。高并发场景下,最常见的两个问题就是慢查询和锁等待,它们往往相互关联,形成恶性循环:慢查询持有锁的时间过长,导致其他事务等待;而锁等待又会进一步加剧系统负载,产生更多的慢查询。
本文将从架构优化、索引调优、查询优化、锁机制理解和监控诊断五个维度,系统性地讲解MySQL高并发处理策略。我们将通过实际案例和可操作的代码示例,帮助你构建一个高性能、高可用的MySQL系统。
一、架构层面的优化策略
1.1 读写分离架构
读写分离是提升MySQL并发处理能力的基础架构优化方案。其核心思想是将读操作和写操作分离到不同的数据库实例上执行。
架构图示:
应用层 → 读写分离中间件 → 主库(写) + 从库(读)
实现方式:
-- 1. 配置主从复制(主库配置)
-- my.cnf 或 my.ini
[mysqld]
server-id = 1
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7
-- 2. 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- 3. 在从库上配置
[mysqld]
server-id = 2
relay_log = mysql-relay-bin
read_only = 1 -- 从库设置为只读
-- 4. 启动从库复制
CHANGE MASTER TO
MASTER_HOST='主库IP',
MASTER_USER='repl',
MASTER_PASSWORD='repl_password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=0;
START SLAVE;
应用层路由示例(Java + ShardingSphere):
// 配置读写分离规则
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
// 配置主从数据源
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 主库(写)
HikariDataSource master = new HikariDataSource();
master.setJdbcUrl("jdbc:mysql://master:3306/test");
master.setUsername("root");
master.setPassword("password");
dataSourceMap.put("master", master);
// 从库(读)
HikariDataSource slave = new HikariDataSource();
slave.setJdbcUrl("jdbc:mysql://slave:3306/test");
slave.setUsername("root");
slave.setPassword("password");
dataSourceMap.put("slave", slave);
// 配置读写分离规则
DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "master");
// 配置分片规则(读写分离)
ShardingRule shardingRule = ShardingRule.builder()
.dataSourceRule(dataSourceRule)
.build();
return ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRule, new Properties());
}
}
读写分离的注意事项:
- 主从延迟问题:从库数据可能存在延迟,对于强一致性要求的读操作,需要强制走主库
- 事务内读写:同一个事务内的读操作必须走主库,避免脏读
- 缓存与数据库同步:读写分离时,缓存更新策略需要特别注意
1.2 分库分表策略
当单表数据量超过千万级或单库连接数成为瓶颈时,需要考虑分库分表。
水平分表示例:
-- 按用户ID取模分表(16张表)
-- 订单表:order_0 ~ order_15
-- 创建分表函数
DELIMITER $$
CREATE FUNCTION get_order_table_name(user_id BIGINT)
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
DECLARE table_index INT;
SET table_index = user_id % 16;
RETURN CONCAT('order_', table_index);
END$$
DELIMITER ;
-- 应用层分表路由(Java示例)
public class OrderShardingService {
private static final int TABLE_COUNT = 16;
public String getTableName(Long userId) {
int index = (int) (userId % TABLE_COUNT);
return "order_" + index;
}
public void insertOrder(Order order) {
String tableName = getTableName(order.getUserId());
String sql = String.format(
"INSERT INTO %s (user_id, order_no, amount, status) VALUES (?, ?, ?, ?)",
tableName
);
// 执行插入
jdbcTemplate.update(sql, order.getUserId(), order.getOrderNo(),
order.getAmount(), order.getStatus());
}
public Order queryOrder(Long userId, String orderNo) {
String tableName = getTableName(userId);
String sql = String.format(
"SELECT * FROM %s WHERE user_id = ? AND order_no = ?",
tableName
);
return jdbcTemplate.queryForObject(sql, new Object[]{userId, orderNo},
new OrderRowMapper());
}
}
分库分表中间件(ShardingSphere配置):
# sharding.yaml
dataSources:
ds_0: !!com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://192.168.1.10:3306/db_0
username: root
password: password
ds_1: !!com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://192.168.1.11:3306/db_1
username: root
password: password
shardingRule:
tables:
order:
actualDataNodes: ds_${0..1}.order_${0..15}
tableStrategy:
inline:
shardingColumn: user_id
algorithmExpression: order_${user_id % 16}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds_${user_id % 2}
bindingTables:
- order
1.3 连接池优化
连接池配置直接影响高并发下的数据库性能。
HikariCP最佳配置示例:
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/test");
config.setUsername("root");
config.setPassword("password");
// 核心配置参数
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setPoolName("HighConcurrencyPool");
// 连接池大小配置(根据CPU核心数和业务特点)
// 公式:连接数 = ((核心数 * 2) + 有效磁盘数)
config.setMaximumPoolSize(50); // 最大连接数
config.setMinimumIdle(10); // 最小空闲连接
config.setConnectionTimeout(30000); // 连接超时30秒
config.setIdleTimeout(600000); // 空闲超时10分钟
config.setMaxLifetime(1800000); // 连接最大存活时间30分钟
config.setLeakDetectionThreshold(60000); // 泄漏检测阈值60秒
// 优化性能参数
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
config.addDataSourceProperty("useServerPrepStmts", "true");
config.addDataSourceProperty("useLocalSessionState", "true");
config.addDataSourceProperty("rewriteBatchedStatements", "true");
config.addDataSourceProperty("cacheResultSetMetadata", "true");
config.addDataSourceProperty("cacheServerConfiguration", "true");
config.addDataSourceProperty("elideSetAutoCommits", "true");
config.addDataSourceProperty("maintainTimeStats", "false");
HikariDataSource dataSource = new HikariDataSource(config);
连接池监控指标:
- 活跃连接数(ActiveConnections)
- 空闲连接数(IdleConnections)
- 等待连接数(WaitCount)
- 平均等待时间(WaitTime)
二、索引优化策略
2.1 索引设计原则
索引是解决慢查询的最有效手段,但不当的索引会成为性能杀手。
索引设计黄金法则:
- 最左前缀原则:复合索引必须从左开始匹配
- 选择性原则:选择性高的列放在索引前面
- 覆盖索引:尽量让查询的列都在索引中
- 避免冗余索引:定期清理未使用的索引
索引设计实例:
-- 订单表结构
CREATE TABLE `order` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`merchant_id` BIGINT NOT NULL,
`order_no` VARCHAR(64) NOT NULL,
`status` TINYINT NOT NULL,
`amount` DECIMAL(10,2) NOT NULL,
`create_time` DATETIME NOT NULL,
`update_time` DATETIME NOT NULL,
INDEX idx_user_status (`user_id`, `status`), -- 用户状态查询
INDEX idx_merchant_time (`merchant_id`, `create_time`), -- 商户时间范围查询
INDEX idx_order_no (`order_no`), -- 订单号唯一查询
INDEX idx_user_amount (`user_id`, `amount`) -- 用户金额范围查询
) ENGINE=InnoDB;
-- 好的查询(命中索引)
EXPLAIN SELECT * FROM `order`
WHERE user_id = 123 AND status = 1; -- 命中 idx_user_status
EXPLAIN SELECT * FROM `order`
WHERE merchant_id = 456 AND create_time >= '2024-01-01'; -- 命中 idx_merchant_time
-- 不好的查询(索引失效)
EXPLAIN SELECT * FROM `order`
WHERE status = 1 AND user_id = 123; -- 索引失效,违反最左前缀
EXPLAIN SELECT * FROM `order`
WHERE user_id = 123 AND amount > 100; -- 范围查询后索引失效
2.2 索引优化实战
案例:电商订单查询优化
-- 优化前(慢查询)
EXPLAIN SELECT
o.order_no,
o.amount,
o.status,
u.username,
p.product_name
FROM `order` o
JOIN user u ON o.user_id = u.id
JOIN order_item oi ON o.id = oi.order_id
JOIN product p ON oi.product_id = p.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND o.amount > 100
AND u.username LIKE '%张%';
-- 执行计划分析:
-- type: ALL(全表扫描)
-- rows: 1000000
-- Extra: Using where; Using temporary; Using filesort
-- 预计执行时间:> 30秒
-- 优化步骤1:添加必要索引
ALTER TABLE `order` ADD INDEX idx_status_time_amount (`status`, `create_time`, `amount`);
ALTER TABLE `user` ADD INDEX idx_username (`username`);
ALTER TABLE `order_item` ADD INDEX idx_order_product (`order_id`, `product_id`);
-- 优化步骤2:重写查询(避免LIKE前缀通配符)
EXPLAIN SELECT
o.order_no,
o.amount,
o.status,
u.username,
p.product_name
FROM `order` o
STRAIGHT_JOIN user u ON o.user_id = u.id
STRAIGHT_JOIN order_item oi ON o.id = oi.order_id
STRAIGHT_JOIN product p ON oi.product_id = p.id
WHERE o.status = 1
AND o.create_time >= '2024-01-01'
AND o.amount > 100
AND u.username LIKE '张%'; -- 改为后缀匹配或全匹配
-- 优化后执行计划:
-- type: ref
-- rows: 50
-- Extra: Using index condition
-- 预计执行时间:< 100ms
索引优化工具:
-- 1. 查看索引使用情况
SELECT
t.TABLE_NAME,
t.INDEX_NAME,
t.SEQ_IN_INDEX,
t.COLUMN_NAME,
t.CARDINALITY,
s.ROWS,
s.DATA_LENGTH
FROM information_schema.STATISTICS t
JOIN information_schema.TABLES s ON t.TABLE_SCHEMA = s.TABLE_SCHEMA AND t.TABLE_NAME = s.TABLE_NAME
WHERE t.TABLE_SCHEMA = 'your_database'
ORDER BY t.TABLE_NAME, t.INDEX_NAME, t.SEQ_IN_INDEX;
-- 2. 查找冗余索引
SELECT
a.TABLE_SCHEMA,
a.TABLE_NAME,
a.INDEX_NAME AS first_index,
b.INDEX_NAME AS second_index,
a.COLUMN_NAME
FROM information_schema.STATISTICS a
JOIN information_schema.STATISTICS b ON a.TABLE_SCHEMA = b.TABLE_SCHEMA
AND a.TABLE_NAME = b.TABLE_NAME
AND a.COLUMN_NAME = b.COLUMN_NAME
AND a.SEQ_IN_INDEX = 1
AND b.SEQ_IN_INDEX > 1
WHERE a.TABLE_SCHEMA = 'your_database';
-- 3. 查找未使用的索引(需要开启userstat)
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
COUNT_READ,
COUNT_WRITE,
COUNT_FETCH,
COUNT_INSERT,
COUNT_UPDATE,
COUNT_DELETE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'your_database'
AND INDEX_NAME IS NOT NULL
AND COUNT_READ = 0
AND COUNT_WRITE = 0;
2.3 覆盖索引优化
覆盖索引可以避免回表操作,极大提升查询性能。
-- 订单表覆盖索引设计
ALTER TABLE `order` ADD INDEX idx_cover_user (
user_id,
status,
create_time,
amount,
order_no
);
-- 覆盖索引查询示例
EXPLAIN SELECT
order_no,
amount,
status,
create_time
FROM `order`
WHERE user_id = 123
AND status = 1
AND create_time >= '2024-01-01';
-- 执行计划显示:
-- type: ref
-- Extra: Using index(关键:表示使用了覆盖索引,无需回表)
-- rows: 10
-- 对比未使用覆盖索引的查询
EXPLAIN SELECT
order_no,
amount,
status,
create_time,
merchant_id -- 不在覆盖索引中,需要回表
FROM `order`
WHERE user_id = 123
AND status = 1
AND create_time >= '2024-01-01';
-- 执行计划显示:
-- type: ref
-- Extra: NULL(需要回表)
-- rows: 10
三、查询语句优化
3.1 避免索引失效的常见场景
-- 1. 避免在索引列上使用函数
-- 错误示例
SELECT * FROM `order` WHERE DATE(create_time) = '2024-01-01';
-- 正确示例
SELECT * FROM `order` WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00';
-- 2. 避免隐式类型转换
-- 错误示例(phone字段是varchar,传入数字)
SELECT * FROM user WHERE phone = 13800138000;
-- 正确示例
SELECT * FROM user WHERE phone = '13800138000';
-- 3. 避免OR条件导致索引失效
-- 错误示例
SELECT * FROM `order` WHERE user_id = 123 OR amount > 1000;
-- 正确示例(使用UNION ALL)
SELECT * FROM `order` WHERE user_id = 123
UNION ALL
SELECT * FROM `order` WHERE amount > 1000 AND user_id != 123;
-- 4. 避免LIKE以%开头
-- 错误示例
SELECT * FROM user WHERE username LIKE '%张三';
-- 正确示例(使用全文索引或倒排索引)
ALTER TABLE user ADD FULLTEXT INDEX ft_username (username);
SELECT * FROM user WHERE MATCH(username) AGAINST('+张三' IN BOOLEAN MODE);
3.2 JOIN优化策略
-- 优化前(嵌套循环JOIN,性能差)
EXPLAIN SELECT
o.order_no,
o.amount,
u.username
FROM `order` o
JOIN user u ON o.user_id = u.id
WHERE o.create_time >= '2024-01-01'
AND u.status = 1;
-- 执行计划:
-- id: 1
-- select_type: SIMPLE
-- table: o
-- type: ALL(全表扫描)
-- rows: 1000000
-- table: u
-- type: eq_ref
-- rows: 1
-- 优化后(强制指定JOIN顺序)
EXPLAIN SELECT
o.order_no,
o.amount,
u.username
FROM `order` o
STRAIGHT_JOIN user u ON o.user_id = u.id
WHERE o.create_time >= '2024-01-01'
AND u.status = 1;
-- 执行计划:
-- id: 1
-- select_type: SIMPLE
-- table: o
-- type: range(使用索引)
-- rows: 1000
-- table: u
-- type: eq_ref
-- rows: 1
-- 大表JOIN优化:使用IN + 子查询
EXPLAIN SELECT
o.order_no,
o.amount
FROM `order` o
WHERE o.user_id IN (
SELECT id FROM user WHERE status = 1
)
AND o.create_time >= '2024-01-01';
3.3 分页查询优化
-- 传统分页(深度分页问题)
SELECT * FROM `order`
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 1000000, 20; -- 扫描100万行,性能极差
-- 优化方案1:延迟关联(覆盖索引)
SELECT o.* FROM `order` o
JOIN (
SELECT id FROM `order`
WHERE user_id = 123
ORDER BY create_time DESC
LIMIT 1000000, 20
) t ON o.id = t.id;
-- 优化方案2:书签法(记录上一页最后一条记录)
-- 第一页
SELECT * FROM `order`
WHERE user_id = 123
ORDER BY create_time DESC, id DESC
LIMIT 20;
-- 第二页(假设上一页最后一条记录的create_time=2024-01-01 10:00:00, id=12345)
SELECT * FROM `order`
WHERE user_id = 123
AND (create_time < '2024-01-01 10:00:00'
OR (create_time = '2024-01-01 10:00:00' AND id < 12345))
ORDER BY create_time DESC, id DESC
LIMIT 20;
-- 优化方案3:使用ES或ClickHouse处理深度分页
3.4 批量操作优化
-- 批量插入优化
-- 错误示例(逐条插入)
INSERT INTO `order` (user_id, order_no, amount) VALUES (1, 'NO001', 100);
INSERT INTO `order` (user_id, order_no, amount) VALUES (2, 'NO002', 200);
-- ... 1000条
-- 正确示例(批量插入)
INSERT INTO `order` (user_id, order_no, amount) VALUES
(1, 'NO001', 100),
(2, 'NO002', 200),
(3, 'NO003', 300),
-- ... 1000条
(1000, 'NO1000', 1000);
-- 批量更新优化
-- 错误示例
UPDATE `order` SET status = 2 WHERE id = 1;
UPDATE `order` SET status = 2 WHERE id = 2;
-- ...
-- 正确示例(批量更新)
UPDATE `order`
SET status = CASE id
WHEN 1 THEN 2
WHEN 2 THEN 2
WHEN 3 THEN 2
END
WHERE id IN (1, 2, 3);
-- 或使用INSERT ON DUPLICATE KEY UPDATE
INSERT INTO `order` (id, user_id, order_no, amount, status) VALUES
(1, 1, 'NO001', 100, 2),
(2, 2, 'NO002', 200, 2)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
amount = VALUES(amount);
四、锁机制与并发控制
4.1 MySQL锁类型详解
-- 1. 共享锁(S锁)- 读锁
SELECT * FROM `order` WHERE id = 1 LOCK IN SHARE MODE;
-- 2. 排他锁(X锁)- 写锁
SELECT * FROM `order` WHERE id = 1 FOR UPDATE;
-- 3. 意向锁(Intention Locks)
-- InnoDB自动管理,无需手动控制
-- 4. 间隙锁(Gap Locks)
-- 在可重复读隔离级别下,防止幻读
-- 示例:范围查询会锁定间隙
BEGIN;
SELECT * FROM `order` WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- 此时其他事务无法插入id在10-20之间的记录
COMMIT;
-- 5. 临键锁(Next-Key Locks)= 记录锁 + 间隙锁
4.2 行锁优化策略
减少锁持有时间:
-- 优化前(长事务,锁持有时间长)
BEGIN;
-- 查询订单
SELECT * FROM `order` WHERE id = 123 FOR UPDATE;
-- 调用外部API(耗时1秒)
-- ... 调用支付接口 ...
-- 更新订单状态
UPDATE `order` SET status = 2 WHERE id = 123;
COMMIT;
-- 优化后(拆分事务,减少锁时间)
-- 步骤1:快速获取锁并更新
BEGIN;
UPDATE `order` SET status = 2 WHERE id = 123 AND status = 1;
-- 检查是否更新成功
SELECT ROW_COUNT();
COMMIT;
-- 步骤2:异步处理后续业务
-- 使用消息队列或异步任务处理支付回调
避免死锁的最佳实践:
-- 死锁场景:两个事务交叉锁定
-- 事务A
BEGIN;
UPDATE `order` SET status = 2 WHERE id = 1;
-- 事务B
BEGIN;
UPDATE `order` SET status = 2 WHERE id = 2;
-- 事务A
UPDATE `order` SET status = 2 WHERE id = 2; -- 等待锁
-- 事务B
UPDATE `order` SET status = 2 WHERE id = 1; -- 死锁!
-- 解决方案1:固定加锁顺序
-- 所有事务按id顺序加锁
BEGIN;
UPDATE `order` SET status = 2 WHERE id IN (1, 2) ORDER BY id;
COMMIT;
-- 解决方案2:使用SELECT ... FOR UPDATE NOWAIT
BEGIN;
-- 如果获取锁失败立即返回错误,而不是等待
SELECT * FROM `order` WHERE id = 1 FOR UPDATE NOWAIT;
-- 或者 WAIT 5(等待5秒)
SELECT * FROM `order` WHERE id = 1 FOR UPDATE WAIT 5;
COMMIT;
4.3 乐观锁与悲观锁应用
乐观锁(适合读多写少场景):
-- 表结构增加版本号字段
CREATE TABLE `order` (
`id` BIGINT PRIMARY KEY,
`amount` DECIMAL(10,2),
`version` INT DEFAULT 0,
-- 其他字段...
);
-- 更新操作
UPDATE `order`
SET amount = 200, version = version + 1
WHERE id = 123 AND version = 5; -- 版本号校验
-- 检查影响行数
-- 如果影响行数为0,说明数据已被其他事务修改,需要重试
悲观锁(适合写多读少场景):
-- 使用FOR UPDATE锁定记录
BEGIN;
SELECT * FROM `order` WHERE id = 123 FOR UPDATE;
-- 业务处理...
UPDATE `order` SET amount = 200 WHERE id = 123;
COMMIT;
4.4 隔离级别选择
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 不同隔离级别的选择策略:
-- 1. 读未提交(READ UNCOMMITTED)
-- 优点:无锁,性能最高
-- 缺点:脏读、不可重复读、幻读
-- 适用:对数据一致性要求极低的统计查询
-- 2. 读已提交(READ COMMITTED)- MySQL默认
-- 优点:避免脏读,性能较好
-- 缺点:不可重复读、幻读
-- 适用:大多数OLTP场景
-- 3. 可重复读(REPEATABLE READ)- InnoDB默认
-- 优点:避免脏读、不可重复读
-- 缺点:幻读(间隙锁解决)
-- 适用:需要事务内数据一致性的场景
-- 4. 串行化(SERIALIZABLE)
-- 优点:完全隔离
-- 缺点:性能最差,大量锁等待
-- 适用:极少使用,仅用于强一致性要求
-- 实际应用示例
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
-- 业务操作...
COMMIT;
五、监控与诊断工具
5.1 慢查询日志分析
# 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. 使用mysqldumpslow分析
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s: 排序方式(t=时间, c=次数, r=行数)
# -t: 显示前N条
# 3. 使用pt-query-digest(更强大)
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
慢查询日志示例:
# Time: 2024-01-01T10:00:00.123456Z
# User@Host: root[root] @ localhost [127.0.0.1]
# Thread_id: 12345 Schema: test QC_hit: No
# Query_time: 2.345678 Lock_time: 0.123456 Rows_sent: 10 Rows_examined: 1000000
# Rows_affected: 0 Bytes_sent: 1234
SET timestamp=1704067200;
SELECT * FROM `order` WHERE status = 1 AND create_time >= '2024-01-01';
5.2 Performance Schema监控
-- 1. 查看最慢的查询
SELECT
DIGEST_TEXT,
COUNT_STAR,
AVG_TIMER_WAIT/1000000000000 AS avg_time_sec,
MAX_TIMER_WAIT/1000000000000 AS max_time_sec,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
-- 2. 查看表I/O统计
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
COUNT_READ,
COUNT_WRITE,
SUM_TIMER_READ/1000000000000 AS read_time_sec,
SUM_TIMER_WRITE/1000000000000 AS write_time_sec
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = 'your_database'
ORDER BY SUM_TIMER_WAIT DESC;
-- 3. 查看索引使用情况
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
INDEX_NAME,
COUNT_READ,
COUNT_WRITE
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE OBJECT_SCHEMA = 'your_database'
AND INDEX_NAME IS NOT NULL
ORDER BY COUNT_READ DESC;
-- 4. 查看锁等待
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
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
5.3 实时诊断工具
-- 1. 查看当前运行的查询
SHOW FULL PROCESSLIST;
-- 2. 查看InnoDB状态(包含锁信息)
SHOW ENGINE INNODB STATUS\G
-- 3. 查看当前锁信息
SELECT
r.trx_id,
r.trx_state,
r.trx_started,
r.trx_query,
l.lock_mode,
l.lock_type,
l.lock_table,
l.lock_index
FROM information_schema.innodb_trx r
LEFT JOIN information_schema.innodb_locks l ON r.trx_id = l.trx_id;
-- 4. 查看缓冲池状态
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';
-- 5. 查看临时表使用情况
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
5.4 性能监控脚本
#!/bin/bash
# MySQL性能监控脚本
MYSQL_CMD="mysql -u root -p'password' -e"
while true; do
echo "=== $(date) ==="
# 活跃连接数
$MYSQL_CMD "SHOW STATUS LIKE 'Threads_connected';" | grep -v "Threads_connected"
# 慢查询数
$MYSQL_CMD "SHOW STATUS LIKE 'Slow_queries';" | grep -v "Slow_queries"
# InnoDB锁等待
$MYSQL_CMD "SELECT COUNT(*) FROM information_schema.innodb_lock_waits;" | tail -1
# 缓冲池命中率
$MYSQL_CMD "SELECT
(1 - (SUM(VARIABLE_VALUE) /
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'))) * 100 AS hit_rate
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads';" | tail -1
sleep 10
done
六、综合案例:秒杀系统优化
6.1 问题场景
业务需求: 10000人同时抢购100件商品,数据库频繁出现锁等待和慢查询。
原始方案:
-- 秒杀表
CREATE TABLE seckill (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
goods_id BIGINT,
stock INT,
version INT
);
-- 秒杀逻辑
BEGIN;
SELECT stock FROM seckill WHERE goods_id = 1;
-- 判断库存...
UPDATE seckill SET stock = stock - 1 WHERE goods_id = 1;
COMMIT;
问题分析:
- 高并发下大量锁等待
- 每次都要查询库存,数据库压力大
- 容易出现超卖
6.2 优化方案
1. 数据库层面优化:
-- 优化后的秒杀表
CREATE TABLE seckill (
id BIGINT PRIMARY KEY,
goods_id BIGINT UNIQUE,
stock INT NOT NULL,
version INT NOT NULL DEFAULT 0,
start_time DATETIME,
end_time DATETIME,
INDEX idx_time (start_time, end_time)
) ENGINE=InnoDB;
-- 优化后的秒杀逻辑(使用乐观锁+批量处理)
BEGIN;
-- 1. 先判断是否在秒杀时间范围内
SELECT COUNT(*) FROM seckill
WHERE goods_id = 1
AND NOW() BETWEEN start_time AND end_time;
-- 2. 使用乐观锁更新(减少锁等待)
UPDATE seckill
SET stock = stock - 1, version = version + 1
WHERE goods_id = 1
AND stock > 0
AND version = (SELECT version FROM seckill WHERE goods_id = 1);
-- 3. 检查影响行数
SELECT ROW_COUNT();
COMMIT;
2. 应用层优化(Redis+消息队列):
@Service
public class SeckillService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private RabbitTemplate rabbitTemplate;
private static final String STOCK_KEY = "seckill:stock:";
private static final String STOCK_VERSION = "seckill:version:";
/**
* 秒杀请求处理
*/
public SeckillResult seckill(Long userId, Long goodsId) {
String stockKey = STOCK_KEY + goodsId;
String versionKey = STOCK_VERSION + goodsId;
// 1. Redis预减库存(原子操作)
Long stock = redisTemplate.opsForValue().decrement(stockKey);
if (stock < 0) {
redisTemplate.opsForValue().increment(stockKey); // 回滚
return SeckillResult.fail("库存不足");
}
// 2. 判断是否已秒杀过
String userKey = "seckill:user:" + goodsId;
if (redisTemplate.opsForSet().add(userKey, userId) == 0) {
return SeckillResult.fail("不能重复秒杀");
}
// 3. 发送消息到队列,异步处理数据库
SeckillMessage message = new SeckillMessage();
message.setUserId(userId);
message.setGoodsId(goodsId);
message.setStock(stock);
rabbitTemplate.convertAndSend("seckill.exchange", "seckill.key", message);
return SeckillResult.success("秒杀成功,正在处理订单");
}
/**
* 消费者处理数据库
*/
@RabbitListener(queues = "seckill.queue")
public void handleSeckillMessage(SeckillMessage message) {
try {
// 1. 检查数据库库存
Integer dbStock = jdbcTemplate.queryForObject(
"SELECT stock FROM seckill WHERE goods_id = ?",
Integer.class, message.getGoodsId()
);
if (dbStock <= 0) {
// 库存不足,回滚Redis
redisTemplate.opsForValue().increment(STOCK_KEY + message.getGoodsId());
return;
}
// 2. 使用乐观锁更新数据库
int updateCount = jdbcTemplate.update(
"UPDATE seckill SET stock = stock - 1, version = version + 1 " +
"WHERE goods_id = ? AND stock > 0 AND version = ?",
message.getGoodsId(), message.getStock() + 1
);
if (updateCount == 0) {
// 更新失败,回滚Redis
redisTemplate.opsForValue().increment(STOCK_KEY + message.getGoodsId());
return;
}
// 3. 创建订单
createOrder(message.getUserId(), message.getGoodsId());
} catch (Exception e) {
// 异常处理,回滚Redis
redisTemplate.opsForValue().increment(STOCK_KEY + message.getGoodsId());
log.error("秒杀处理失败", e);
}
}
}
3. 数据库最终优化方案:
-- 1. 创建秒杀专用表(独立表空间)
CREATE TABLE seckill_stock (
goods_id BIGINT PRIMARY KEY,
stock INT NOT NULL,
version INT NOT NULL DEFAULT 0
) ENGINE=InnoDB;
-- 2. 创建秒杀订单表(分表)
CREATE TABLE seckill_order_0 (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
goods_id BIGINT,
order_no VARCHAR(64),
create_time DATETIME,
INDEX idx_user (user_id),
INDEX idx_goods (goods_id)
) ENGINE=InnoDB;
-- 3. 创建秒杀订单表(分表)
CREATE TABLE seckill_order_1 (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
goods_id BIGINT,
order_no VARCHAR(64),
create_time DATETIME,
INDEX idx_user (user_id),
INDEX idx_goods (goods_id)
) ENGINE=InnoDB;
-- 4. 分表路由函数
DELIMITER $$
CREATE FUNCTION get_seckill_order_table(user_id BIGINT)
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
RETURN CONCAT('seckill_order_', user_id % 2);
END$$
DELIMITER ;
6.3 压测与监控
# 使用sysbench进行压测
sysbench oltp_read_write \
--mysql-host=localhost \
--mysql-user=root \
--mysql-password=password \
--mysql-db=test \
--tables=10 \
--table-size=100000 \
--threads=100 \
--time=60 \
--report-interval=10 \
run
# 监控指标
# - QPS(每秒查询数)
# - TPS(每秒事务数)
# - 平均响应时间
# - 95%响应时间
# - 错误率
# - 连接数
# - 慢查询数
七、总结与最佳实践
7.1 高并发优化检查清单
架构层面:
- [ ] 是否实施读写分离?
- [ ] 数据量是否超过千万级(考虑分库分表)?
- [ ] 连接池配置是否合理?
- [ ] 是否使用缓存(Redis)减少数据库压力?
索引层面:
- [ ] 所有查询是否都有合适的索引?
- [ ] 是否遵循最左前缀原则?
- [ ] 是否使用覆盖索引减少回表?
- [ ] 是否定期清理无用索引?
查询层面:
- [ ] 避免SELECT *,只查询需要的字段
- [ ] 避免索引列上使用函数
- [ ] 避免隐式类型转换
- [ ] 避免深度分页
- [ ] 批量操作代替单条操作
锁层面:
- [ ] 事务是否足够小(短事务)?
- [ ] 是否合理使用乐观锁/悲观锁?
- [ ] 是否避免了死锁(固定加锁顺序)?
- [ ] 是否选择了合适的隔离级别?
监控层面:
- [ ] 是否开启慢查询日志?
- [ ] 是否定期分析慢查询?
- [ ] 是否监控锁等待?
- [ ] 是否监控连接池状态?
7.2 性能优化黄金法则
- 先测量,再优化:使用监控工具定位真正的性能瓶颈
- 80/20原则:优化20%的慢查询解决80%的性能问题
- 分层优化:从架构 → 索引 → 查询 → 锁,逐层深入
- 避免过度优化:保持代码可读性,不要为不存在的瓶颈优化
- 持续监控:性能优化是持续过程,不是一次性工作
7.3 常见误区提醒
- 误区1:索引越多越好 → 正确:索引会降低写性能
- 误区2:事务越大越好 → 正确:事务越小越好,减少锁持有时间
- 误区3:连接数越大越好 → 正确:连接数过多会导致上下文切换开销
- 误区4:内存越大越好 → 正确:需要合理配置缓冲池大小
- 误区5:分库分表是银弹 → 正确:分库分表带来复杂度,应最后考虑
通过以上系统性的优化策略,可以有效解决MySQL高并发下的慢查询和锁等待问题。记住,性能优化是一个系统工程,需要从架构、设计、实现到监控的全方位考虑。
