引言:理解高并发场景下的数据库挑战
在现代互联网应用中,高并发场景已经成为常态。无论是电商秒杀、社交平台热点事件,还是金融交易系统,MySQL数据库都面临着每秒数千甚至数万请求的严峻考验。当并发请求超过数据库的处理能力时,系统响应时间急剧增加,甚至导致服务完全不可用,这就是我们常说的”系统崩溃”。
高并发对MySQL的冲击主要体现在三个方面:连接资源耗尽、CPU和内存过载、磁盘I/O瓶颈。想象一下,如果每个数据库连接都需要消耗内存和CPU资源,当10000个并发请求同时到达时,数据库服务器可能瞬间被压垮。更糟糕的是,不当的查询可能导致锁竞争,使得所有请求都在等待,形成雪崩效应。
本文将从架构设计、MySQL配置优化、SQL与索引优化、缓存策略、读写分离、数据库拆分等多个维度,系统性地讲解如何优化MySQL以应对海量请求。我们会结合具体的配置参数、代码示例和实际案例,帮助你构建一个高可用、高性能的数据库系统。
一、架构层面的优化策略
1.1 读写分离架构
读写分离是应对高并发最有效的架构手段之一。其核心思想是将读请求和写请求分离到不同的数据库实例,利用多个从库来分担读压力。
实现原理:
- 主库(Master):处理所有写操作(INSERT/UPDATE/DELETE)和部分读操作
- 从库(Slave):处理所有读操作(SELECT),通过主从复制同步数据
代码示例:使用ShardingSphere实现读写分离
// 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
配置要点:
- 主从复制延迟:需要监控复制延迟,避免读取到过期数据
- 主从数据一致性:对于强一致性要求的场景,可以强制读主库
- 负载均衡:合理选择轮询、随机或基于权重的负载均衡策略
1.2 数据库垂直拆分
当单表字段过多(如超过50个字段)或业务逻辑清晰时,可以考虑垂直拆分。将大表按业务模块拆分成多个小表,减少单行数据大小,提高查询效率。
案例:用户表拆分
-- 拆分前:用户表包含所有字段
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100),
email VARCHAR(100),
phone VARCHAR(20),
address TEXT,
avatar TEXT,
profile TEXT,
created_at TIMESTAMP,
updated_at TIMESTAMP,
-- ... 更多字段
INDEX idx_username (username)
);
-- 拆分后:基础信息表 + 详情表
CREATE TABLE user_base (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE,
password VARCHAR(100),
email VARCHAR(100),
phone VARCHAR(20),
created_at TIMESTAMP,
INDEX idx_username (username)
);
CREATE TABLE user_profile (
user_id BIGINT PRIMARY KEY,
address TEXT,
avatar TEXT,
profile TEXT,
updated_at TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user_base(id)
);
优势:
- 热点数据(如username/password)查询更快
- 非频繁访问的字段(如profile)不占用主表内存
- 更灵活的缓存策略
1.3 数据库水平拆分(分库分表)
当单表数据量超过1000万或单库连接数不足时,需要水平拆分。将数据按某种规则(如用户ID哈希)分布到多个物理表中。
分表策略示例:按用户ID取模分表
-- 分表规则:user_id % 4,共4张表
CREATE TABLE user_0 (
id BIGINT PRIMARY KEY,
user_id BIGINT,
username VARCHAR(50),
-- 其他字段
INDEX idx_user_id (user_id)
);
CREATE TABLE user_1 (
id BIGINT PRIMARY KEY,
user_id BIGINT,
username VARCHAR(50),
-- 其他字段
INDEX idx_user_id (user_id)
);
-- ... user_2, user_3
分表路由代码实现:
public class ShardingUtil {
private static final int TABLE_COUNT = 4;
// 根据user_id计算表名
public static String getTableName(Long userId) {
int index = (int) (userId % TABLE_COUNT);
return "user_" + index;
}
// 动态SQL示例(MyBatis)
// <select id="getUserById" resultType="User">
// SELECT * FROM ${tableName} WHERE user_id = #{userId}
// </select>
// 使用示例
public User getUserById(Long userId) {
String tableName = ShardingUtil.getTableName(userId);
Map<String, Object> params = new HashMap<>();
params.put("tableName", tableName);
params.put("userId", userId);
return userMapper.getUserById(params);
}
}
分库分表中间件:
- ShardingSphere:功能强大,支持多种分片算法
- MyCat:基于代理的分库分表方案
- Vitess:YouTube开源的分库分表方案,适合超大规模
二、MySQL配置参数优化
2.1 连接相关配置
高并发下,连接数是最容易成为瓶颈的资源。合理配置连接参数至关重要。
关键参数:
# /etc/my.cnf [mysqld] 部分
# 最大连接数,默认151,高并发建议500-2000
max_connections = 1000
# 每个连接的最大请求数,防止连接泄漏
max_user_connections = 800
# 连接超时时间(秒)
connect_timeout = 10
# 等待超时时间(秒),空闲连接自动关闭
wait_timeout = 600
# 交互式超时时间(秒)
interactive_timeout = 600
# back_log队列长度,允许暂时挂起的连接数
back_log = 500
参数详解:
- max_connections:这个值不是越大越好。每个连接至少消耗1MB内存,1000个连接就是1GB。同时,操作系统对进程能打开的文件数有限制(ulimit -n),需要同步调整。
- wait_timeout:设置过长会导致大量空闲连接占用资源,过短会导致频繁重连。600秒(10分钟)是较合理的值。
监控连接数:
-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 查看最大连接数
SHOW VARIABLES LIKE 'max_connections';
-- 查看连接数使用率
SELECT
VARIABLE_VALUE as max_conn,
(SELECT COUNT(*) FROM information_schema.PROCESSLIST) as current_conn,
(SELECT COUNT(*) FROM information_schema.PROCESSLIST) / VARIABLE_VALUE * 100 as usage_rate
FROM information_schema.GLOBAL_VARIABLES
WHERE VARIABLE_NAME = 'max_connections';
2.2 InnoDB缓冲池配置
InnoDB缓冲池(Buffer Pool)是MySQL性能的核心,它缓存数据和索引在内存中,减少磁盘I/O。
关键配置:
# 缓冲池大小,通常设置为物理内存的50%-70%
innodb_buffer_pool_size = 8G
# 缓冲池实例数,多实例减少竞争
innodb_buffer_pool_instances = 8
# 缓冲池刷新策略
innodb_buffer_pool_flush_at_neighbors = 1
innodb_buffer_pool_dump_at_shutdown = 1
innodb_buffer_pool_load_at_startup = 1
# 页大小
innodb_page_size = 16K
配置建议:
- 8GB内存服务器:
innodb_buffer_pool_size = 4G - 32GB内存服务器:
innodb_buffer_pool_size = 24G - innodb_buffer_pool_instances:当缓冲池大于1GB时,建议设置为8或16,避免大内存下的锁竞争
监控缓冲池命中率:
-- 缓冲池命中率应 > 99%
SELECT
(1 - (SUM(VARIABLE_VALUE) / @@innodb_buffer_pool_size)) * 100 AS hit_rate
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME IN ('Innodb_buffer_pool_reads', 'Innodb_buffer_pool_read_requests');
-- 查看缓冲池使用情况
SHOW ENGINE INNODB STATUS\G
-- 查看BUFFER POOL AND MEMORY部分
2.3 日志文件配置
日志文件影响写入性能和数据安全性。
关键配置:
# 事务日志文件大小
innodb_log_file_size = 2G
# 日志文件数量
innodb_log_files_in_group = 3
# 日志缓冲区大小
innodb_log_buffer_size = 64M
# 刷新日志的策略
innodb_flush_log_at_trx_commit = 1 # 1=每次提交刷盘(最安全),2=每秒刷盘(高性能)
innodb_flush_method = O_DIRECT # 绕过操作系统缓存,直接写入磁盘
配置说明:
- innodb_log_file_size:总日志大小约为1-2GB,太大恢复时间长,太小导致频繁checkpoint
- innodb_flush_log_at_trx_commit:
- 1:每次事务提交都写入磁盘,ACID完全保证,性能最低
- 2:每秒写入磁盘,可能丢失1秒数据,性能最高
- 0:写入操作系统缓存,不保证刷盘,风险最大
2.4 其他重要参数
# 并发线程数
innodb_thread_concurrency = 0 # 0表示不限制,让InnoDB自动管理
# 读入缓冲区
read_buffer_size = 2M
# 排序缓冲区
sort_buffer_size = 4M
# 临时表内存大小
tmp_table_size = 64M
max_heap_table_size = 64M
# 查询缓存(MySQL 8.0已移除)
# query_cache_type = 0
# query_cache_size = 0
# 慢查询日志
slow_query_log = 1
long_query_time = 1
log_queries_not_using_indexes = 1
三、SQL与索引优化
3.1 索引设计原则
索引是SQL性能优化的重中之重。好的索引能让查询速度提升几个数量级。
索引设计黄金法则:
- 最左前缀原则:复合索引必须从最左列开始匹配
- 选择性高:区分度大的列适合建索引(如user_id, email)
- 覆盖索引:查询列全部在索引中,避免回表
- 避免冗余索引:定期检查并删除无用索引
案例:用户表索引优化
-- 原始表结构
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(100),
status TINYINT,
created_at TIMESTAMP,
age INT,
city VARCHAR(50)
);
-- 优化前:全表扫描
SELECT * FROM user WHERE email = 'user@example.com';
-- 优化后:添加索引
ALTER TABLE user ADD INDEX idx_email (email);
-- 复合索引示例:查询活跃用户
SELECT id, username FROM user
WHERE status = 1 AND created_at > '2024-01-01';
-- 创建复合索引(注意顺序)
ALTER TABLE user ADD INDEX idx_status_created (status, created_at);
-- 覆盖索引示例:避免回表
SELECT id, username, email FROM user
WHERE username LIKE 'john%';
-- 创建覆盖索引
ALTER TABLE user ADD INDEX idx_username_cover (username, id, email);
索引使用分析:
-- 使用EXPLAIN分析执行计划
EXPLAIN SELECT * FROM user WHERE email = 'user@example.com';
-- 关注type: ref(索引查找) vs ALL(全表扫描)
-- 关注rows: 扫描行数越少越好
-- 关注Extra: Using index(覆盖索引)vs Using where(回表)
-- 查看索引使用情况
SELECT * FROM sys.schema_unused_indexes; -- 未使用索引
SELECT * FROM sys.schema_redundant_indexes; -- 冗余索引
3.2 避免索引失效的常见场景
反面案例:
-- 1. 索引列参与计算
SELECT * FROM user WHERE YEAR(created_at) = 2024; -- 失效
-- 优化:SELECT * FROM user WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
-- 2. 隐式类型转换
SELECT * FROM user WHERE phone = 12345678901; -- phone是VARCHAR,会失效
-- 优化:SELECT * FROM user WHERE phone = '12345678901';
-- 3. 前缀模糊查询
SELECT * FROM user WHERE username LIKE '%john'; -- 失效
-- 优化:SELECT * FROM user WHERE username LIKE 'john%'; -- 前缀匹配有效
-- 4. OR条件(部分列无索引)
SELECT * FROM user WHERE email = 'a@b.com' OR age = 25; -- age无索引导致全表扫描
-- 优化:UNION ALL 或分别查询
-- 5. 范围查询后无法使用后续索引
SELECT * FROM user WHERE status > 0 AND created_at = '2024-01-01';
-- 复合索引(status, created_at)中,created_at无法使用
-- 优化:调整索引顺序或拆分查询
3.3 SQL语句优化技巧
*1. 避免SELECT **:
-- 反例:查询所有列,可能无法使用覆盖索引
SELECT * FROM user WHERE id = 1;
-- 正例:只查询需要的列
SELECT id, username, email FROM user WHERE id = 1;
2. 分页优化:
-- 反例:深度分页性能差
SELECT * FROM user ORDER BY id LIMIT 1000000, 20;
-- 正例1:延迟关联
SELECT u.* FROM user u
INNER JOIN (SELECT id FROM user ORDER BY id LIMIT 1000000, 20) tmp
ON u.id = tmp.id;
-- 正例2:记录上次id
SELECT * FROM user WHERE id > 1000000 ORDER BY id LIMIT 20;
3. 批量操作:
-- 反例:逐条插入
INSERT INTO user (username, email) VALUES ('user1', 'a@b.com');
INSERT INTO user (username, email) VALUES ('user2', 'c@d.com');
-- ... 1000次
-- 正例:批量插入
INSERT INTO user (username, email) VALUES
('user1', 'a@b.com'),
('user2', 'c@d.com'),
-- ... 1000条
('user1000', 'z@y.com');
-- 批量更新
UPDATE user SET status = 1 WHERE id IN (1,2,3,...,1000);
4. 使用UNION ALL替代UNION:
-- 如果不需要去重,UNION ALL性能更好
SELECT id FROM user WHERE status = 1
UNION ALL
SELECT id FROM user WHERE status = 2;
5. 小表驱动大表:
-- 反例:大表驱动小表
SELECT * FROM big_table b
JOIN small_table s ON b.id = s.big_id
WHERE s.type = 'A';
-- 正例:小表驱动大表(MySQL优化器会自动处理,但显式控制更好)
SELECT * FROM small_table s
JOIN big_table b ON s.big_id = b.id
WHERE s.type = 'A';
3.4 慢查询分析与优化
开启慢查询日志:
-- 临时开启
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 1;
-- 永久配置(my.cnf)
slow_query_log = 1
long_query_time = 1
slow_query_log_file = /var/log/mysql/slow.log
log_queries_not_using_indexes = 1
使用pt-query-digest分析:
# 安装Percona Toolkit
sudo apt-get install percona-toolkit
# 分析慢查询日志
pt-query-digest /var/log/mysql/slow.log > slow_report.txt
# 输出示例:
# Query 1: 12.5 QPS, 95% latency 250ms
# SELECT * FROM user WHERE email = ?
# Time range: 2024-01-01 00:00:00 to 01:01:00
# Attribute pct total min max avg 95% stddev median
# ============ === ======= ======= ======= ======= ======= ======= =======
# Count 45 4500
# Exec time 67 1125s 200ms 2s 250ms 450ms 180ms 220ms
# Rows sent 3 9000 2 2 2 2 0 2
# Rows examine 89 45.0M 10.0K 10.0K 10.0K 10.0K 0 10.0K
# Bytes sent 100 1.2M 272 272 272 272 0 272
# ============ === ======= ======= ======= ======= ======= ======= =======
# Profile
# Rank Query ID Response time Calls R/Call V/M Item
# ==== ================== ================ ===== ====== ===== ===========
# 1 0xF9A57DD5A4C... 1125.0452 67.0% 4500 0.2500 0.02 SELECT user
四、缓存策略优化
4.1 多级缓存架构
高并发下,纯数据库查询无法支撑,必须引入缓存。
典型架构:
客户端 → CDN → Nginx → 应用缓存 → Redis → MySQL
缓存策略:
- 本地缓存:Caffeine/Guava Cache,响应快但容量有限
- 分布式缓存:Redis/Memcached,容量大但网络开销
- 多级缓存:本地缓存 + 分布式缓存组合
4.2 Redis缓存实战
缓存读写模式:
// 1. Cache Aside模式(最常用)
public User getUserById(Long id) {
// 1. 先读缓存
String key = "user:" + id;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 2. 缓存未命中,读数据库
User user = userMapper.selectById(id);
if (user != null) {
// 3. 写入缓存,设置过期时间
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
}
return user;
}
// 4. 更新缓存(更新数据库后删除缓存)
public void updateUser(User user) {
userMapper.updateById(user);
// 删除缓存,让下次查询重新加载
String key = "user:" + user.getId();
redisTemplate.delete(key);
}
缓存穿透防护:
// 布隆过滤器防止缓存穿透
public User getUserWithBloom(Long id) {
String key = "user:" + id;
// 1. 布隆过滤器快速判断key是否存在
if (!bloomFilter.mightContain(key)) {
// 不存在,直接返回null,不查数据库
return null;
}
// 2. 正常缓存逻辑
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
// 3. 查询数据库
User user = userMapper.selectById(id);
// 4. 如果数据库也不存在,缓存空值(防止同一key反复查询)
if (user == null) {
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES); // 缓存空值5分钟
return null;
}
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
return user;
}
缓存雪崩防护:
// 设置随机过期时间,避免同时失效
public void setWithRandomExpire(String key, Object value) {
int expire = 1800 + new Random().nextInt(600); // 30-40分钟随机
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
// 热点数据永不过期 + 后台异步更新
public void setHotData(String key, Object value) {
// 设置过期时间很长(如7天)
redisTemplate.opsForValue().set(key, value, 7, TimeUnit.DAYS);
// 启动定时任务异步更新
scheduleHotDataRefresh(key);
}
4.3 缓存与数据库一致性
最终一致性方案:
// 使用Canal监听MySQL binlog,同步更新缓存
// Canal Server配置
canal.instance.filter.regex=db\\.user
canal.instance.filter.black.field=user:status
// 消费binlog示例
@Component
public class CanalClient {
@Autowired
private RedisTemplate redisTemplate;
public void handleUpdate(String table, String data) {
if ("user".equals(table)) {
// 解析binlog数据
User user = parseUser(data);
// 删除或更新缓存
String key = "user:" + user.getId();
redisTemplate.delete(key);
}
}
}
五、数据库连接池优化
5.1 HikariCP配置
HikariCP是目前性能最好的Java连接池。
配置示例:
spring:
datasource:
hikari:
# 连接池名称
pool-name: MasterDBPool
# 最大连接数
maximum-pool-size: 50
# 最小空闲连接
minimum-idle: 10
# 连接超时时间(毫秒)
connection-timeout: 30000
# 空闲连接存活时间(毫秒)
idle-timeout: 600000
# 连接最大生命周期(毫秒)
max-lifetime: 1800000
# 连接测试查询
connection-test-query: SELECT 1
# 是否自动提交
auto-commit: true
# 连接预热
initialization-fail-timeout: 1
# leakDetectionThreshold: 60000 # 开发环境开启泄漏检测
参数调优建议:
- maximum-pool-size:根据CPU核心数和业务类型计算
- CPU密集型:CPU核心数 + 1
- IO密集型:CPU核心数 * 2
- 混合型:CPU核心数 * 2 ~ CPU核心数 * 4
- minimum-idle:通常设置为maximum-pool-size的1/3到1/2
- max-lifetime:建议小于MySQL的wait_timeout,避免被服务端关闭
5.2 连接池监控
// HikariCP监控指标
@Component
public class DataSourceMonitor {
@Autowired
private HikariDataSource dataSource;
@Scheduled(fixedRate = 60000)
public void monitor() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
System.out.println("当前活跃连接: " + poolMXBean.getActiveConnections());
System.out.println("空闲连接: " + poolMXBean.getIdleConnections());
System.out.println("总连接: " + poolMXBean.getTotalConnections());
System.out.println("等待连接的线程数: " + poolMXBean.getThreadsAwaitingConnection());
// 如果等待线程数持续>0,说明连接池不足,需要扩容
if (poolMXBean.getThreadsAwaitingConnection() > 10) {
// 告警或扩容
alert("连接池不足,需要扩容");
}
}
}
六、高级优化策略
6.1 分区表优化
当单表数据量巨大但访问有明显时间特征时,使用分区表。
-- 按时间分区(RANGE分区)
CREATE TABLE log (
id BIGINT,
log_time DATETIME,
content TEXT,
INDEX idx_time (log_time)
) PARTITION BY RANGE (YEAR(log_time) * 100 + MONTH(log_time)) (
PARTITION p202401 VALUES LESS THAN (202402),
PARTITION p202402 VALUES LESS THAN (202403),
PARTITION p202403 VALUES LESS THAN (202404),
PARTITION p_max VALUES LESS THAN MAXVALUE
);
-- 查询时自动分区裁剪
SELECT * FROM log WHERE log_time BETWEEN '2024-02-01' AND '2024-02-28';
-- 只扫描p202402分区
6.2 并发控制
应用层限流:
// 使用RateLimiter限制数据库写入频率
RateLimiter writeLimiter = RateLimiter.create(1000.0); // 每秒1000次
public void writeData(Data data) {
writeLimiter.acquire(); // 获取许可,如果没有则阻塞
// 执行数据库写入
dataMapper.insert(data);
}
数据库层锁优化:
-- 使用乐观锁(适合读多写少)
UPDATE user SET
balance = balance - 100,
version = version + 1
WHERE id = 1 AND version = 5; -- 版本号校验
-- 使用悲观锁(适合写多读少)
SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 加排他锁
6.3 监控与告警
Prometheus + Grafana监控MySQL:
# mysqld_exporter配置
# /etc/default/mysqld_exporter
DATA_SOURCE_NAME="user:password@(localhost:3306)/"
# Prometheus配置
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['localhost:9104']
关键监控指标:
- QPS:Queries per second
- TPS:Transactions per second
- 连接数使用率:Threads_connected / max_connections
- 慢查询数:Slow_queries
- InnoDB缓冲池命中率:(1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100
- 复制延迟:Seconds_Behind_Master
告警规则示例:
# 连接数超过80%告警
- alert: MySQLHighConnections
expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections * 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL连接数过高"
description: "当前连接数 {{ $value }}%,超过80%阈值"
# 慢查询告警
- alert: MySQLSlowQueries
expr: increase(mysql_global_status_slow_queries[5m]) > 10
for: 5m
labels:
severity: critical
annotations:
summary: "MySQL慢查询激增"
description: "5分钟内新增 {{ $value }} 个慢查询"
七、实战案例:秒杀系统优化
7.1 问题场景
原始方案:
-- 秒杀扣减库存
UPDATE item SET stock = stock - 1 WHERE id = 1 AND stock > 0;
问题:
- 高并发下大量请求阻塞在UPDATE语句
- 数据库连接池耗尽
- 系统响应时间从10ms飙升到5s+
7.2 优化方案
1. 预减库存(Redis):
public boolean seckill(Long itemId) {
String key = "item:stock:" + itemId;
// 1. Redis预减库存
Long stock = redisTemplate.opsForValue().decrement(key);
if (stock < 0) {
// 库存不足,回滚
redisTemplate.opsForValue().increment(key);
return false;
}
// 2. 发送消息到队列
rabbitTemplate.convertAndSend("seckill.order", itemId);
return true;
}
// 异步消费队列,最终扣减数据库
@RabbitListener(queues = "seckill.order")
public void createOrder(Long itemId) {
// 3. 数据库最终扣减(单线程,无竞争)
itemMapper.reduceStock(itemId);
// 4. 创建订单
orderMapper.createOrder(itemId);
}
2. 数据库优化:
-- 1. stock字段加唯一索引,防止超卖
ALTER TABLE item ADD UNIQUE INDEX idx_stock (stock);
-- 2. 使用乐观锁
UPDATE item SET stock = stock - 1, version = version + 1
WHERE id = 1 AND stock > 0 AND version = #{version};
-- 3. 分库分表(item表按id取模分4张表)
3. 限流熔断:
// 令牌桶限流
RateLimiter limiter = RateLimiter.create(5000.0); // 每秒5000个请求
public boolean seckill(Long itemId) {
if (!limiter.tryAcquire()) {
throw new RateLimitException("请求过于频繁,请稍后重试");
}
// ... 业务逻辑
}
// 熔断器(Hystrix)
@HystrixCommand(
fallbackMethod = "seckillFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
}
)
public boolean seckill(Long itemId) {
// ... 业务逻辑
}
public boolean seckillFallback(Long itemId) {
return false; // 降级返回
}
7.3 效果对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| QPS | 500 | 15000 |
| 平均响应时间 | 2000ms | 50ms |
| 99%响应时间 | 5000ms | 200ms |
| 数据库连接数 | 100+ | <20 |
| 是否超卖 | 是 | 否 |
八、总结与最佳实践
8.1 优化优先级
- SQL与索引优化(成本最低,效果最明显)
- 缓存策略(大幅提升QPS)
- 读写分离(解决读压力)
- 连接池调优(避免资源耗尽)
- 分库分表(终极方案,成本高)
8.2 日常维护 checklist
- [ ] 每天检查慢查询日志
- [ ] 监控连接数使用率
- [ ] 检查主从复制延迟
- [ ] 定期分析索引使用情况
- [ ] 监控缓冲池命中率(>99%)
- [ ] 检查磁盘空间和I/O
- [ ] 定期备份并测试恢复
8.3 避免的坑
- 不要盲目增加max_connections:可能导致OOM
- 不要在数据库做复杂计算:应用层处理
- 不要忽略主从延迟:可能导致数据不一致
- 不要滥用索引:每个索引都有维护成本
- 不要忽略监控:优化是持续的过程
8.4 性能优化哲学
“先测量,再优化”:使用性能分析工具(Percona Toolkit, pt-query-digest)找到真正的瓶颈,而不是凭感觉猜测。
“二八法则”:80%的性能问题来自20%的慢查询,集中精力优化这些查询。
“架构演进”:不要一开始就设计复杂的架构,根据业务增长逐步演进。
通过以上策略的综合运用,MySQL完全可以支撑每秒数万甚至数十万的并发请求。记住,优化是一个持续的过程,需要根据业务发展和监控数据不断调整策略。
