引言
在当今互联网应用中,高并发场景已成为常态。无论是电商平台的秒杀活动、社交媒体的热点事件,还是金融系统的交易高峰,数据库作为数据存储和访问的核心,其性能和稳定性直接决定了整个系统的可用性和用户体验。MySQL作为最流行的开源关系型数据库之一,在高并发场景下面临着严峻的挑战:查询响应时间变长、锁竞争加剧、连接数激增、甚至出现服务不可用等问题。
本文将深入探讨MySQL在高并发场景下的性能优化与稳定性保障策略,从架构设计、配置调优、SQL优化、缓存策略、监控告警等多个维度进行全面分析,并结合实际案例提供可落地的解决方案。
一、高并发场景下的MySQL性能瓶颈分析
1.1 常见性能瓶颈点
在高并发场景下,MySQL的性能瓶颈通常出现在以下几个方面:
- CPU瓶颈:大量复杂查询、排序、聚合操作导致CPU使用率过高
- I/O瓶颈:频繁的磁盘读写操作,尤其是随机I/O
- 内存瓶颈:缓冲池(Buffer Pool)不足,导致频繁的磁盘访问
- 锁竞争:行锁、表锁、元数据锁的争用
- 连接数瓶颈:连接数过多导致资源耗尽
- 网络瓶颈:大量数据传输导致网络延迟
1.2 性能分析工具
在进行优化前,需要先定位瓶颈所在。以下是常用的性能分析工具:
-- 1. 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 2. 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
-- 3. 查看InnoDB状态
SHOW ENGINE INNODB STATUS;
-- 4. 查看性能模式(Performance Schema)
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;
-- 5. 使用sys schema(MySQL 5.7+)
SELECT * FROM sys.statement_analysis ORDER BY exec_count DESC LIMIT 10;
二、架构层面的优化策略
2.1 读写分离架构
读写分离是应对高并发读操作的最有效策略之一。通过将读请求分发到多个从库,减轻主库压力。
实现方案:
- 基于中间件的读写分离(如MyCat、ShardingSphere)
- 应用层读写分离(通过数据源路由)
- MySQL原生复制+负载均衡
代码示例(Spring Boot + ShardingSphere配置):
# 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-host:3306/db
username: root
password: password
slave0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://slave0-host:3306/db
username: root
password: password
slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://slave1-host:3306/db
username: root
password: password
rules:
readwrite-splitting:
data-sources:
ds0:
type: Static
props:
write-data-source-name: master
read-data-source-names: slave0,slave1
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
2.2 分库分表策略
当单表数据量超过千万级别时,需要考虑分库分表。常见的分片策略包括:
- 垂直分库:按业务模块拆分数据库
- 水平分库:按数据特征拆分到不同数据库
- 水平分表:将大表拆分为多个小表
分片键选择原则:
- 选择查询频率高的字段
- 选择值分布均匀的字段
- 避免使用自增ID作为分片键(热点问题)
代码示例(ShardingSphere分表配置):
spring:
shardingsphere:
rules:
sharding:
tables:
user_order:
actual-data-nodes: ds0.user_order_$->{0..9}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: user_id_mod
sharding-algorithms:
user_id_mod:
type: MOD
props:
sharding-count: 10
2.3 缓存策略
引入缓存层可以显著降低数据库压力。常见的缓存策略:
- 本地缓存:Guava Cache、Caffeine
- 分布式缓存:Redis、Memcached
- 多级缓存:本地缓存 + 分布式缓存
缓存设计要点:
- 缓存穿透:布隆过滤器
- 缓存雪崩:随机过期时间
- 缓存击穿:互斥锁或逻辑过期
代码示例(Redis缓存实现):
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper;
private static final String USER_CACHE_KEY = "user:";
private static final long CACHE_EXPIRE_TIME = 300; // 5分钟
public User getUserById(Long userId) {
String key = USER_CACHE_KEY + userId;
// 1. 先从缓存获取
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 2. 缓存未命中,查询数据库
user = userMapper.selectById(userId);
if (user != null) {
// 3. 写入缓存,设置随机过期时间防止雪崩
long expireTime = CACHE_EXPIRE_TIME + new Random().nextInt(60);
redisTemplate.opsForValue().set(key, user, expireTime, TimeUnit.SECONDS);
}
return user;
}
public void updateUser(User user) {
// 更新数据库
userMapper.updateById(user);
// 删除缓存(或更新缓存)
String key = USER_CACHE_KEY + user.getId();
redisTemplate.delete(key);
}
}
三、数据库配置优化
3.1 InnoDB核心参数调优
3.1.1 缓冲池(Buffer Pool)配置
缓冲池是InnoDB最重要的性能参数,决定了数据在内存中的缓存能力。
-- 查看当前缓冲池大小
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 推荐配置(根据服务器内存调整)
-- 通常设置为物理内存的50%-70%
SET GLOBAL innodb_buffer_pool_size = 4G; -- 4GB内存服务器
-- 查看缓冲池使用情况
SELECT
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_total') AS total_pages,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_data') AS data_pages,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty') AS dirty_pages,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_pages_free') AS free_pages;
3.1.2 日志文件配置
日志文件大小影响写入性能和恢复时间。
-- 查看当前日志文件配置
SHOW VARIABLES LIKE 'innodb_log_file_size';
SHOW VARIABLES LIKE 'innodb_log_buffer_size';
-- 推荐配置
SET GLOBAL innodb_log_file_size = 2G; -- 日志文件大小
SET GLOBAL innodb_log_buffer_size = 64M; -- 日志缓冲区大小
-- 查看日志写入性能
SELECT
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_log_waits') AS log_waits,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_log_write_requests') AS write_requests,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_log_writes') AS log_writes;
3.1.3 并发线程配置
-- 查看当前线程配置
SHOW VARIABLES LIKE 'innodb_thread_concurrency';
SHOW VARIABLES LIKE 'innodb_read_io_threads';
SHOW VARIABLES LIKE 'innodb_write_io_threads';
-- 推荐配置(根据CPU核心数调整)
SET GLOBAL innodb_thread_concurrency = 0; -- 0表示自动管理
SET GLOBAL innodb_read_io_threads = 8; -- 读线程数
SET GLOBAL innodb_write_io_threads = 8; -- 写线程数
3.2 连接池配置
连接池是应用与数据库之间的桥梁,合理的配置可以避免连接数过多或过少的问题。
HikariCP连接池配置示例:
spring:
datasource:
hikari:
# 连接池名称
pool-name: HikariCP-Connection-Pool
# 最小连接数
minimum-idle: 10
# 最大连接数(根据并发量调整)
maximum-pool-size: 50
# 连接超时时间(毫秒)
connection-timeout: 30000
# 空闲连接最大存活时间(毫秒)
idle-timeout: 600000
# 连接最大生命周期(毫秒)
max-lifetime: 1800000
# 连接测试查询
connection-test-query: SELECT 1
# 连接泄漏检测
leak-detection-threshold: 60000
# 连接预热
initialization-fail-timeout: 1
# 连接获取超时
acquire-timeout: 30000
连接数计算公式:
最大连接数 = (核心数 * 2) + 有效磁盘数
3.3 查询缓存配置
注意:MySQL 8.0已移除查询缓存,建议使用应用层缓存或Redis。
对于MySQL 5.7及以下版本:
-- 查看查询缓存状态
SHOW VARIABLES LIKE 'query_cache_type';
SHOW VARIABLES LIKE 'query_cache_size';
-- 推荐配置(如果使用查询缓存)
SET GLOBAL query_cache_type = 1; -- 启用查询缓存
SET GLOBAL query_cache_size = 128M; -- 缓存大小
-- 查看查询缓存命中率
SELECT
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Qcache_hits') AS hits,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Qcache_inserts') AS inserts,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Qcache_not_cached') AS not_cached;
四、SQL优化策略
4.1 索引优化
4.1.1 索引设计原则
- 最左前缀原则:复合索引必须从最左列开始使用
- 选择性原则:选择区分度高的列建立索引
- 覆盖索引:尽量使用覆盖索引避免回表
- 避免冗余索引:定期检查并删除重复索引
4.1.2 索引优化实战
问题场景: 用户订单查询,需要按用户ID和订单状态查询,同时按创建时间排序。
优化前SQL:
SELECT * FROM orders
WHERE user_id = 123 AND status = 'PAID'
ORDER BY create_time DESC
LIMIT 20;
优化方案:
-- 1. 创建复合索引(注意顺序)
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time DESC);
-- 2. 使用覆盖索引(避免回表)
SELECT order_id, user_id, status, create_time, amount
FROM orders
WHERE user_id = 123 AND status = 'PAID'
ORDER BY create_time DESC
LIMIT 20;
-- 3. 查看执行计划
EXPLAIN SELECT order_id, user_id, status, create_time, amount
FROM orders
WHERE user_id = 123 AND status = 'PAID'
ORDER BY create_time DESC
LIMIT 20;
执行计划分析:
+----+-------------+--------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | orders | NULL | ref | idx_user_status_time | idx_user_status_time | 10 | const | 100 | 100.00 | Using where |
+----+-------------+--------+------------+------+-----------------------+-----------------------+---------+-------+------+----------+-------------+
4.2 查询语句优化
4.2.1 避免使用SELECT *
问题: 使用SELECT *会返回所有列,增加网络传输和内存消耗。
优化前:
SELECT * FROM users WHERE id = 1;
优化后:
SELECT id, username, email, created_at FROM users WHERE id = 1;
4.2.2 避免在WHERE子句中对字段进行函数操作
问题: 对字段使用函数会导致索引失效。
优化前:
SELECT * FROM orders WHERE DATE(create_time) = '2023-01-01';
优化后:
SELECT * FROM orders
WHERE create_time >= '2023-01-01 00:00:00'
AND create_time < '2023-01-02 00:00:00';
4.2.3 使用JOIN替代子查询
问题: 子查询可能导致性能问题,特别是在高并发场景下。
优化前:
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM users WHERE status = 'ACTIVE');
优化后:
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 'ACTIVE';
4.3 分页优化
4.3.1 传统分页的问题
-- 传统分页(深度分页问题)
SELECT * FROM orders ORDER BY create_time DESC LIMIT 1000000, 20;
问题分析:
- MySQL需要扫描1000002条记录,然后丢弃前1000000条
- 随着偏移量增大,性能急剧下降
4.3.2 优化方案
方案1:延迟关联
SELECT o.* FROM orders o
INNER JOIN (
SELECT order_id FROM orders
ORDER BY create_time DESC
LIMIT 1000000, 20
) AS tmp ON o.order_id = tmp.order_id;
方案2:记录上一页最后一条记录
-- 假设上一页最后一条记录的create_time是'2023-01-01 12:00:00'
SELECT * FROM orders
WHERE create_time < '2023-01-01 12:00:00'
ORDER BY create_time DESC
LIMIT 20;
方案3:使用游标分页(推荐)
-- 第一页
SELECT * FROM orders
ORDER BY create_time DESC, id DESC
LIMIT 20;
-- 第二页(使用上一页最后一条记录的create_time和id)
SELECT * FROM orders
WHERE (create_time < '2023-01-01 12:00:00')
OR (create_time = '2023-01-01 12:00:00' AND id < 12345)
ORDER BY create_time DESC, id DESC
LIMIT 20;
4.4 批量操作优化
4.4.1 批量插入
优化前(逐条插入):
// 每次插入一条,性能差
for (Order order : orderList) {
orderMapper.insert(order);
}
优化后(批量插入):
// 批量插入,减少网络往返
orderMapper.batchInsert(orderList);
// MyBatis XML配置
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO orders (user_id, amount, status, create_time) VALUES
<foreach collection="list" item="order" separator=",">
(#{order.userId}, #{order.amount}, #{order.status}, #{order.createTime})
</foreach>
</insert>
4.4.2 批量更新
优化前:
UPDATE orders SET status = 'PAID' WHERE id = 1;
UPDATE orders SET status = 'PAID' WHERE id = 2;
UPDATE orders SET status = 'PAID' WHERE id = 3;
优化后:
-- 使用CASE WHEN批量更新
UPDATE orders
SET status = CASE
WHEN id = 1 THEN 'PAID'
WHEN id = 2 THEN 'PAID'
WHEN id = 3 THEN 'PAID'
ELSE status
END
WHERE id IN (1, 2, 3);
-- 或者使用INSERT ON DUPLICATE KEY UPDATE
INSERT INTO orders (id, status) VALUES
(1, 'PAID'), (2, 'PAID'), (3, 'PAID')
ON DUPLICATE KEY UPDATE status = VALUES(status);
五、锁优化策略
5.1 锁类型分析
5.1.1 InnoDB锁类型
- 共享锁(S锁):读锁,多个事务可以同时持有
- 排他锁(X锁):写锁,只有一个事务可以持有
- 意向锁:表级锁,用于快速判断是否存在行锁
- 记录锁:锁定索引记录
- 间隙锁:锁定索引记录之间的间隙
- 临键锁:记录锁+间隙锁(InnoDB默认)
5.1.2 查看锁信息
-- 查看当前锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
SELECT * FROM information_schema.INNODB_TRX;
-- 查看死锁信息
SHOW ENGINE INNODB STATUS;
5.2 锁优化策略
5.2.1 减少锁持有时间
问题: 长事务导致锁持有时间过长,增加锁竞争。
优化前:
@Transactional
public void processOrder(Long orderId) {
// 1. 查询订单
Order order = orderMapper.selectById(orderId);
// 2. 调用外部服务(耗时操作)
externalService.process(order);
// 3. 更新订单状态
order.setStatus("PROCESSED");
orderMapper.updateById(order);
}
优化后:
public void processOrder(Long orderId) {
// 1. 查询订单(非事务)
Order order = orderMapper.selectById(orderId);
// 2. 调用外部服务(事务外)
externalService.process(order);
// 3. 在短事务中更新
transactionTemplate.execute(status -> {
Order orderInTx = orderMapper.selectById(orderId);
orderInTx.setStatus("PROCESSED");
orderMapper.updateById(orderInTx);
return null;
});
}
5.2.2 避免间隙锁
问题: 范围查询或未使用索引的查询会导致间隙锁。
优化前:
-- 未使用索引,导致全表扫描和间隙锁
UPDATE orders SET status = 'PAID' WHERE amount > 1000;
优化后:
-- 使用索引,减少锁范围
CREATE INDEX idx_amount ON orders(amount);
UPDATE orders SET status = 'PAID' WHERE amount > 1000;
5.2.3 使用乐观锁
场景: 低冲突场景,减少锁竞争。
// 1. 添加版本号字段
ALTER TABLE orders ADD COLUMN version INT DEFAULT 1;
// 2. 更新时检查版本号
public boolean updateOrderWithOptimisticLock(Order order) {
int affected = orderMapper.updateWithVersion(
order.getId(),
order.getStatus(),
order.getVersion()
);
return affected > 0;
}
// MyBatis XML配置
<update id="updateWithVersion">
UPDATE orders
SET status = #{status}, version = version + 1
WHERE id = #{id} AND version = #{version}
</update>
六、稳定性保障策略
6.1 监控告警体系
6.1.1 关键监控指标
- 连接数监控
-- 监控连接数使用率
SELECT
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Threads_connected') AS current_connections,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Max_used_connections') AS max_connections,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Threads_running') AS running_threads;
- 慢查询监控
-- 查看慢查询统计
SELECT
DIGEST_TEXT,
COUNT_STAR,
AVG_TIMER_WAIT/1000000000000 AS avg_time_sec,
SUM_ROWS_EXAMINED,
SUM_ROWS_SENT
FROM performance_schema.events_statements_summary_by_digest
WHERE COUNT_STAR > 0
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 10;
- InnoDB状态监控
-- 查看InnoDB缓冲池命中率
SELECT
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests') AS read_requests,
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') AS buffer_reads,
ROUND((1 - (SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads') /
(SELECT VARIABLE_VALUE FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests')) * 100, 2) AS hit_rate;
6.1.2 监控工具集成
Prometheus + Grafana监控方案:
# prometheus.yml 配置
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['mysql-exporter:9104']
metrics_path: /metrics
params:
collect[]:
- global_status
- global_variables
- innodb_metrics
- performance_schema
MySQL Exporter配置:
# 启动mysql_exporter
docker run -d \
--name mysql-exporter \
-p 9104:9104 \
-e DATA_SOURCE_NAME="user:password@(mysql-host:3306)/" \
prom/mysqld-exporter \
--collect.global_status \
--collect.global_variables \
--collect.innodb_metrics \
--collect.performance_schema
6.2 高可用架构
6.2.1 主从复制
配置主从复制:
-- 主库配置
-- my.cnf
[mysqld]
server-id = 1
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 7
-- 从库配置
-- my.cnf
server-id = 2
relay_log = mysql-relay-bin
read_only = 1
-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
-- 查看主库状态
SHOW MASTER STATUS;
-- 从库配置复制
CHANGE MASTER TO
MASTER_HOST='master-host',
MASTER_USER='repl',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=1234;
-- 启动复制
START SLAVE;
6.2.2 MHA高可用方案
MHA(Master High Availability)配置:
# 1. 安装MHA
wget https://github.com/yoshinorim/mha4mysql-manager/releases/download/v0.58/mha4mysql-manager-0.58-0.el7.noarch.rpm
wget https://github.com/yoshinorim/mha4mysql-node/releases/download/v0.58/mha4mysql-node-0.58-0.el7.noarch.rpm
# 2. 配置MHA Manager
cat > /etc/mha/app1.cnf << EOF
[server default]
manager_workdir=/var/log/mha/app1
manager_log=/var/log/mha/app1/manager.log
user=root
password=password
ping_interval=1
repl_password=password
repl_user=repl
[server1]
hostname=master-host
candidate_master=1
[server2]
hostname=slave1-host
candidate_master=1
[server3]
hostname=slave2-host
no_master=1
EOF
# 3. 启动MHA
masterha_manager --conf=/etc/mha/app1.cnf
6.3 备份与恢复策略
6.3.1 物理备份(Percona XtraBackup)
# 全量备份
xtrabackup --backup --user=root --password=password --target-dir=/backup/full
# 增量备份
xtrabackup --backup --user=root --password=password --target-dir=/backup/incremental \
--incremental-basedir=/backup/full
# 准备备份
xtrabackup --prepare --apply-log-only --target-dir=/backup/full
xtrabackup --prepare --apply-log-only --target-dir=/backup/full \
--incremental-dir=/backup/incremental
# 恢复
systemctl stop mysql
rm -rf /var/lib/mysql/*
xtrabackup --copy-back --target-dir=/backup/full
chown -R mysql:mysql /var/lib/mysql
systemctl start mysql
6.3.2 逻辑备份(mysqldump)
# 全量备份
mysqldump -u root -p --single-transaction --master-data=2 \
--databases mydb > /backup/full_$(date +%Y%m%d).sql
# 增量备份(基于binlog)
mysqlbinlog --read-from-remote-server --raw \
--host=master-host --user=repl --password=password \
--start-position=1234 /backup/binlog/
# 恢复
mysql -u root -p mydb < /backup/full_20230101.sql
mysqlbinlog /backup/binlog/mysql-bin.000001 | mysql -u root -p mydb
6.4 故障处理预案
6.4.1 主从延迟处理
问题: 主从复制延迟导致数据不一致。
解决方案:
-- 1. 查看从库状态
SHOW SLAVE STATUS\G
-- 2. 查看延迟
SELECT
Seconds_Behind_Master,
Relay_Log_File,
Relay_Log_Pos,
Master_Log_File,
Read_Master_Log_Pos
FROM information_schema.SLAVE_STATUS;
-- 3. 优化从库配置
SET GLOBAL slave_parallel_workers = 8; -- 并行复制
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK'; -- 逻辑时钟并行
6.4.2 死锁处理
问题: 死锁导致事务回滚。
解决方案:
-- 1. 查看死锁信息
SHOW ENGINE INNODB STATUS;
-- 2. 分析死锁日志
-- 在my.cnf中配置
innodb_print_all_deadlocks = 1
-- 3. 应用层重试机制
public void executeWithRetry(Runnable task, int maxRetries) {
int retryCount = 0;
while (retryCount < maxRetries) {
try {
task.run();
return;
} catch (DeadlockException e) {
retryCount++;
if (retryCount >= maxRetries) {
throw e;
}
// 等待随机时间后重试
try {
Thread.sleep(new Random().nextInt(100));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
}
七、实战案例:电商秒杀系统优化
7.1 业务场景分析
需求: 100万用户同时抢购1000件商品,要求系统稳定、数据一致。
7.2 架构设计
用户请求 → Nginx → 应用服务器集群 → Redis缓存 → MySQL数据库
7.3 关键优化点
7.3.1 库存预热
// 秒杀开始前,将库存加载到Redis
public void preloadStock(Long productId, Integer stock) {
String key = "seckill:stock:" + productId;
redisTemplate.opsForValue().set(key, stock, 30, TimeUnit.MINUTES);
// 使用Lua脚本保证原子性
String luaScript =
"local stock = redis.call('get', KEYS[1]) " +
"if tonumber(stock) > 0 then " +
" redis.call('decr', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key));
}
7.3.2 限流策略
// 使用令牌桶算法限流
@Component
public class RateLimiter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public boolean tryAcquire(String key, int permits, int windowSeconds) {
String luaScript =
"local current = redis.call('get', KEYS[1]) " +
"if not current then " +
" redis.call('setex', KEYS[1], ARGV[1], ARGV[2]) " +
" return 1 " +
"elseif tonumber(current) < tonumber(ARGV[2]) then " +
" redis.call('incr', KEYS[1]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
windowSeconds,
permits
);
return result != null && result == 1;
}
}
7.3.3 异步下单
// 使用消息队列异步处理订单
public void createOrderAsync(Long userId, Long productId) {
// 1. 校验库存(Redis)
String stockKey = "seckill:stock:" + productId;
Long stock = redisTemplate.opsForValue().decrement(stockKey);
if (stock == null || stock < 0) {
throw new RuntimeException("库存不足");
}
// 2. 发送消息到MQ
OrderMessage message = new OrderMessage();
message.setUserId(userId);
message.setProductId(productId);
message.setTimestamp(System.currentTimeMillis());
rabbitTemplate.convertAndSend("seckill.order", message);
// 3. 返回处理中状态
return "订单处理中,请稍后查看";
}
// 消费者处理消息
@RabbitListener(queues = "seckill.order")
public void processOrder(OrderMessage message) {
// 1. 检查是否已处理(幂等性)
String orderKey = "seckill:order:" + message.getUserId() + ":" + message.getProductId();
if (redisTemplate.hasKey(orderKey)) {
return;
}
// 2. 创建订单(事务)
transactionTemplate.execute(status -> {
// 检查数据库库存
Integer dbStock = orderMapper.getStock(message.getProductId());
if (dbStock <= 0) {
throw new RuntimeException("库存不足");
}
// 扣减数据库库存
orderMapper.decreaseStock(message.getProductId());
// 创建订单
Order order = new Order();
order.setUserId(message.getUserId());
order.setProductId(message.getProductId());
order.setStatus("PAID");
orderMapper.insert(order);
// 标记已处理
redisTemplate.opsForValue().set(orderKey, "1", 1, TimeUnit.HOURS);
return order;
});
}
7.3.4 数据库优化
-- 1. 创建秒杀专用表(避免与业务表混合)
CREATE TABLE seckill_orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_product (user_id, product_id),
KEY idx_product_id (product_id)
) ENGINE=InnoDB;
-- 2. 使用INSERT IGNORE避免重复
INSERT IGNORE INTO seckill_orders (user_id, product_id, amount)
VALUES (?, ?, ?);
-- 3. 批量插入优化
INSERT INTO seckill_orders (user_id, product_id, amount) VALUES
(1, 1001, 99.99),
(2, 1001, 99.99),
(3, 1001, 99.99)
ON DUPLICATE KEY UPDATE status = VALUES(status);
7.4 压力测试与调优
7.4.1 使用JMeter进行压力测试
<!-- JMeter测试计划配置 -->
<TestPlan>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="秒杀并发测试">
<stringProp name="ThreadGroup.num_threads">1000</stringProp>
<stringProp name="ThreadGroup.ramp_time">10</stringProp>
<stringProp name="ThreadGroup.duration">60</stringProp>
</ThreadGroup>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="秒杀请求">
<stringProp name="HTTPSampler.domain">seckill.example.com</stringProp>
<stringProp name="HTTPSampler.port">80</stringProp>
<stringProp name="HTTPSampler.path">/api/seckill</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<elementProp name="Arguments" elementType="Arguments">
<collectionProp name="Arguments.arguments">
<elementProp name="productId" elementType="HTTPArgument">
<boolProp name="HTTPArgument.always_encode">false</boolProp>
<stringProp name="Argument.value">1001</stringProp>
<stringProp name="Argument.metadata">=</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</HTTPSamplerProxy>
</TestPlan>
7.4.2 性能指标监控
# 监控MySQL性能指标
watch -n 1 "mysql -u root -p -e 'SHOW PROCESSLIST;' | wc -l"
# 监控连接数
mysqladmin -u root -p extended-status | grep Threads_connected
# 监控慢查询
tail -f /var/log/mysql/slow.log
八、总结与最佳实践
8.1 优化优先级
架构优化(最高优先级)
- 读写分离
- 缓存策略
- 分库分表
SQL优化
- 索引优化
- 查询语句优化
- 避免全表扫描
配置优化
- 缓冲池大小
- 连接池配置
- 日志文件大小
应用层优化
- 连接池调优
- 批量操作
- 异步处理
8.2 监控告警清单
| 监控项 | 告警阈值 | 处理措施 |
|---|---|---|
| 连接数使用率 | > 80% | 增加连接池大小或优化连接释放 |
| 慢查询数量 | > 10/分钟 | 分析并优化慢查询 |
| 主从延迟 | > 5秒 | 检查从库性能,优化复制配置 |
| 缓冲池命中率 | < 95% | 增加缓冲池大小或优化查询 |
| 死锁数量 | > 5/小时 | 分析死锁日志,优化事务逻辑 |
8.3 持续优化建议
- 定期审查慢查询日志
- 定期分析表结构和索引
- 定期进行压力测试
- 定期备份和恢复演练
- 持续监控和告警优化
九、常见问题与解决方案
Q1:如何处理数据库连接池耗尽问题?
解决方案:
- 增加连接池最大连接数
- 优化SQL,减少查询时间
- 引入连接池监控
- 使用连接池泄漏检测
- 考虑使用连接池代理(如ProxySQL)
Q2:如何避免缓存穿透?
解决方案:
- 使用布隆过滤器
- 缓存空值(设置较短过期时间)
- 接口层限流
- 对于不存在的数据,也缓存null值
Q3:如何优化大表的DDL操作?
解决方案:
- 使用pt-online-schema-change工具
- 在业务低峰期执行
- 分批处理
- 使用MySQL 8.0的Online DDL特性
Q4:如何监控MySQL的性能瓶颈?
解决方案:
- 使用Performance Schema
- 使用sys schema
- 使用慢查询日志
- 使用第三方监控工具(如Percona Monitoring and Management)
十、结语
MySQL在高并发场景下的性能优化是一个系统工程,需要从架构设计、配置调优、SQL优化、缓存策略、监控告警等多个维度综合考虑。没有一劳永逸的解决方案,需要根据业务特点和实际负载情况持续调整和优化。
通过本文介绍的策略和方法,您可以构建一个高性能、高可用的MySQL数据库系统,有效应对高并发挑战。记住,优化是一个持续的过程,需要不断监控、分析和改进。
最后建议:
- 建立完善的监控体系
- 制定详细的应急预案
- 定期进行性能测试
- 保持学习和更新知识
- 与团队分享优化经验
希望本文能为您的MySQL性能优化之旅提供有价值的参考和指导。
