"老板说要抗住双11的流量,我看着现在1000 QPS就挂的接口陷入了沉思..." 😰
📖 什么是高并发?
想象一下:
- 低并发:小饭馆,每次来3-5个客人,厨师一个人搞定 🍜
- 高并发:春运火车站,同时涌入1万人抢票,工作人员忙得像陀螺 🎫🏃🏃🏃
高并发的核心指标:
- QPS(每秒查询数):Queries Per Second
- TPS(每秒事务数):Transactions Per Second
- RT(响应时间):Response Time
并发等级分类:
低并发:QPS < 100
中并发:QPS 100-1000
高并发:QPS 1000-10000 ← 我们的目标
超高并发:QPS > 10000 ← 神仙级别
🎯 高并发优化核心思路
高并发优化金字塔
/\
/ \
/限流\
/降级熔断\ ← 保护层
/──────────\
/ 异步化 \ ← 解耦层
/ 消息队列削峰 \
/────────────────\
/ 缓存 \ ← 加速层
/ Redis / 本地缓存 \
/──────────────────────\
/ 数据库优化 \ ← 基础层
/ 索引 / 读写分离 / 分库分表 \
/────────────────────────────\
/ 硬件优化 \ ← 根基
/ SSD / 多核CPU / 大内存 / 网络 \
──────────────────────────────────
🔥 第一层:缓存优化 - 提速的秘密武器
为什么要缓存?
无缓存:
用户请求 → API → 数据库查询(100ms)→ 返回
每次都查数据库,数据库:我太累了!😫
有缓存:
用户请求 → API → Redis查询(1ms)→ 返回
数据库:我可以休息一下了!😎
性能提升:100倍!
缓存策略 1:多级缓存架构
┌──────────┐
│ 浏览器 │ ← L1:浏览器缓存(静态资源)
└─────┬────┘
↓
┌──────────┐
│ CDN │ ← L2:CDN缓存(图片、视频)
└─────┬────┘
↓
┌──────────┐
│ Nginx │ ← L3:Nginx本地缓存
└─────┬────┘
↓
┌──────────┐
│ 应用 │ ← L4:应用本地缓存(Caffeine/Guava)
└─────┬────┘
↓
┌──────────┐
│ Redis │ ← L5:分布式缓存
└─────┬────┘
↓
┌──────────┐
│ MySQL │ ← L6:数据库(最后一道防线)
└──────────┘
原则:能用前面的就不用后面的,层层拦截!
缓存策略 2:Cache Aside 模式(最常用)
// ✅ 读取数据
public User getUser(Long userId) {
// 1. 先查缓存
String cacheKey = "user:" + userId;
User user = redis.get(cacheKey);
if (user != null) {
return user; // 缓存命中,直接返回 🎯
}
// 2. 缓存未命中,查数据库
user = userMapper.selectById(userId);
// 3. 写入缓存
if (user != null) {
redis.setex(cacheKey, 3600, user); // 过期时间1小时
}
return user;
}
// ✅ 更新数据
public void updateUser(User user) {
// 1. 先更新数据库
userMapper.updateById(user);
// 2. 删除缓存(下次查询时重新加载)
String cacheKey = "user:" + user.getId();
redis.del(cacheKey);
}
为什么是"删除缓存"而不是"更新缓存"?
场景:商品价格从 100 改成 50
❌ 更新缓存:
并发请求1:更新DB(100→50)→ 更新缓存(50)
并发请求2:更新DB(50→30) → 更新缓存(30)
如果顺序错乱 → 缓存和DB不一致!
✅ 删除缓存:
并发请求1:更新DB(100→50)→ 删除缓存
并发请求2:更新DB(50→30) → 删除缓存
下次查询时从DB加载最新数据 → 一定一致!
缓存策略 3:热点数据预热
// 系统启动时预加载热点数据
@PostConstruct
public void warmUpCache() {
log.info("开始预热缓存...");
// 1. 预热热门商品
List<Product> hotProducts = productMapper.selectHotProducts(100);
for (Product product : hotProducts) {
redis.setex("product:" + product.getId(), 3600, product);
}
// 2. 预热配置信息
Map<String, Object> configs = configMapper.selectAll();
redis.hmset("system:config", configs);
log.info("缓存预热完成!");
}
生活比喻:餐厅提前把热门菜品的食材准备好,客人一点单立刻就能做 🍽️
缓存三大问题及解决方案 ⚠️
问题 1:缓存穿透(查不存在的数据)
恶意用户疯狂查询 userId = -1(数据库里不存在)
→ 缓存里没有
→ 每次都打到数据库
→ 数据库被打崩!💥
解决方案:
// ✅ 方案1:缓存空对象
public User getUser(Long userId) {
String cacheKey = "user:" + userId;
User user = redis.get(cacheKey);
if (user != null) {
if (user.getId() == null) {
return null; // 空对象,直接返回
}
return user;
}
user = userMapper.selectById(userId);
if (user == null) {
// 缓存一个空对象,过期时间短一点(避免占用太多内存)
redis.setex(cacheKey, 60, new User()); // 1分钟
} else {
redis.setex(cacheKey, 3600, user);
}
return user;
}
// ✅ 方案2:布隆过滤器(最优)
@Autowired
private BloomFilter<Long> userBloomFilter;
public User getUser(Long userId) {
// 先用布隆过滤器判断是否存在
if (!userBloomFilter.mightContain(userId)) {
return null; // 一定不存在,直接返回
}
// 可能存在,继续查询...
// ...
}
// 系统启动时加载所有用户ID到布隆过滤器
@PostConstruct
public void initBloomFilter() {
List<Long> allUserIds = userMapper.selectAllIds();
allUserIds.forEach(userBloomFilter::put);
}
问题 2:缓存击穿(热点数据过期)
热门商品的缓存过期了
→ 同一时刻1000个请求进来
→ 都发现缓存没有
→ 1000个请求同时查数据库
→ 数据库瞬间崩溃!💥
解决方案:
// ✅ 方案1:互斥锁(只让一个线程查数据库)
public User getUser(Long userId) {
String cacheKey = "user:" + userId;
User user = redis.get(cacheKey);
if (user != null) {
return user;
}
// 缓存未命中,尝试获取分布式锁
String lockKey = "lock:user:" + userId;
boolean locked = redis.setNX(lockKey, "1", 10); // 10秒过期
if (locked) {
try {
// 获取锁成功,查数据库
user = userMapper.selectById(userId);
redis.setex(cacheKey, 3600, user);
return user;
} finally {
redis.del(lockKey); // 释放锁
}
} else {
// 没获取到锁,等待一会再查缓存(此时第一个线程应该已经加载好了)
Thread.sleep(50);
return getUser(userId); // 递归查询
}
}
// ✅ 方案2:永不过期(推荐)
public User getUser(Long userId) {
String cacheKey = "user:" + userId;
CacheObject cacheObj = redis.get(cacheKey);
if (cacheObj != null) {
// 检查逻辑过期时间
if (cacheObj.getExpireTime() > System.currentTimeMillis()) {
return cacheObj.getData(); // 未过期,返回数据
}
// 逻辑过期了,异步更新缓存
threadPoolExecutor.execute(() -> {
User user = userMapper.selectById(userId);
CacheObject newObj = new CacheObject(user,
System.currentTimeMillis() + 3600000);
redis.set(cacheKey, newObj); // 注意:不设置过期时间
});
// 先返回旧数据(保证可用性)
return cacheObj.getData();
}
// 首次加载
// ...
}
问题 3:缓存雪崩(大量缓存同时过期)
双11活动,所有商品缓存设置了1小时过期
→ 1小时后,所有缓存同时失效
→ 所有请求同时打到数据库
→ 数据库:救命啊!💀
解决方案:
// ✅ 过期时间加随机值
int expireTime = 3600 + new Random().nextInt(300); // 3600-3900秒
redis.setex(cacheKey, expireTime, product);
// ✅ 多级缓存 + 降级
public Product getProduct(Long productId) {
// L1:本地缓存
Product product = localCache.get(productId);
if (product != null) return product;
// L2:Redis
product = redis.get("product:" + productId);
if (product != null) {
localCache.put(productId, product);
return product;
}
// L3:数据库(加分布式锁)
// ...
}
// ✅ 缓存预热 + 定时刷新
@Scheduled(fixedRate = 600000) // 每10分钟刷新一次
public void refreshHotCache() {
List<Product> hotProducts = productMapper.selectHotProducts(100);
for (Product product : hotProducts) {
redis.setex("product:" + product.getId(), 3600, product);
}
}
⚡ 第二层:异步化 - 解耦提速
为什么要异步?
同步调用:
用户下单 → 创建订单(100ms)→ 扣减库存(50ms)→ 发送短信(200ms)
→ 发送邮件(150ms)→ 更新积分(80ms)→ 返回结果
总耗时:580ms 😫
异步调用:
用户下单 → 创建订单(100ms)→ 发MQ消息 → 立即返回结果
总耗时:100ms ✅ 😎
后台异步处理:
MQ消费者1:扣减库存(50ms)
MQ消费者2:发送短信(200ms)
MQ消费者3:发送邮件(150ms)
MQ消费者4:更新积分(80ms)
性能提升:5倍以上!
异步方案 1:@Async 注解(简单场景)
@Service
public class OrderService {
@Autowired
private NotificationService notificationService;
public OrderVO createOrder(OrderDTO dto) {
// 1. 创建订单(核心流程,同步)
Order order = new Order();
// ... 保存订单
orderMapper.insert(order);
// 2. 发送通知(非核心流程,异步)
notificationService.sendNotification(order.getId());
// 3. 立即返回
return OrderVO.from(order);
}
}
@Service
public class NotificationService {
@Async // 异步执行
public void sendNotification(Long orderId) {
// 发送短信
smsService.send(orderId);
// 发送邮件
emailService.send(orderId);
}
}
// 配置线程池
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
异步方案 2:消息队列(推荐)
@Service
public class OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public OrderVO createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = new Order();
orderMapper.insert(order);
// 2. 发送MQ消息(异步)
OrderMessage message = new OrderMessage(order.getId(), order.getUserId());
rocketMQTemplate.convertAndSend("order-topic", message);
// 3. 立即返回
return OrderVO.from(order);
}
}
// 消息消费者
@Service
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderConsumer implements RocketMQListener<OrderMessage> {
@Override
public void onMessage(OrderMessage message) {
// 异步处理
// 1. 扣减库存
inventoryService.deduct(message.getOrderId());
// 2. 发送通知
notificationService.send(message.getOrderId());
// 3. 更新积分
pointService.add(message.getUserId(), 100);
}
}
MQ 的优势:
- ✅ 削峰填谷(应对流量洪峰)
- ✅ 系统解耦(订单系统和库存系统解耦)
- ✅ 异步提速(响应时间大幅降低)
- ✅ 可靠性高(消息持久化,不会丢失)
🛡️ 第三层:限流降级 - 保命的最后一道防线
为什么要限流?
黄牛用脚本疯狂刷接口:
正常用户:每秒点击 1 次
黄牛脚本:每秒请求 1000 次
如果不限流:
100个黄牛 = 10万QPS → 服务器瞬间崩溃 💥
限流后:
每个IP限制每秒10次请求
100个黄牛 = 1000 QPS → 服务器稳如狗 😎
限流算法 1:固定窗口计数器
// 简单但有临界问题
public class FixedWindowRateLimiter {
private AtomicInteger counter = new AtomicInteger(0);
private long windowStart = System.currentTimeMillis();
private int limit = 100; // 每秒100次
public boolean tryAcquire() {
long now = System.currentTimeMillis();
// 新窗口,重置计数器
if (now - windowStart > 1000) {
counter.set(0);
windowStart = now;
}
// 判断是否超过限制
if (counter.incrementAndGet() <= limit) {
return true; // 允许通过
} else {
return false; // 拒绝
}
}
}
// ❌ 临界问题:
// 0.9秒时来了100个请求 ✅
// 1.1秒时又来了100个请求 ✅
// 实际0.2秒内来了200个请求!突破限制!
限流算法 2:滑动窗口(推荐)
public class SlidingWindowRateLimiter {
private Queue<Long> timestamps = new LinkedList<>();
private int limit = 100;
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
// 移除1秒前的时间戳
while (!timestamps.isEmpty() && now - timestamps.peek() > 1000) {
timestamps.poll();
}
// 判断是否超过限制
if (timestamps.size() < limit) {
timestamps.offer(now);
return true;
} else {
return false;
}
}
}
优势:解决了临界问题,更平滑!
限流算法 3:令牌桶(最优)
// Google Guava 的 RateLimiter
public class TokenBucketDemo {
// 每秒生成100个令牌
private RateLimiter rateLimiter = RateLimiter.create(100);
@GetMapping("/api/product/{id}")
public Result<Product> getProduct(@PathVariable Long id) {
// 获取1个令牌,最多等待100ms
if (!rateLimiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS)) {
return Result.fail("系统繁忙,请稍后再试");
}
// 业务逻辑...
Product product = productService.getById(id);
return Result.success(product);
}
}
特点:
- ✅ 允许突发流量(桶里积攒的令牌可以一次性用掉)
- ✅ 平滑限流(令牌匀速生成)
- ✅ 支持预热(系统启动时逐渐提高速率)
分布式限流(Redis + Lua)
@Service
public class RedisRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
// Lua脚本(原子操作)
private static final String LUA_SCRIPT =
"local key = KEYS[1] " +
"local limit = tonumber(ARGV[1]) " +
"local current = tonumber(redis.call('get', key) or '0') " +
"if current + 1 > limit then " +
" return 0 " + // 超过限制
"else " +
" redis.call('INCRBY', key, 1) " +
" redis.call('expire', key, 1) " + // 1秒过期
" return 1 " + // 允许通过
"end";
public boolean tryAcquire(String key, int limit) {
RedisScript<Long> script = RedisScript.of(LUA_SCRIPT, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(key), String.valueOf(limit));
return result != null && result == 1;
}
}
// 使用
@GetMapping("/api/seckill/{id}")
public Result<String> seckill(@PathVariable Long id, HttpServletRequest request) {
String ip = getClientIP(request);
String key = "rate_limit:seckill:" + ip;
// 每个IP每秒最多10次请求
if (!rateLimiter.tryAcquire(key, 10)) {
return Result.fail("请求过于频繁");
}
// 秒杀逻辑...
return Result.success("秒杀成功");
}
服务降级
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, Product> redisTemplate;
// 降级开关(可以通过配置中心动态控制)
@Value("${feature.recommend.enabled:true}")
private boolean recommendEnabled;
public ProductDetailVO getProductDetail(Long productId) {
ProductDetailVO vo = new ProductDetailVO();
// 1. 核心数据:商品基本信息(必须查询)
Product product = productMapper.selectById(productId);
vo.setProduct(product);
// 2. 非核心数据:推荐商品(可降级)
if (recommendEnabled) {
try {
List<Product> recommends = getRecommendProducts(productId);
vo.setRecommends(recommends);
} catch (Exception e) {
log.warn("获取推荐商品失败,降级处理", e);
vo.setRecommends(Collections.emptyList()); // 返回空列表
}
}
// 3. 非核心数据:用户评论(可降级)
if (canQueryComments()) { // 根据系统负载决定是否查询
vo.setComments(getComments(productId));
} else {
vo.setComments(Collections.emptyList()); // 降级:不返回评论
}
return vo;
}
// 判断是否可以查询评论(根据系统负载)
private boolean canQueryComments() {
// 当前QPS > 5000 时,降级评论查询
return metricsService.getCurrentQPS() < 5000;
}
}
降级策略:
- 自动降级:系统负载高时自动关闭非核心功能
- 手动降级:通过配置中心开关控制
- 降级顺序:评论 → 推荐 → 图片 → 核心数据
🔧 第四层:数据库优化
读写分离
应用服务器
|
┌──────┴──────┐
↓ ↓
写操作 读操作
↓ ↓
主库(Master) 从库(Slave1, Slave2, Slave3)
| / | \
└─ 同步 ─→ ...
// 配置多数据源
@Configuration
public class DataSourceConfig {
@Bean
public DataSource masterDataSource() {
// 主库配置
}
@Bean
public DataSource slaveDataSource() {
// 从库配置(可以配置多个)
}
@Bean
public DataSource dynamicDataSource() {
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setDefaultTargetDataSource(masterDataSource());
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave", slaveDataSource());
dataSource.setTargetDataSources(targetDataSources);
return dataSource;
}
}
// 动态切换数据源
@Transactional
public void updateUser(User user) {
// 写操作 → 主库
DataSourceContextHolder.set("master");
userMapper.updateById(user);
}
public User getUser(Long id) {
// 读操作 → 从库
DataSourceContextHolder.set("slave");
return userMapper.selectById(id);
}
效果:
- 主库:处理写操作(INSERT、UPDATE、DELETE)
- 从库:处理读操作(SELECT)
- 读写分离后,读操作QPS可以提升 5-10 倍!
分库分表
原来:单库单表
orders 表(1亿条数据)
→ 查询慢、写入慢、索引大
分库分表后:
db_0.orders_0 (250万)
db_0.orders_1 (250万)
db_1.orders_0 (250万)
db_1.orders_1 (250万)
...共 4个库 × 10个表 = 40张表
每张表只有 250 万数据 → 查询飞快!
分片策略:
// 根据用户ID分片
int dbIndex = userId % 4; // 库编号 0-3
int tableIndex = (userId / 4) % 10; // 表编号 0-9
String tableName = "db_" + dbIndex + ".orders_" + tableIndex;
📊 完整优化方案对比
| 优化方案 | 性能提升 | 实现难度 | 适用场景 |
|---|---|---|---|
| Redis缓存 | 10-100倍 | ⭐⭐ | 查询接口 |
| 本地缓存 | 100-1000倍 | ⭐ | 配置/字典 |
| 异步化 | 3-5倍 | ⭐⭐ | 非核心流程 |
| 消息队列 | 5-10倍 | ⭐⭐⭐ | 削峰填谷 |
| 限流 | - | ⭐⭐ | 防止打垮 |
| 降级 | - | ⭐⭐ | 保证核心 |
| 读写分离 | 5-10倍 | ⭐⭐⭐ | 读多写少 |
| 分库分表 | 10-50倍 | ⭐⭐⭐⭐⭐ | 海量数据 |
🎯 实战案例:秒杀接口优化
优化前(1000 QPS就挂)
@PostMapping("/seckill/{productId}")
public Result<String> seckill(@PathVariable Long productId, Long userId) {
// 1. 查询商品(查数据库)
Product product = productMapper.selectById(productId);
if (product.getStock() <= 0) {
return Result.fail("库存不足");
}
// 2. 扣减库存(更新数据库)
product.setStock(product.getStock() - 1);
productMapper.updateById(product);
// 3. 创建订单(插入数据库)
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
orderMapper.insert(order);
// 4. 发送短信(同步调用,200ms)
smsService.send(userId, "秒杀成功");
return Result.success("秒杀成功");
}
问题:
- ❌ 每次都查数据库(库存信息)
- ❌ 扣库存有并发问题(超卖)
- ❌ 同步发送短信(慢)
- ❌ 没有限流(黄牛可以刷)
优化后(支持10万+ QPS)
@PostMapping("/seckill/{productId}")
public Result<String> seckill(@PathVariable Long productId,
@RequestHeader("User-Id") Long userId,
HttpServletRequest request) {
// 1. 限流(每个用户每秒最多1次)
String limitKey = "seckill:limit:" + userId;
if (!redisRateLimiter.tryAcquire(limitKey, 1)) {
return Result.fail("请求过于频繁");
}
// 2. 检查是否已经秒杀过(防止重复下单)
String userKey = "seckill:user:" + productId + ":" + userId;
if (redis.exists(userKey)) {
return Result.fail("您已经参与过秒杀");
}
// 3. Redis扣减库存(原子操作)
String stockKey = "seckill:stock:" + productId;
Long stock = redis.decr(stockKey);
if (stock < 0) {
// 库存不足,回滚
redis.incr(stockKey);
return Result.fail("库存不足");
}
// 4. 标记用户已秒杀
redis.setex(userKey, 86400, "1"); // 24小时过期
// 5. 异步创建订单(发MQ消息)
SeckillMessage message = new SeckillMessage(userId, productId);
rocketMQTemplate.convertAndSend("seckill-topic", message);
// 6. 立即返回
return Result.success("秒杀成功,订单生成中...");
}
// 异步消费者
@RocketMQMessageListener(topic = "seckill-topic", consumerGroup = "seckill-consumer")
public class SeckillConsumer implements RocketMQListener<SeckillMessage> {
@Override
public void onMessage(SeckillMessage message) {
try {
// 1. 创建订单
Order order = new Order();
order.setUserId(message.getUserId());
order.setProductId(message.getProductId());
orderMapper.insert(order);
// 2. 扣减数据库库存(兜底)
productMapper.decrStock(message.getProductId());
// 3. 发送短信(异步)
smsService.send(message.getUserId(), "秒杀成功");
} catch (Exception e) {
log.error("秒杀订单创建失败", e);
// 回滚Redis库存
redis.incr("seckill:stock:" + message.getProductId());
}
}
}
优化效果:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| QPS | 1,000 | 100,000+ | 100倍 |
| RT(响应时间) | 300ms | 10ms | 30倍 |
| 数据库压力 | 高 | 低 | 90%降低 |
| 超卖问题 | 有 | 无 | ✅ |
💡 面试加分回答模板
面试官:"如何优化一个高并发接口?"
标准回答:
"我会从以下几个层面进行优化:
1. 缓存层:
- 使用 Redis 缓存热点数据,减少数据库压力
- 本地缓存 + Redis 组成二级缓存
- 解决缓存穿透(布隆过滤器)、击穿(互斥锁)、雪崩(过期时间随机化)
2. 异步化:
- 非核心流程使用 @Async 或 MQ 异步处理
- 消息队列削峰填谷,提升系统吞吐量
3. 限流降级:
- 使用令牌桶算法限流,防止系统被打垮
- 分布式限流(Redis + Lua脚本)
- 系统负载高时自动降级非核心功能
4. 数据库优化:
- 读写分离:主库写,从库读
- 分库分表:单表数据量控制在 500 万以内
- 索引优化、SQL优化
5. 架构优化:
- 微服务拆分,避免单点故障
- 容器化部署,支持弹性扩容
实际案例:我曾经优化过一个秒杀接口,通过 Redis 扣库存 + MQ 异步下单 + 限流,将 QPS 从 1000 提升到 10 万+,响应时间从 300ms 降到 10ms 以内。"
🎉 总结
高并发优化的本质:
- 能缓存的都缓存 🎯
- 能异步的都异步 ⚡
- 能限流的都限流 🛡️
- 能降级的都降级 🔻
记住这句话:
缓存解决慢的问题
异步解决卡的问题
限流解决崩的问题
降级解决死的问题
最后送你一个公式 🎁:
高并发系统 = 缓存 + 异步 + 限流 + 降级 + 分布式
掌握这5招,走遍天下都不怕!😎
祝你的接口永远快如闪电,稳如泰山! 🚀⚡