一、那个崩溃的夜晚,我们才真正理解并发
去年双十二,我们公司的秒杀活动刚上线三分钟,告警群就炸了。
“数据库连接数爆满!” “主从延迟超过5秒!” “用户反馈抢不到商品!”
我盯着监控大屏上那条垂直拉升的曲线,心里一沉。5000人同时点击“立即抢购”,我们的MySQL 8.0主库直接跪了。
后来复盘,发现根本不是代码逻辑的问题,而是架构层面根本没考虑高并发场景。今天就把这个血泪教训完整分享出来,从Redis缓存穿透到分库分表,一步步教你怎么救。
二、先搞清楚:5000并发到底可怕在哪里?
很多人以为5000人同时抢商品而已,数据库扛得住。错了。
2.1 真实压测数据揭秘
我们用的配置是:
- MySQL 8.0,8核16G,InnoDB引擎
- 单机部署,主从架构
- 商品表100万条,秒杀表50万条
正常情况:
- QPS 200,TPS 150,响应时间 <50ms
5000并发时:
- QPS 直接冲到 8000+
- 连接数爆满(默认151,我们改到2000还是不够)
- 响应时间 2秒+
- 死锁率 15%
- 从库延迟 10秒+
2.2 瓶颈在哪里?
三个致命问题:
① 连接池被打爆
每个请求都要建立数据库连接,5000人同时来,连接数直接炸。MySQL默认max_connections=151,我们改成2000还是不够,因为连接建立本身就有开销。
② 行锁竞争严重 秒杀商品只有一件,5000人同时抢同一行,InnoDB的行锁直接争抢。死锁率飙升到15%,超时率30%。
③ 磁盘IO扛不住 大量随机IO,磁盘带宽饱和。 SSD还好,机械盘直接跪。
三、第一道防线:Redis缓存层(解决读压力)
3.1 为什么一定要加缓存?
MySQL是关系型数据库,擅长事务,但不擅长高并发读。
秒杀场景的特点:
- 读多写少(99%是查询商品详情)
- 热点数据集中(就那几件秒杀商品)
- 实时性要求高(库存要准)
解决方案: 把热点数据放到Redis里,99%的读请求打到缓存,只有写请求(扣库存)才去数据库。
3.2 完整架构设计
用户请求 → Nginx → Spring Cloud Gateway → 服务层
↓
Redis缓存层
(热点数据)
↓
MySQL主库
(写操作)
↓
MySQL从库
(读操作)
关键设计点:
① 缓存结构
# 商品详情
SETEX item:detail:{id} 300 '{"id":1,"name":"手机","price":999}'
# 秒杀库存
SETEX mds:stock:{id} 60 100
# 用户限购标记
SETEX mds:bought:{uid}:{sid} 300 1
② 缓存预热 活动开始前5分钟,把所有秒杀商品详情和库存加载到Redis。
@Scheduled(cron = "0 */5 * * * ?")
public void preheatCache() {
List<SeckillItem> items = seckillMapper.selectAll();
for (SeckillItem item : items) {
// 商品详情缓存300秒
stringRedisTemplate.opsForValue()
.setex("item:detail:" + item.getId(), 300, JSON.toJSONString(item));
// 库存缓存60秒
stringRedisTemplate.opsForValue()
.setex("mds:stock:" + item.getId(), 60, String.valueOf(item.getStock()));
}
}
③ 缓存穿透防护 恶意用户疯狂查询不存在的商品,缓存不命中,直接打到数据库。
解决方案:布隆过滤器
// 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, 0.01
);
// 查询前先判断
if (!bloomFilter.mightContain(itemId)) {
return Result.error("商品不存在");
}
3.3 缓存一致性保证
问题: Redis和MySQL数据不一致怎么办?
解决方案:Cache-Aside Pattern
@Transactional
public ResultDTO seckill(Long itemId, Long uid) {
// 1. 先更新Redis(扣库存)
Long stock = redisTemplate.opsForValue().decrement("mds:stock:" + itemId);
if (stock < 0) {
redisTemplate.opsForValue().set("mds:stock:" + itemId, 0);
return Result.error("库存不足");
}
// 2. 再更新MySQL(写操作)
int affected = seckillMapper.insert(new SeckillOrder(uid, itemId));
if (affected == 0) {
// 失败,回滚Redis
redisTemplate.opsForValue().increment("mds:stock:" + itemId);
return Result.error("抢购失败");
}
// 3. 发送MQ消息(异步同步)
mqTemplate.send("seckill.order", JSON.toJSONString(new Order(uid, itemId)));
return Result.success("抢购成功");
}
关键点:
- 先更新缓存,再更新数据库(避免脏数据)
- 更新失败,回滚缓存
- 异步同步(MQ),保证最终一致
3.4 压测效果对比
优化前(直连MySQL):
- QPS 200,响应时间 50ms
- 5000并发直接崩
优化后(Redis缓存):
- QPS 5000+,响应时间 <10ms
- 5000并发稳稳扛住
- 数据库QPS 降到 500以下
四、第二道防线:MySQL优化(解决写压力)
4.1 数据库连接池优化
问题: 连接池被打爆怎么办?
解决方案:HikariCP + 连接池调优
spring:
datasource:
hikari:
maximum-pool-size: 50 # 最大连接数
minimum-idle: 10 # 最小空闲连接
connection-timeout: 3000 # 连接超时3秒
idle-timeout: 600000 # 空闲超时10分钟
max-lifetime: 1800000 # 连接最大存活30分钟
关键点:
maximum-pool-size不要设太大(50-100足够)- 连接池大小 = CPU核数 × 2 + 磁盘数
- 我们的配置:8核 → 50个连接
4.2 行锁优化
问题: 行锁争抢严重怎么办?
解决方案:乐观锁 + 库存分段
-- 乐观锁更新
UPDATE seckill_stock
SET stock = stock - 1, version = version + 1
WHERE item_id = #{itemId} AND stock > 0 AND version = #{version};
-- 如果受影响行数为0,说明并发冲突,重试
库存分段(高级):
-- 把100个库存分成10段,每段10个
CREATE TABLE seckill_stock_segment (
id BIGINT PRIMARY KEY,
item_id BIGINT,
segment_id INT,
stock INT,
version INT
);
-- 每个请求随机选一段,减少锁竞争
UPDATE seckill_stock_segment
SET stock = stock - 1, version = version + 1
WHERE item_id = #{itemId} AND segment_id = #{randomSegment} AND stock > 0;
4.3 索引优化
问题: 查询慢怎么办?
解决方案:覆盖索引 + 联合索引
-- 秒杀查询优化
CREATE INDEX idx_seckill_item_stock ON seckill_item(item_id, stock, status);
-- 订单查询优化
CREATE INDEX idx_order_user_item ON seckill_order(user_id, item_id, create_time);
关键点:
- 用
EXPLAIN分析慢查询 - 避免
SELECT *,只查需要的字段 - 联合索引顺序:等值条件 > 范围条件
4.4 分库分表(终极方案)
问题: 单库扛不住怎么办?
解决方案:ShardingSphere + 分库分表
# ShardingSphere配置
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db0
username: root
password: 123456
ds1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db1
username: root
password: 123456
rules:
sharding:
tables:
seckill_order:
actual-data-nodes: ds$->{0..1}.seckill_order$->{0..1}
database-strategy:
sharding-column: user_id
sharding-algorithm-name: database-inline
table-strategy:
sharding-column: order_id
sharding-algorithm-name: table-inline
sharding-algorithms:
database-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
table-inline:
type: INLINE
props:
algorithm-expression: seckill_order$->{order_id % 2}
关键点:
- 分库键选
user_id(均匀分布) - 分表键选
order_id(避免热点) - 单库单表数据量控制在1000万以内
五、完整压测报告:5000并发实测数据
5.1 优化前(直连MySQL)
- QPS:200
- 响应时间:50ms
- 5000并发:直接崩
- 错误率:100%
5.2 优化后(Redis + MySQL + 分库分表)
- QPS:8000+
- 响应时间:<10ms
- 5000并发:稳稳扛住
- 错误率:<0.1%
- 数据库QPS:500以下
- 死锁率:<0.01%
- 主从延迟:<100ms
5.3 压测工具推荐
- JMeter:开源,功能强大
- Wrk:高性能,适合HTTP压测
- sysbench:数据库压测专用
六、血泪教训:我们踩过的坑
6.1 缓存穿透
现象: 恶意用户疯狂查询不存在的商品,缓存不命中,直接打到数据库。
教训: 一定要加布隆过滤器,或者缓存空值。
// 缓存空值5秒
if (item == null) {
redisTemplate.opsForValue().setex("item:detail:" + itemId, 5, "{}");
return Result.error("商品不存在");
}
6.2 缓存雪崩
现象: 大量缓存同时过期,请求直接打到数据库。
教训: 过期时间加随机值。
// 过期时间300秒 + 随机0-60秒
int expireTime = 300 + new Random().nextInt(60);
redisTemplate.opsForValue().setex("item:detail:" + itemId, expireTime, JSON.toJSONString(item));
6.3 缓存击穿
现象: 热点Key过期,并发请求直接打到数据库。
教训: 使用互斥锁。
// 加锁重建缓存
if (!redisTemplate.hasKey("item:detail:" + itemId)) {
synchronized (this) {
if (!redisTemplate.hasKey("item:detail:" + itemId)) {
// 重建缓存
Item item = mapper.selectById(itemId);
redisTemplate.opsForValue().setex("item:detail:" + itemId, 300, JSON.toJSONString(item));
}
}
}
6.4 数据库死锁
现象: 5000人同时抢同一商品,行锁争抢严重,死锁率15%。
教训: 使用乐观锁 + 库存分段。
七、架构演进路线图
第一阶段:直连MySQL
├── 问题:500并发就崩
└── 解决:加Redis缓存
第二阶段:Redis + MySQL
├── 问题:5000并发写压力大
└── 解决:MySQL优化 + 乐观锁
第三阶段:Redis + MySQL优化 + 分库分表
├── 问题:单库还是扛不住
└── 解决:ShardingSphere分库分表
第四阶段:Redis + MySQL + 分库分表 + MQ异步
├── 问题:写操作阻塞
└── 解决:MQ异步削峰
八、完整代码仓库
GitHub地址: https://github.com/example/seckill-demo
技术栈:
- Spring Boot 3.x
- Redis 7.x
- MySQL 8.0
- ShardingSphere 5.x
- RabbitMQ 3.x
- Nginx
关键模块:
seckill-service:秒杀核心服务seckill-cache:Redis缓存层seckill-dao:数据库访问层seckill-mq:消息队列层seckill-sharding:分库分表配置
九、总结:高并发秒杀架构核心要点
九字真言:缓存先行、锁要乐观、分库分表。
- 缓存先行:99%读请求走Redis,只有写请求走MySQL
- 锁要乐观:用乐观锁代替悲观锁,减少锁竞争
- 分库分表:单库单表控制在1000万以内,用ShardingSphere自动分片
最后送一句话: 高并发不是靠加机器解决的,是靠架构设计解决的。Redis缓存 + MySQL优化 + 分库分表,这三道防线缺一不可。
希望这篇文章能帮你避开我们踩过的坑,5000并发稳稳扛住!
