今天咱们聊点实在的——MySQL在高并发场景下该怎么玩。这不是一道简单的理论题,而是很多互联网大厂每天面临的实际挑战。就拿我接触过的一个电商系统案例来说吧,那真叫一个“热闹”。每次大促,流量如洪水般袭来,数据库差点被冲垮。今天我就把这个案例里的坑怎么填、索引怎么调、表怎么拆,一股脑儿都给你掰扯清楚。
一、背景:为什么我们这么头疼?
先说个大家可能没意识到的小秘密:MySQL并不是天生为高并发而生的。它的设计初衷更偏向于事务性、一致性和易用性,但在面对海量并发写入和读请求时,性能瓶颈很快就会暴露出来。
就拿那个电商系统的订单表(order)来举例子吧,一年下来有几亿条记录。平时查询速度还凑合,一到“双11”秒杀期间,秒级订单量飙到几万,数据库CPU利用率直接飙到90%+,响应时间从200ms飙到3秒以上,用户直接“劝退”。
这时候光靠加内存、升级硬件已经不够了,得从软件层面入手——也就是我们今天重点讲的索引优化 + 分库分表。
二、第一步:索引优化——别让MySQL“瞎跑”
1. 问题定位:执行计划告诉你真相
我们先用 EXPLAIN 看看SQL是怎么跑的。
假设有一个常见的查询语句:
SELECT * FROM order WHERE user_id = 123456 AND status = 'paid' ORDER BY create_time DESC LIMIT 10;
执行后看到:
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | order | ALL | NULL | 50M | Using where; Using temporary; Using filesort |
看到没有?type=ALL 表示全表扫描!5000万行数据啊!这能不慢吗?而且 Using filesort 说明排序也没用上索引,还要自己排一遍。
2. 解决方案:创建合适联合索引
根据查询条件,我们创建一个覆盖式联合索引:
ALTER TABLE order ADD INDEX idx_user_status_time (user_id, status, create_time);
再次执行 EXPLAIN:
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | order | ref | idx_user_status_time | 1200 | Using index covering |
哇!现在用了索引,跳过了全表扫描,甚至都不用回查主键树了(Using index covering),性能提升不止一倍。
💡 小技巧:记得把最常出现在WHERE子句中的字段放前面,再是筛选度高的字段,最后是ORDER/GROUP BY字段。这就是所谓的“最左匹配原则”。
3. 避坑指南:别乱建索引!
很多人以为索引越多越好,其实大错特错。每个索引都会占用存储空间,并且在写操作(INSERT/UPDATE/DELETE)时需要维护索引结构,反而会拖慢写入速度。
比如上面的订单表,如果再加个索引 idx_create_time,那么每次插入一条订单,就要更新两个索引,成本double加倍。所以一定要按需建索引,用 SHOW INDEX FROM table_name 查看已有索引,删掉那些没用的。
三、第二步:分库分表——当一张表扛不住时怎么办
到了这个阶段,即使索引再优化,面对几十亿级别的单表,依然力不从存了。这时候就得考虑水平分表或垂直分库。
1. 什么是分库分表?
- 分表(Sharding by Table):将一个大表拆成多个小表,比如按
user_id % 100拆成100张表:order_0,order_1, …,order_99。 - 分库(Sharding by Database):将不同业务的数据放在不同的数据库中,比如订单库、商品库、用户库分开部署。
2. 实际案例:如何拆分订单表?
在这个电商系统中,订单数据量增长极快,且大部分查询是按 user_id 查询用户的订单列表。于是我们决定按 user_id 进行模运算分表。
设计思路:
- 总共有 100 个子表:
order_0~order_99 - 路由规则:
table_index = user_id % 100 - 插入时:根据
user_id计算目标表,插入对应子表 - 查询时:同样计算目标表,只查那张表
Java伪代码示例(使用JdbcTemplate):
public void saveOrder(Order order) {
int tableIndex = order.getUserId() % 100;
String sql = "INSERT INTO order_" + tableIndex + "(user_id, product_id, amount, create_time) VALUES(?, ?, ?, ?)";
jdbcTemplate.update(sql, order.getUserId(), order.getProductId(), order.getAmount(), order.getCreateTime());
}
public List<Order> getOrdersByUser(int userId) {
int tableIndex = userId % 100;
String sql = "SELECT * FROM order_" + tableIndex + " WHERE user_id = ? ORDER BY create_time DESC LIMIT ?";
return jdbcTemplate.query(sql, new OrderRowMapper(), userId, 10);
}
⚠️ 注意:这里动态拼接表名存在SQL注入风险,在实际项目中建议使用参数化或使用框架提供的分片机制(如MyCat、ShardingSphere)。
3. 分布式事务怎么办?
分库分表后,跨表、跨库的事务就变成了难题。比如退款操作涉及订单表和资金表,但它们在分库上不一致。
解决办法:
- 尽量避免跨库事务;
- 若必须使用,引入可靠消息队列 + 最终一致性方案(如RocketMQ事务消息);
- 或者采用中间件支持的事务模式,如Seata的AT模式。
4. 分页和聚合函数怎么处理?
分表后,COUNT(*)、SUM()、OFFSET LIMIT 这类操作会变得复杂。
例如要查“所有用户前1000个最新订单”,就不能简单分页了,需要从各子表分别查结果,然后合并排序再取前1000 —— 这叫“全局分页”,性能开销较大。
✅ 推荐做法:
- 限制业务范围:如“查某用户的订单” → 直接命中单表;
- 对需要统计的场景,提前计算好缓存在Redis中;
- 对于复杂的报表任务,走离线计算引擎(如Spark/Flink),不压在线库。
四、进阶技巧:读写分离 + 缓存缓存再缓存
除了索引和分库分表,还有几个重要手段可以进一步提升系统稳定性:
1. 读写分离(Read/Write Splitting)
主库负责写,从库负责读。通过配置Proxy层(如MaxRoute、Vitess)自动路由SQL。
# example configuration for MyCat
dataNode dn1 => host='master', db='order_db';
dataNode dn2 => host='slave1', db='order_db';
dataNode dn3 => host='slave2', db='order_db';
rule: read-write-splitting-rule
这样写入走主库,读走从库,有效分担压力。
2. Redis缓存高频数据
热门商品信息、用户Session、秒杀库存等,统统塞进Redis。
比如商品详情页:
def get_product_detail(product_id):
cache_key = f"product:{product_id}"
data = redis.get(cache_key)
if data:
return json.loads(data)
else:
db_data = mysql.query("SELECT * FROM product WHERE id=?", product_id)
redis.setex(cache_key, 300, json.dumps(db_data)) # 缓存5分钟
return db_data
设置合理的过期时间和降级策略,防止缓存击穿、雪崩。
3. 异步削峰
对于非实时敏感的操作(如发送短信、积分发放、日志记录),放入消息队列异步处理。
使用 Kafka / RocketMQ / RabbitMQ 等中间件,把突发流量“削峰填谷”,保护后端数据库。
五、总结:一套完整的抗压组合拳
回顾一下整个链路:
| 层级 | 技术手段 | 作用 |
|---|---|---|
| SQL层面 | 合理索引、避免SELECT * | 减少IO和CPU消耗 |
| 架构层面 | 分库分表 | 横向扩展存储容量与吞吐 |
| 网络层面 | 读写分离 | 提升读取性能 |
| 应用层面 | 缓存+消息队列 | 屏蔽瞬时高峰,解耦依赖 |
这些都不是孤立的,而是一个系统工程。就像开车上山,你不能只顾着踩油门(加硬件),还要会换挡(索引调整)、规划路线(分片策略)、检查胎压(监控报警),才能稳稳抵达山顶。
六、最后送你一句话
“不要等到系统挂了才想到优化,要在它还跑得动的时候就动手改。”
如果你正在搭建一个新系统,请从一开始就考虑高可用性和可扩展性;如果你接手的是一个老系统,那就勇敢 refactor,哪怕一点点改进也能带来巨大收益。
希望这篇干货对你有帮助!有任何问题欢迎评论区交流~ 👇
