电商大促秒杀卡顿崩溃数据库扛不住 5个MySQL高并发实战优化方案从连接池调优到读写分离彻底解决系统雪崩问题
你有没有经历过那种”黑色星期五”的噩梦?凌晨三点监控报警炸裂,订单系统卡死,用户疯狂点击”下单”按钮,结果页面一直转圈,最后一声”提交失败”。后台日志里,MySQL的连接数蹭蹭往上涨,CPU飙到100%,然后——崩了。
这种场景,每个做电商的工程师都心里有数。今天咱们就来聊聊,怎么把MySQL从”高并发杀手”变成”高并发推手”。
一、连接池调优:别让数据库被连接数淹死
1.1 连接数爆炸的真实代价
想象一下,秒杀活动开始的那一刻,10万用户同时涌入,每个用户都要建立数据库连接。如果每次请求都新建连接,数据库的CPU全用在”握手-认证-断连”上了,根本没空处理业务。
-- 查看当前连接状态
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_created';
-- 查看连接配置
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
1.2 连接池的核心参数调优
HikariCP(Java项目首选)配置示例:
# application.yml - 高并发场景下的连接池配置
spring:
datasource:
hikari:
# 最大连接数(关键!不是越大越好)
maximum-pool-size: 50
# 最小空闲连接数
minimum-idle: 10
# 连接超时时间(毫秒)- 别让请求无限等待
connection-timeout: 30000
# 空闲连接最大存活时间
idle-timeout: 600000
# 连接最大存活时间(防止连接泄漏)
max-lifetime: 1800000
# 连接测试查询
connection-test-query: SELECT 1
# 启用泄漏检测(开发环境)
leak-detection-threshold: 60000
连接数估算公式:
合理连接数 = (CPU核心数 × 2) + 磁盘数
例如:8核CPU + 2块磁盘 = 18个连接
💡 实战经验:连接池不是越大越好。超过合理值后,数据库切换上下文的开销会超过连接复用的收益。我曾经见过一个项目把连接池开到200,结果TPS反而下降了40%。
1.3 秒杀场景的特殊处理
// 秒杀专用数据源配置 - 隔离业务
@Configuration
public class SecKillDataSourceConfig {
@Bean("secKillDataSource")
@Primary
public DataSource secKillDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://10.0.0.10:3306/sec_kill_db?useSSL=false&serverTimezone=Asia/Shanghai");
dataSource.setUsername("sec_kill_user");
dataSource.setPassword("s3c_kill!2024");
// 秒杀专用连接池 - 独立配置
dataSource.setMaximumPoolSize(30); // 独立于主库
dataSource.setMinimumIdle(5);
dataSource.setConnectionTimeout(5000); // 更短的超时
return dataSource;
}
// 主库数据源
@Bean("mainDataSource")
public DataSource mainDataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://10.0.0.11:3306/main_db?useSSL=false&serverTimezone=Asia/Shanghai");
// ... 主库配置
return dataSource;
}
}
关键原则:秒杀业务必须隔离! 不能让秒杀请求拖垮主库,影响正常下单和查询。
二、SQL优化:每一条慢查询都是定时炸弹
2.1 索引设计是王道
-- ❌ 错误示范:没有索引的全表扫描
SELECT * FROM orders WHERE user_id = 12345 AND status = 0;
-- ✅ 正确做法:联合索引覆盖查询
CREATE INDEX idx_user_status_created ON orders(user_id, status, created_at);
-- 验证索引使用
EXPLAIN SELECT * FROM orders WHERE user_id = 12345 AND status = 0 ORDER BY created_at DESC LIMIT 10;
2.2 避免大事务和长连接
// ❌ 错误:长事务占用连接和锁
@Transactional
public void seckillProcess(Long userId, Long itemId) {
// 1. 查询库存
Order order = orderMapper.selectById(itemId);
// 2. 业务逻辑处理(可能很慢)
processUserData(userId);
// 3. 更新库存
orderMapper.updateStock(itemId, -1);
// 4. 创建订单
orderMapper.insert(buildOrder(userId, itemId));
}
// ✅ 正确:拆分事务,快速提交
public void seckillProcess(Long userId, Long itemId) {
// 1. 前置校验(非事务)
validateUser(userId);
validateItem(itemId);
// 2. 短事务:只做核心操作
try {
transactionTemplate.execute(status -> {
int stock = stockMapper.decrementStock(itemId);
if (stock < 0) {
status.setRollbackOnly();
throw new RuntimeException("库存不足");
}
orderMapper.insert(buildOrder(userId, itemId));
return true;
});
} catch (Exception e) {
// 处理异常
}
// 3. 后置处理(非事务)
sendNotification(userId, itemId);
}
2.3 批量操作代替循环单条
-- ❌ 错误:循环单条插入(N+1问题)
FOR i IN 1..1000 LOOP
INSERT INTO order_detail (order_id, item_id, price)
VALUES (order_id, item_id, price);
END LOOP;
-- ✅ 正确:批量插入
INSERT INTO order_detail (order_id, item_id, price)
VALUES
(10001, 201, 99.00),
(10001, 202, 199.00),
(10001, 203, 59.00),
-- ... 建议每批100-500条
(10001, 300, 299.00);
// Java批量插入示例
public void batchInsertOrders(List<Order> orders) {
String sql = "INSERT INTO orders (user_id, item_id, price, status, created_at) " +
"VALUES (?, ?, ?, ?, NOW())";
// 使用JDBC批量执行
jdbcTemplate.batchUpdate(sql,
orders,
orders.size(),
(ps, order) -> {
ps.setLong(1, order.getUserId());
ps.setLong(2, order.getItemId());
ps.setBigDecimal(3, order.getPrice());
ps.setInt(4, order.getStatus());
}
);
}
三、读写分离:让从库分担压力
3.1 基础架构设计
┌─────────────┐
│ 网关层 │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌───▼────┐ ┌────▼─────┐
│ 主库Master │ │缓存层 │ │ 从库Slave1 │
│ (写操作) │ │Redis │ │ (读操作) │
└─────┬─────┘ └────────┘ └─────┬──────┘
│ │
┌─────▼─────┐ ┌──────▼──────┐
│ 从库Slave2 │ │ 从库Slave3 │
│ (读操作) │ │ (读操作) │
└───────────┘ └─────────────┘
3.2 Spring动态数据源实现
@Configuration
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource master,
@Qualifier("slave1DataSource") DataSource slave1,
@Qualifier("slave2DataSource") DataSource slave2) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DbTypeEnum.MASTER, master);
targetDataSources.put(DbTypeEnum.SLAVE1, slave1);
targetDataSources.put(DbTypeEnum.SLAVE2, slave2);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(master);
return dataSource;
}
}
// 动态数据源切换
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
// 数据源上下文
public class DbContextHolder {
private static final ThreadLocal<String> context = new ThreadLocal<>();
public static void setDbType(DbTypeEnum dbType) {
context.set(dbType.name());
}
public static String getDbType() {
return context.get();
}
public static void clearDbType() {
context.remove();
}
}
// AOP切面自动切换
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.example.annotation.ReadOnly)")
public void readOnlyPointcut() {}
@Before("readOnlyPointcut()")
public void beforeReadOnly(JoinPoint point) {
DbContextHolder.setDbType(DbTypeEnum.SLAVE1); // 轮询从库
}
@After("readOnlyPointcut()")
public void afterReadOnly() {
DbContextHolder.clearDbType();
}
}
3.3 读写分离的注意事项
-- 主从延迟测试
SHOW SLAVE STATUS\G
-- 关键字段:
-- Seconds_Behind_Master: 延迟秒数
-- Slave_IO_Running: 必须为 Yes
-- Slave_SQL_Running: 必须为 Yes
⚠️ 严重提醒:读写分离最大的坑是主从延迟。秒杀场景下,用户下单后立即可查,如果查询走从库可能查不到最新数据。
解决方案:敏感查询强制走主库
// 注解方式标记敏感查询
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterOnly {
}
// 使用方式
@MasterOnly
public Order queryOrderAfterCreate(Long orderId) {
// 强制走主库,确保查到最新数据
return orderMapper.selectById(orderId);
}
四、缓存层设计:Redis是秒杀的第一道防线
4.1 缓存架构分层
请求流程:
用户请求 → Redis缓存 → MySQL数据库
↓
缓存穿透/击穿/雪崩处理
4.2 库存扣减的Redis方案
@Service
public class SeckillStockService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String STOCK_KEY_PREFIX = "seckill:stock:";
private static final String ORDER_KEY_PREFIX = "seckill:order:";
/**
* 秒杀扣减库存 - Lua脚本保证原子性
*/
public boolean deductStock(Long itemId, Long userId) {
String stockKey = STOCK_KEY_PREFIX + itemId;
String orderKey = ORDER_KEY_PREFIX + userId + ":" + itemId;
// Lua脚本:先检查用户是否已购买,再扣减库存
String script =
"local stock = redis.call('get', KEYS[1]) " +
"if stock == false then return -1 end " +
"if tonumber(stock) <= 0 then return 0 end " +
"local hasBought = redis.call('sismember', KEYS[2], ARGV[1]) " +
"if hasBought == 1 then return 2 end " +
"redis.call('decrby', KEYS[1], 1) " +
"redis.call('sadd', KEYS[2], ARGV[1]) " +
"return 1";
Long result = (Long) redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Arrays.asList(stockKey, orderKey),
String.valueOf(userId)
);
// 返回结果:-1库存不存在,0库存不足,1扣减成功,2已购买
if (result == null || result == 0 || result == 2) {
return false;
}
return true;
}
/**
* 预热库存到Redis
*/
public void preloadStock(Long itemId, int stock) {
String stockKey = STOCK_KEY_PREFIX + itemId;
redisTemplate.opsForValue().set(stockKey, String.valueOf(stock),
Duration.ofHours(2)); // 活动后2小时过期
}
}
4.3 缓存穿透/击穿/雪崩防护
@Configuration
public class RedisConfig {
/**
* 缓存空对象防穿透
*/
@Bean
public Cache<String, Object> seckillCache(RedisTemplate<String, Object> redisTemplate) {
CacheBuilder<String, Object> builder = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES);
return Caffeine.from(builder)
.removalListener((RemovalNotification<String, Object> notification) -> {
// 监听缓存淘汰
})
.build();
}
/**
* 布隆过滤器防穿透
*/
@Bean
public BloomFilter<String> itemBloomFilter() {
// 预估容量100万,误判率0.01%
return BloomFilter.create(
Funnels.stringFunnel(Charsets.UTF_8),
1000000,
0.001
);
}
}
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private BloomFilter<String> itemBloomFilter;
/**
* 查询秒杀商品详情
*/
public SeckillItem querySeckillItem(Long itemId) {
String cacheKey = "seckill:item:" + itemId;
// 1. 先查缓存
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return JSON.parseObject(cached, SeckillItem.class);
}
// 2. 布隆过滤器判断是否存在
if (!itemBloomFilter.mightContain(String.valueOf(itemId))) {
return null; // 肯定不存在,直接返回
}
// 3. 查数据库
SeckillItem item = itemMapper.selectById(itemId);
if (item == null) {
// 4. 缓存空对象,防止穿透
redisTemplate.opsForValue().set(cacheKey, "", 30, TimeUnit.SECONDS);
return null;
}
// 5. 缓存结果
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(item),
30, TimeUnit.MINUTES);
itemBloomFilter.put(String.valueOf(itemId));
return item;
}
}
五、数据库内核优化:MySQL配置调优
5.1 关键参数调整
# my.cnf - 高并发场景优化配置
[mysqld]
# ==================== 连接与线程 ====================
max_connections = 2000 # 最大连接数
max_user_connections = 1500 # 单用户最大连接
thread_cache_size = 100 # 线程缓存
# ==================== 内存配置 ====================
innodb_buffer_pool_size = 8G # 缓冲池(物理内存的60-70%)
innodb_buffer_pool_instances = 8 # 实例数(与buffer_pool_size配合)
# ==================== 日志配置 ====================
innodb_log_file_size = 512M # 日志文件大小
innodb_log_buffer_size = 32M # 日志缓冲区
flush_log_at_trx_commit = 2 # 每秒刷盘(性能优先)
sync_binlog = 0 # 不强制刷盘(可接受少量数据丢失)
# ==================== 查询优化 ====================
tmp_table_size = 64M # 临时表大小
max_heap_table_size = 64M # 内存表最大大小
query_cache_type = 0 # MySQL 8.0已移除查询缓存
query_cache_size = 0
# ==================== 锁优化 ====================
innodb_lock_wait_timeout = 5 # 锁等待超时
innodb_deadlock_detect = on # 死锁检测
# ==================== 复制优化(主从)====================
slave_parallel_type = LOGICAL_CLOCK
slave_parallel_workers = 4
master_info_repository = TABLE
relay_log_info_repository = TABLE
5.2 监控关键指标
-- 实时监控脚本
SELECT
VARIABLE_NAME,
VARIABLE_VALUE
FROM information_schema.GLOBAL_STATUS
WHERE VARIABLE_NAME IN (
'Threads_connected', -- 当前连接数
'Threads_running', -- 活跃线程数
'Questions', -- 总查询数
'Slow_queries', -- 慢查询数
'Innodb_buffer_pool_pages_dirty', -- 脏页数量
'Innodb_data_writes', -- InnoDB写入次数
'Handler_read_next', -- 全表扫描
'Connections' -- 总连接数
);
-- 查看锁等待情况
SELECT
r.THREAD_ID as '等待线程',
r.PROCESSLIST_ID as '等待ID',
w.THREAD_ID as '持有线程',
w.PROCESSLIST_ID as '持有ID',
TIMESTAMPDIFF(SECOND, r.LOCK_TIME, NOW()) as '等待秒数',
r.REQUESTING_ENGINE
FROM performance_schema.data_locks w
JOIN performance_schema.data_lock_waits r ON w.ENGINE = r.ENGINE
WHERE r.REQUESTING_ENGINE = w.ENGINE;
5.3 慢查询分析与优化
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过1秒的记录
-- 查看慢查询
SHOW VARIABLES LIKE 'slow_query_log%';
-- 使用pt-query-digest分析
pt-query-digest /var/log/mysql/slow.log
六、秒杀架构完整方案
6.1 整体架构图
┌─────────────────────────────────────────────────────────────┐
│ 用户端 │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ CDN + 静态资源 │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ API网关(限流) │
│ - 令牌桶限流:1000 QPS/用户 │
│ - IP黑名单 │
│ - 请求签名验证 │
└───────────────────────────┬─────────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────────┐
│ 业务服务层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 秒杀服务 │ │ 订单服务 │ │ 库存服务 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼───────────────┘
│ │ │
┌─────────▼────────────────▼────────────────▼───────────────┐
│ 缓存层(Redis集群) │
│ - 库存预扣减(Lua脚本) │
│ - 用户秒杀资格校验 │
│ - 热点数据缓存 │
└─────────┬────────────────┬────────────────┬───────────────┘
│ │ │
┌─────────▼────────────────▼────────────────▼───────────────┐
│ 消息队列(RocketMQ) │
│ - 削峰填谷 │
│ - 异步下单 │
│ - 库存最终扣减 │
└─────────┬────────────────┬────────────────┬───────────────┘
│ │ │
┌─────────▼────────────────▼────────────────▼───────────────┐
│ 数据库层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 主库 │ │ 从库1 │ │ 从库2 │ │
│ │ (写操作) │ │ (读操作) │ │ (读操作) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└──────────────────────────────────────────────────────────┘
6.2 秒杀流程时序图
用户 → 网关 → 秒杀服务 → Redis → 消息队列 → MySQL
详细流程:
1. 用户请求到达网关,限流过滤
2. 校验用户资格(Redis:是否已秒杀、是否黑名单)
3. Redis预扣减库存(Lua原子操作)
4. 扣减成功,发送异步消息到MQ
5. 立即返回"排队中"
6. MQ消费者异步创建订单,写入MySQL
7. 轮询查询订单状态
6.3 防超卖核心代码
@Service
public class SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 秒杀入口
*/
public SeckillResult seckill(Long itemId, Long userId) {
// 1. 基础校验
validateSeckill(itemId, userId);
// 2. Redis预扣减库存
boolean success = redisTemplate.execute(
(RedisCallback<Boolean>) connection -> {
String stockKey = "seckill:stock:" + itemId;
String orderKey = "seckill:order:" + userId + ":" + itemId;
byte[] stock = connection.get(stockKey.getBytes());
if (stock == null) {
return false;
}
long stockValue = Long.parseLong(new String(stock));
if (stockValue <= 0) {
return false;
}
// 检查是否已购买
boolean bought = connection.sIsMember(
orderKey.getBytes(),
String.valueOf(userId).getBytes()
);
if (bought) {
return false;
}
// 原子扣减
connection.decrBy(stockKey.getBytes(), 1);
connection.sAdd(orderKey.getBytes(),
String.valueOf(userId).getBytes());
return true;
}
);
if (!success) {
return SeckillResult.fail("秒杀失败");
}
// 3. 发送异步消息
SeckillMessage message = new SeckillMessage(userId, itemId);
rocketMQTemplate.syncSend("seckill-order-topic", message);
return SeckillResult.success("抢购成功,请耐心等待");
}
}
七、实战经验总结
7.1 优化前后的对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| QPS | 500 | 5000 | 10x |
| 平均响应时间 | 2000ms | 50ms | 40x |
| 数据库连接数峰值 | 1500 | 80 | 18x |
| 系统可用性 | 85% | 99.99% | - |
| 慢查询数 | 500+/小时 | <10/小时 | 50x |
7.2 核心原则
1. 缓存优先:能走缓存的绝不查库
2. 异步解耦:写操作异步化,削峰填谷
3. 隔离保护:秒杀业务独立部署,避免雪崩
4. 限流降级:网关层限流,服务层降级
5. 监控告警:实时监控,快速响应
7.3 避坑指南
⚠️ 血的教训:
- 不要在生产环境开大的连接池,200连接可能直接拖垮数据库
- 不要在事务中做网络请求,锁会一直持有
- 不要忽略主从延迟,敏感查询必须走主库
- 不要把所有数据都放Redis,热点数据才值得缓存
- 不要没有监控就上秒杀,出问题连定位都困难
写在最后
做高并发系统,就像在钢丝上跳舞。MySQL从来不是不能扛高并发,而是你需要给它足够的”空间”和”工具”。连接池、索引、读写分离、缓存、异步化——这五个维度环环相扣,缺任何一个都可能成为系统的短板。
记住一句话:不要让数据库做它不该做的事。查询走缓存,写入走异步,热点数据放Redis,这才是高并发场景的正确姿势。
希望这篇文章能帮你在下一次大促中,不再凌晨三点被报警电话惊醒。
