引言:理解高并发场景下的数据库挑战
在现代互联网应用中,高并发访问已经成为常态。无论是电商平台的秒杀活动、社交媒体的热点事件,还是金融系统的交易高峰期,MySQL数据库都面临着前所未有的压力。当并发连接数激增时,数据库性能往往会急剧下降,甚至导致系统完全崩溃。因此,掌握MySQL高并发处理策略对于保障系统稳定性和用户体验至关重要。
高并发场景下,MySQL面临的主要挑战包括:
- 连接资源耗尽:大量并发连接导致线程池耗尽
- 锁竞争激烈:行锁、表锁争用导致事务等待时间延长
- I/O瓶颈:磁盘读写速度跟不上内存处理速度
- CPU过载:复杂查询和排序操作消耗大量CPU资源
- 内存不足:缓冲池命中率低,频繁的磁盘I/O
本文将从多个维度深入探讨MySQL高并发优化策略,帮助您构建高性能、高可用的数据库系统。
一、架构层面的优化策略
1.1 读写分离架构
读写分离是应对高并发读写场景的经典架构模式。通过将读操作和写操作分离到不同的数据库实例,可以有效分担主库压力。
实现方案:
-- 主库配置(写操作)
-- my.cnf 配置
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-format=ROW
-- 从库配置(读操作)
[mysqld]
server-id=2
relay-log=mysql-relay-bin
read-only=1
应用层路由逻辑示例(Java):
public class DataSourceRouter {
private DataSource masterDataSource; // 主库
private DataSource slaveDataSource; // 从库
public Connection getConnection() throws SQLException {
String currentTransaction = TransactionContext.getCurrentTransaction();
if ("WRITE".equals(currentTransaction)) {
return masterDataSource.getConnection();
} else {
// 简单的轮询策略
return slaveDataSource.getConnection();
}
}
}
读写分离的注意事项:
- 主从延迟问题:从库数据可能存在延迟,需要特殊处理关键业务
- 事务一致性:同一个事务内的读写操作必须路由到主库
- 从库负载均衡:多个从库时需要实现负载均衡
1.2 分库分表策略
当单表数据量超过千万级时,查询性能会显著下降。分库分表是解决大数据量和高并发的有效手段。
水平分表示例:
-- 原始订单表
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME,
INDEX idx_user_id (user_id)
) ENGINE=InnoDB;
-- 分表后的订单表(按user_id取模分16张表)
CREATE TABLE orders_0 (
order_id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME,
INDEX idx_user_id (user_id)
) ENGINE=InnoDB;
-- 重复创建 orders_1 到 orders_15
分表路由算法(Java):
public class TableSharding {
private static final int TABLE_COUNT = 16;
public String getTableName(Long userId) {
int index = (int) (userId % TABLE_COUNT);
return "orders_" + index;
}
// 分片查询示例
public List<Order> getOrdersByUserId(Long userId) {
String tableName = getTableName(userId);
String sql = "SELECT * FROM " + tableName + " WHERE user_id = ?";
// 执行查询...
return orders;
}
}
分库分表的挑战:
- 跨分片查询:需要额外的中间件或应用层处理
- 分布式事务:跨库事务处理复杂
- 数据迁移:扩容时的数据迁移问题
二、MySQL配置参数优化
2.1 InnoDB引擎核心参数
InnoDB是MySQL的默认存储引擎,其配置直接影响高并发性能。
关键参数配置示例:
# my.cnf 核心配置
[mysqld]
# 连接相关
max_connections = 2000 # 最大连接数,根据业务调整
max_connect_errors = 100000 # 连接错误限制
thread_cache_size = 64 # 线程缓存
# InnoDB缓冲池(最重要的参数)
innodb_buffer_pool_size = 24G # 通常设置为物理内存的70-80%
innodb_buffer_pool_instances = 8 # 缓冲池实例数,减少竞争
# 日志文件
innodb_log_file_size = 2G # 重做日志大小
innodb_log_buffer_size = 64M # 日志缓冲区
innodb_flush_log_at_trx_commit = 1 # 事务提交策略(1=最安全,2=性能更好)
# I/O相关
innodb_flush_method = O_DIRECT # 直接I/O,避免双缓存
innodb_io_capacity = 2000 # I/O能力(SSD可设置更高)
innodb_io_capacity_max = 4000 # 最大I/O能力
# 并发控制
innodb_thread_concurrency = 0 # 0表示InnoDB自动管理线程并发
innodb_read_io_threads = 8 # 读线程数
innodb_write_io_threads = 8 # 写线程数
# 锁相关
innodb_lock_wait_timeout = 50 # 锁等待超时(秒)
innodb_rollback_on_timeout = OFF # 超时是否回滚
# 查询缓存(MySQL 8.0已移除)
# query_cache_type = 0
# query_cache_size = 0
# 临时表
tmp_table_size = 512M # 临时表大小
max_heap_table_size = 512M # 内存表最大大小
# 排序缓冲区
sort_buffer_size = 4M # 每个线程的排序缓冲区
read_buffer_size = 4M # 顺序读缓冲区
read_rnd_buffer_size = 8M # 随机读缓冲区
# 连接超时
wait_timeout = 600 # 非交互连接超时(秒)
interactive_timeout = 600 # 交互连接超时(秒)
参数调优建议:
- innodb_buffer_pool_size:这是最重要的参数,直接影响数据命中率。可以通过以下SQL监控:
-- 查看缓冲池命中率
SELECT
(1 - (SUM(VARIABLE_VALUE) / @@innodb_buffer_pool_size)) * 100 AS buffer_hit_rate
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads';
-- 命中率应保持在99%以上,低于95%需要增加缓冲池大小
- innodb_flush_log_at_trx_commit:权衡数据安全性和性能
1:每次事务提交都写入磁盘(最安全,性能最低)2:每秒写入磁盘(性能最好,可能丢失1秒数据)0:每秒写入并刷新(性能最好,风险最大)
2.2 查询缓存优化(MySQL 8.0之前)
虽然MySQL 8.0已移除查询缓存,但在低版本中仍需了解:
-- 查看查询缓存状态
SHOW VARIABLES LIKE 'query_cache%';
SHOW STATUS LIKE 'Qcache%';
-- 查询缓存命中率计算
-- Qcache_hits / (Qcache_hits + Qcache_inserts) * 100
注意:在高并发更新场景下,查询缓存往往弊大于利,建议关闭。
2.3 连接池配置
应用层连接池配置同样关键:
HikariCP配置示例(Java):
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setUsername("user");
config.setPassword("password");
// 核心连接数(根据业务调整)
config.setMinimumIdle(20);
config.setMaximumPoolSize(100);
// 连接超时
config.setConnectionTimeout(30000); // 30秒
config.setIdleTimeout(600000); // 10分钟
config.setMaxLifetime(1800000); // 30分钟
// 测试查询
config.setConnectionTestQuery("SELECT 1");
// 预编译语句缓存
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
HikariDataSource dataSource = new HikariDataSource(config);
三、SQL语句优化
3.1 索引优化策略
索引是提升查询性能的最有效手段,但不当的索引会降低写性能。
索引设计原则:
- 最左前缀原则:复合索引必须从最左列开始匹配
- 区分度原则:选择区分度高的列建索引
- 覆盖索引:尽量让索引包含查询所有列
索引优化示例:
-- 原始查询(未优化)
SELECT user_id, order_amount, order_time
FROM orders
WHERE user_id = 12345
AND order_status = 'PAID'
AND order_time > '2024-01-01';
-- 优化1:创建合适的复合索引
CREATE INDEX idx_user_status_time ON orders(user_id, order_status, order_time);
-- 优化2:使用覆盖索引(避免回表)
CREATE INDEX idx_user_status_time_amount ON orders(user_id, order_status, order_time, order_amount);
-- 查看索引使用情况
EXPLAIN SELECT user_id, order_amount, order_time
FROM orders
WHERE user_id = 12345
AND order_status = 'PAID'
AND order_time > '2024-01-01';
EXPLAIN输出分析:
+----+-------------+--------+------------+-------+-----------------------------+-----------------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+-------------+-----------------------------+-----------------------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | orders | NULL | range | idx_user_status_time | idx_user_status_time | 10 | NULL | 1000 | 100.00 | Using where |
+----+-------------+--------+------------+-------+-----------------------------+-----------------------------+---------+------+------+----------+-------------+
关键指标说明:
- type:
ALL(全表扫描)最差,index(索引扫描)次之,range(范围扫描)较好,ref(索引查找)最好 - key:实际使用的索引
- rows:预估扫描行数(越少越好)
- Extra:
Using index(覆盖索引)最佳,Using where(需要回表)
3.2 避免索引失效的常见场景
索引失效示例:
-- 1. 对索引列进行函数操作
-- 失效示例
SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01';
-- 优化
SELECT * FROM orders WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00';
-- 2. 隐式类型转换
-- 失效示例(user_id是varchar类型,但传入数字)
SELECT * FROM users WHERE user_id = 12345;
-- 优化
SELECT * FROM users WHERE user_id = '12345';
-- 3. LIKE以通配符开头
-- 失效示例
SELECT * FROM products WHERE product_name LIKE '%手机';
-- 优化(使用全文索引或倒排索引)
ALTER TABLE products ADD FULLTEXT INDEX ft_product_name (product_name);
SELECT * FROM products WHERE MATCH(product_name) AGAINST('手机' IN BOOLEAN MODE);
-- 4. OR条件(除非所有条件都有索引)
-- 可能失效
SELECT * FROM orders WHERE order_id = 1 OR user_id = 123;
-- 优化
SELECT * FROM orders WHERE order_id = 1
UNION ALL
SELECT * FROM orders WHERE user_id = 123;
-- 5. 负向查询(!=, NOT IN, NOT LIKE)
-- 通常无法使用索引
SELECT * FROM orders WHERE status != 'CANCELLED';
-- 优化(改为正向查询)
SELECT * FROM orders WHERE status IN ('PAID', 'SHIPPED', 'DELIVERED');
3.3 分页查询优化
高并发场景下,深度分页性能极差。
问题示例:
-- 性能极差(扫描100万行,只返回10行)
SELECT * FROM orders ORDER BY order_id LIMIT 1000000, 10;
优化方案1:延迟关联
-- 先查主键,再关联详情
SELECT o.*
FROM orders o
INNER JOIN (
SELECT order_id
FROM orders
ORDER BY order_id
LIMIT 1000000, 10
) AS tmp ON o.order_id = tmp.order_id;
优化方案2:书签记录法
-- 应用层记录上次查询的最大ID
-- 第一页
SELECT * FROM orders WHERE order_id > 0 ORDER BY order_id LIMIT 10;
-- 第二页(应用层记录上一页最大ID=10)
SELECT * FROM orders WHERE order_id > 10 ORDER BY order_id LIMIT 10;
优化方案3:使用子查询
-- 使用子查询减少扫描范围
SELECT * FROM orders
WHERE order_id >= (
SELECT order_id FROM orders ORDER BY order_id LIMIT 1000000, 1
)
ORDER BY order_id LIMIT 10;
3.4 大批量数据操作优化
批量插入优化:
-- 低效:逐条插入
INSERT INTO orders (user_id, amount) VALUES (1, 100.00);
INSERT INTO orders (user_id, amount) VALUES (2, 200.00);
-- ... 10000次
-- 高效:批量插入
INSERT INTO orders (user_id, amount) VALUES
(1, 100.00),
(2, 200.00),
(3, 300.00),
-- ... 一次1000条
(1000, 100000.00);
-- 更高效:LOAD DATA INFILE(适用于初始化数据)
LOAD DATA LOCAL INFILE '/path/to/data.csv'
INTO TABLE orders
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(user_id, amount);
大批量更新优化:
-- 低效:逐条更新
UPDATE orders SET status = 'PAID' WHERE order_id = 1;
UPDATE orders SET status = 'PAID' WHERE order_id = 2;
-- ...
-- 高效:批量更新
UPDATE orders
SET status = 'PAID'
WHERE order_id IN (1, 2, 3, ..., 1000);
-- 更高效:使用CASE WHEN(不同值不同更新)
UPDATE orders
SET status = CASE order_id
WHEN 1 THEN 'PAID'
WHEN 2 THEN 'SHIPPED'
WHEN 3 THEN 'DELIVERED'
END
WHERE order_id IN (1, 2, 3);
四、事务与锁优化
4.1 事务设计原则
事务过长的危害:
- 长时间持有锁,阻塞其他事务
- 回滚段过大,消耗内存
- 可能导致死锁
优化事务设计:
// 优化前:长事务
@Transactional
public void processOrder(Long orderId) {
// 1. 查询订单(持有读锁)
Order order = orderDao.selectById(orderId);
// 2. 调用外部API(耗时操作,锁一直不释放)
paymentService.verifyPayment(order);
// 3. 更新状态
orderDao.updateStatus(orderId, "PAID");
}
// 优化后:短事务
public void processOrder(Long orderId) {
// 1. 在事务外执行耗时操作
paymentService.verifyPayment(order);
// 2. 缩短事务范围
transactionTemplate.execute(status -> {
Order order = orderDao.selectById(orderId);
orderDao.updateStatus(orderId, "PAID");
return null;
});
}
4.2 锁优化策略
InnoDB锁类型:
- 共享锁(S锁):读锁,多个事务可同时持有
- 排他锁(X锁):写锁,只有一个事务可持有
- 意向锁:表级锁,表示事务将在某行加锁
减少锁竞争的技巧:
-- 1. 使用乐观锁(适合读多写少)
ALTER TABLE orders ADD COLUMN version INT DEFAULT 0;
-- 更新时检查版本
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE order_id = 123 AND version = 0;
-- 如果更新失败(影响行数为0),说明已被其他事务修改
-- 2. 使用悲观锁(适合写多读少)
SELECT * FROM orders WHERE order_id = 123 FOR UPDATE;
-- 3. 批量操作减少锁时间
-- 优化前:逐条更新,锁竞争激烈
UPDATE orders SET status = 'PAID' WHERE order_id = 1;
UPDATE orders SET status = 'PAID' WHERE order_id = 2;
-- ...
-- 优化后:批量更新,减少锁次数
UPDATE orders SET status = 'PAID' WHERE order_id IN (1,2,3,...,1000);
死锁预防:
-- 死锁场景示例
-- 事务A
BEGIN;
UPDATE orders SET status = 'PAID' WHERE order_id = 1;
UPDATE order_items SET quantity = 10 WHERE order_id = 1;
COMMIT;
-- 事务B(同时执行)
BEGIN;
UPDATE order_items SET quantity = 10 WHERE order_id = 1;
UPDATE orders SET status = 'PAID' WHERE order_id = 1;
COMMIT;
-- 预防策略:固定加锁顺序
-- 统一按 orders -> order_items 顺序加锁
4.3 隔离级别选择
MySQL四种隔离级别:
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别(会话级)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置隔离级别(全局级)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
隔离级别对比:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 最高 | 无事务要求 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 高 | 大多数场景 |
| REPEATABLE READ | 不可能 | 不可能 | 可能 | 中 | 需要一致性读 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 低 | 强一致性 |
推荐配置:
- 互联网应用:
READ COMMITTED(平衡性能和一致性) - 金融系统:
REPEATABLE READ(保证数据一致性) - 报表系统:
READ COMMITTED(避免长事务)
五、监控与诊断
5.1 实时性能监控
MySQL内置监控命令:
-- 1. 查看当前连接状态
SHOW PROCESSLIST;
-- 2. 查看InnoDB状态(重要)
SHOW ENGINE INNODB STATUS\G
-- 3. 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 4. 查看锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 5. 查看缓冲池状态
SHOW ENGINE INNODB STATUS\G
-- 关注:BUFFER POOL AND MEMORY, ROW OPERATIONS
性能模式(Performance Schema)监控:
-- 启用性能模式(默认开启)
SHOW VARIABLES LIKE 'performance_schema';
-- 查看最耗时的SQL
SELECT
DIGEST_TEXT,
COUNT_STAR,
AVG_TIMER_WAIT/1000000000000 AS avg_time_sec,
MAX_TIMER_WAIT/1000000000000 AS max_time_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
-- 查看表I/O统计
SELECT
OBJECT_SCHEMA,
OBJECT_NAME,
COUNT_READ,
COUNT_WRITE,
SUM_NUMBER_OF_BYTES_READ,
SUM_NUMBER_OF_BYTES_WRITE
FROM performance_schema.table_io_waits_summary_by_table
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- 查看索引使用情况
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 INDEX_NAME IS NOT NULL
ORDER BY COUNT_FETCH DESC;
5.2 慢查询分析
开启慢查询日志:
# my.cnf
slow_query_log = ON
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1 # 记录超过1秒的查询
log_queries_not_using_indexes = ON
log_slow_admin_statements = ON
log_slow_slave_statements = ON
使用mysqldumpslow分析:
# 查看最慢的10条查询
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# 查看出现次数最多的10条查询
mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
# 查看包含"orders"表的慢查询
mysqldumpslow -g "orders" /var/log/mysql/slow.log
使用pt-query-digest分析(Percona Toolkit):
# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
# 分析实时查询
pt-query-digest --processlist h=localhost --interval 0.1
5.3 性能问题诊断流程
诊断步骤:
- 查看当前负载
-- CPU使用率
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
-- I/O等待
SHOW GLOBAL STATUS LIKE 'Innodb_data_pending_reads';
SHOW GLOBAL STATUS LIKE 'Innodb_data_pending_writes';
- 检查锁等待
-- 查看当前锁等待
SELECT
r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.INNODB_LOCK_WAITS w
INNER JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
- 分析I/O瓶颈
-- 查看I/O统计
SHOW GLOBAL STATUS LIKE 'Innodb_data_reads';
SHOW GLOBAL STATUS LIKE 'Innodb_data_writes';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';
-- 计算缓冲池命中率
-- (Innodb_buffer_pool_read_requests - Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests * 100
- 检查临时表使用
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
SHOW GLOBAL STATUS LIKE 'Created_tmp_tables';
-- 磁盘临时表比例应小于10%
-- Created_tmp_disk_tables / Created_tmp_tables * 100
六、高级优化技巧
6.1 覆盖索引与索引下推
覆盖索引示例:
-- 创建覆盖索引
CREATE INDEX idx_cover ON orders(user_id, order_status, order_amount, create_time);
-- 查询只需要扫描索引,无需回表
EXPLAIN SELECT user_id, order_status, order_amount
FROM orders
WHERE user_id = 12345 AND order_status = 'PAID';
-- Extra: Using index
索引下推(ICP,MySQL 5.6+):
-- 索引下推自动启用,无需配置
-- 查询条件:user_id = 12345 AND order_status = 'PAID' AND order_amount > 100
-- 索引:(user_id, order_status, order_amount)
-- MySQL会在索引层面过滤order_amount,减少回表次数
EXPLAIN SELECT * FROM orders
WHERE user_id = 12345
AND order_status = 'PAID'
AND order_amount > 100;
-- Extra: Using index condition
6.2 MRR(Multi-Range Read)优化
MRR将随机I/O转换为顺序I/O,提升范围查询性能。
-- 查看MRR状态
SHOW VARIABLES LIKE 'optimizer_switch';
-- 确保mrr=on, mrr_cost_based=off
SET SESSION optimizer_switch = 'mrr=on,mrr_cost_based=off';
-- MRR适用场景:二级索引范围扫描后回表
EXPLAIN SELECT * FROM orders
WHERE user_id BETWEEN 1000 AND 2000
AND order_status = 'PAID';
-- Extra: Using MRR
6.3 BKA(Batched Key Access)优化
BKA结合MRR,批量获取索引键,提升表连接性能。
-- 启用BKA
SET SESSION optimizer_switch = 'batched_key_access=on';
-- BKA适用于JOIN查询
EXPLAIN SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.order_status = 'PAID';
-- Extra: Using join buffer
6.4 分区表优化
Range分区示例:
-- 按时间分区
CREATE TABLE orders_partitioned (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME,
PRIMARY KEY (order_id, create_time)
) PARTITION BY RANGE (YEAR(create_time)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- 查询时自动分区裁剪
EXPLAIN SELECT * FROM orders_partitioned
WHERE create_time >= '2024-01-01' AND create_time < '2024-02-01';
-- partitions: p2024
List分区示例:
-- 按地区分区
CREATE TABLE orders_by_region (
order_id BIGINT,
user_id BIGINT,
region VARCHAR(20),
amount DECIMAL(10,2),
PRIMARY KEY (order_id, region)
) PARTITION BY LIST COLUMNS(region) (
PARTITION p_north VALUES IN ('北京', '天津', '河北'),
PARTITION p_east VALUES IN ('上海', '江苏', '浙江'),
PARTITION p_south VALUES IN ('广东', '广西', '福建'),
PARTITION p_west VALUES IN ('四川', '重庆', '陕西')
);
6.5 全文索引优化
创建全文索引:
-- 创建全文索引
ALTER TABLE products ADD FULLTEXT INDEX ft_product_name (product_name);
-- 自然语言搜索
SELECT * FROM products
WHERE MATCH(product_name) AGAINST('手机' IN NATURAL LANGUAGE MODE);
-- 布尔模式搜索
SELECT * FROM products
WHERE MATCH(product_name) AGAINST('+手机 -苹果' IN BOOLEAN MODE);
-- + 表示必须包含,- 表示排除
-- 查看全文索引状态
SHOW INDEX FROM products;
全文索引配置:
# my.cnf
[mysqld]
innodb_ft_min_token_size = 2 # 最小词长度(默认3)
ft_min_word_len = 2 # MyISAM最小词长度
七、应用层优化策略
7.1 缓存策略
Redis缓存示例:
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String ORDER_CACHE_PREFIX = "order:";
private static final long CACHE_TTL = 300; // 5分钟
public Order getOrder(Long orderId) {
String cacheKey = ORDER_CACHE_PREFIX + orderId;
// 1. 先查缓存
Order order = (Order) redisTemplate.opsForValue().get(cacheKey);
if (order != null) {
return order;
}
// 2. 缓存未命中,查数据库
order = orderDao.selectById(orderId);
if (order != null) {
// 3. 写入缓存
redisTemplate.opsForValue().set(cacheKey, order, CACHE_TTL, TimeUnit.SECONDS);
}
return order;
}
public void updateOrder(Order order) {
// 1. 更新数据库
orderDao.update(order);
// 2. 删除缓存(避免脏数据)
String cacheKey = ORDER_CACHE_PREFIX + order.getOrderId();
redisTemplate.delete(cacheKey);
}
}
缓存穿透、击穿、雪崩防护:
// 1. 缓存穿透(查询不存在的数据)
public Order getOrder(Long orderId) {
String cacheKey = ORDER_CACHE_PREFIX + orderId;
// 缓存空对象(TTL较短)
Object cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if (cached instanceof NullObject) {
return null;
}
return (Order) cached;
}
Order order = orderDao.selectById(orderId);
if (order == null) {
// 缓存空对象,防止穿透
redisTemplate.opsForValue().set(cacheKey, new NullObject(), 60, TimeUnit.SECONDS);
return null;
}
redisTemplate.opsForValue().set(cacheKey, order, CACHE_TTL, TimeUnit.SECONDS);
return order;
}
// 2. 缓存击穿(热点key过期)
public Order getOrderWithLock(Long orderId) {
String cacheKey = ORDER_CACHE_PREFIX + orderId;
// 先查缓存
Order order = (Order) redisTemplate.opsForValue().get(cacheKey);
if (order != null) {
return order;
}
// 使用分布式锁
String lockKey = "lock:" + orderId;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查
order = (Order) redisTemplate.opsForValue().get(cacheKey);
if (order != null) {
return order;
}
// 查询数据库
order = orderDao.selectById(orderId);
if (order != null) {
redisTemplate.opsForValue().set(cacheKey, order, CACHE_TTL, TimeUnit.SECONDS);
}
return order;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 等待并重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getOrder(orderId); // 递归重试
}
}
// 3. 缓存雪崩(大量key同时过期)
// 解决方案:随机TTL
public void setCacheWithRandomTTL(String key, Object value) {
long ttl = CACHE_TTL + new Random().nextInt(300); // 5-10分钟随机
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
7.2 批量处理与异步化
批量处理示例:
// 优化前:逐条处理
public void processOrders(List<Order> orders) {
for (Order order : orders) {
orderDao.updateStatus(order.getOrderId(), "PROCESSED");
}
}
// 优化后:批量处理
public void processOrdersBatch(List<Order> orders) {
// 使用JDBC批量处理
String sql = "UPDATE orders SET status = ? WHERE order_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
conn.setAutoCommit(false);
for (Order order : orders) {
ps.setString(1, "PROCESSED");
ps.setLong(2, order.getOrderId());
ps.addBatch();
// 每1000条提交一次
if (orders.indexOf(order) % 1000 == 0) {
ps.executeBatch();
conn.commit();
}
}
// 提交剩余
ps.executeBatch();
conn.commit();
} catch (SQLException e) {
// 异常处理
}
}
异步处理示例:
// 使用线程池异步处理
@Service
public class OrderAsyncService {
private final ExecutorService executor = new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public void processOrderAsync(Long orderId) {
executor.submit(() -> {
try {
// 耗时操作
Order order = orderDao.selectById(orderId);
paymentService.verifyPayment(order);
orderDao.updateStatus(orderId, "PAID");
} catch (Exception e) {
// 记录日志,重试机制
log.error("处理订单失败: {}", orderId, e);
}
});
}
}
7.3 读写分离中间件
ShardingSphere配置示例:
# sharding.yaml
dataSources:
ds_0: !!com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://master:3306/mydb
username: root
password: root
ds_1: !!com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://slave1:3306/mydb
username: root
password: root
ds_2: !!com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
jdbcUrl: jdbc:mysql://slave2:3306/mydb
username: root
password: root
shardingRule:
tables:
orders:
actualDataNodes: ds_0.orders_$->{0..15}
tableStrategy:
inline:
shardingColumn: user_id
algorithmExpression: orders_$->{user_id % 16}
databaseStrategy:
inline:
shardingColumn: user_id
algorithmExpression: ds_$->{user_id % 2}
defaultDatabaseStrategy:
none:
defaultTableStrategy:
none:
# 读写分离
masterSlaveRules:
ds_0:
masterDataSourceName: ds_0
slaveDataSourceNames:
- ds_1
- ds_2
loadBalanceAlgorithmType: ROUND_ROBIN
八、性能测试与基准
8.1 sysbench压测
安装sysbench:
# CentOS
yum install -y sysbench
# Ubuntu
apt-get install -y sysbench
准备测试数据:
# 准备测试表
sysbench oltp_read_write --mysql-host=localhost --mysql-user=root \
--mysql-password=password --mysql-db=testdb --tables=10 --table-size=1000000 prepare
执行测试:
# 读写混合测试
sysbench oltp_read_write --mysql-host=localhost --mysql-user=root \
--mysql-password=password --mysql-db=testdb --tables=10 --table-size=1000000 \
--threads=100 --time=300 --report-interval=10 run
# 只读测试
sysbench oltp_read_only --mysql-host=localhost --mysql-user=root \
--mysql-password=password --mysql-db=testdb --tables=10 --table-size=1000000 \
--threads=100 --time=300 run
# 写入测试
sysbench oltp_write_only --mysql-host=localhost --mysql-user=root \
--mysql-password=password --mysql-db=testdb --tables=10 --table-size=1000000 \
--threads=100 --time=300 run
清理测试数据:
sysbench oltp_read_write --mysql-host=localhost --mysql-user=root \
--mysql-password=password --mysql-db=testdb cleanup
8.2 自定义性能测试脚本
Python测试脚本示例:
import mysql.connector
import time
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
class MySQLBenchmark:
def __init__(self, host, user, password, database):
self.config = {
'host': host,
'user': user,
'password': password,
'database': database
}
def get_connection(self):
return mysql.connector.connect(**self.config)
def test_read_query(self, thread_id, num_queries):
"""测试读查询性能"""
conn = self.get_connection()
cursor = conn.cursor()
start_time = time.time()
success_count = 0
for i in range(num_queries):
try:
user_id = (thread_id * 1000 + i) % 1000000
cursor.execute(
"SELECT * FROM orders WHERE user_id = %s ORDER BY order_id LIMIT 10",
(user_id,)
)
cursor.fetchall()
success_count += 1
except Exception as e:
print(f"Thread {thread_id} error: {e}")
end_time = time.time()
cursor.close()
conn.close()
return {
'thread_id': thread_id,
'queries': success_count,
'time': end_time - start_time,
'qps': success_count / (end_time - start_time)
}
def test_write_query(self, thread_id, num_queries):
"""测试写查询性能"""
conn = self.get_connection()
cursor = conn.cursor()
start_time = time.time()
success_count = 0
for i in range(num_queries):
try:
user_id = (thread_id * 1000 + i) % 1000000
amount = 100.0 + i
cursor.execute(
"INSERT INTO orders (user_id, amount, status) VALUES (%s, %s, %s)",
(user_id, amount, 'PENDING')
)
conn.commit()
success_count += 1
except Exception as e:
print(f"Thread {thread_id} error: {e}")
conn.rollback()
end_time = time.time()
cursor.close()
conn.close()
return {
'thread_id': thread_id,
'queries': success_count,
'time': end_time - start_time,
'qps': success_count / (end_time - start_time)
}
def run_benchmark(self, threads=10, queries_per_thread=100, test_type='read'):
"""运行基准测试"""
print(f"开始测试 - 线程数: {threads}, 每线程查询: {queries_per_thread}, 类型: {test_type}")
start_time = time.time()
with ThreadPoolExecutor(max_workers=threads) as executor:
futures = []
for i in range(threads):
if test_type == 'read':
future = executor.submit(self.test_read_query, i, queries_per_thread)
else:
future = executor.submit(self.test_write_query, i, queries_per_thread)
futures.append(future)
results = []
for future in as_completed(futures):
results.append(future.result())
total_time = time.time() - start_time
total_queries = sum(r['queries'] for r in results)
total_qps = total_queries / total_time
print(f"\n测试结果:")
print(f"总耗时: {total_time:.2f}秒")
print(f"总查询数: {total_queries}")
print(f"总QPS: {total_qps:.2f}")
print(f"平均QPS/线程: {total_qps/threads:.2f}")
return results
# 使用示例
if __name__ == '__main__':
benchmark = MySQLBenchmark('localhost', 'root', 'password', 'testdb')
# 读测试
benchmark.run_benchmark(threads=20, queries_per_thread=500, test_type='read')
# 写测试
benchmark.run_benchmark(threads=20, queries_per_thread=500, test_type='write')
九、总结与最佳实践
9.1 高并发优化检查清单
架构层:
- [ ] 是否采用读写分离架构?
- [ ] 数据量是否超过千万级?是否需要分库分表?
- [ ] 是否使用缓存层(Redis/Memcached)?
- [ ] 应用层是否实现连接池?
配置层:
- [ ] innodb_buffer_pool_size是否设置为物理内存的70-80%?
- [ ] max_connections是否足够?
- [ ] innodb_flush_log_at_trx_commit是否根据业务需求调整?
- [ ] 慢查询日志是否开启?
SQL层:
- [ ] 所有查询都有合适的索引?
- [ ] 避免SELECT *,只查询需要的列?
- [ ] 复合索引遵循最左前缀原则?
- [ ] 避免在索引列上使用函数?
- [ ] 分页查询是否优化?
事务层:
- [ ] 事务是否足够短?
- [ ] 是否避免长事务?
- [ ] 隔离级别是否合适?
- [ ] 是否有死锁预防机制?
监控层:
- [ ] 是否监控慢查询?
- [ ] 是否监控锁等待?
- [ ] 是否监控缓冲池命中率?
- [ ] 是否监控连接数?
9.2 性能优化黄金法则
- 先测量,再优化:不要凭感觉优化,用数据说话
- 80/20原则:80%的性能问题来自20%的SQL
- 索引不是越多越好:每个索引都会影响写性能
- 缓存是银弹:但要注意缓存一致性
- 架构决定上限:单机优化有极限,架构优化无止境
9.3 持续优化建议
- 定期审查慢查询日志:每周分析一次,优化TOP 10慢查询
- 定期检查索引使用情况:删除未使用的索引
- 定期压力测试:模拟高并发场景,发现潜在问题
- 保持MySQL版本更新:新版本通常有更好的优化器
- 建立性能基线:记录正常情况下的性能指标,便于对比
通过以上策略的综合应用,可以显著提升MySQL在高并发场景下的性能,避免系统崩溃,确保快速响应。记住,性能优化是一个持续的过程,需要根据业务发展和技术演进不断调整和完善。
