从限流算法、缓存策略到消息队列,一套能保命的高并发实战指南

沉默聊科技 2026-01-07 09:38:57
限流算法怎么选?

 

缓存更新怎么做?

 

消息队列细节怎么踩稳?

 

一篇吃透,真正掌握抵御高并发的底气。

 

 

一、限流

 

1、为什么要限流?

 

想象下:

 

你开一辆小车,在高速路口突然涌来10万辆车,不管不顾往里冲。

 

你的发动机再好、刹车再灵,都必然“嗝屁”。

 

系统也是一样:负载过大,不崩才怪。

 

所以限流,就是设置一道闸门——

 

来多少车,按节奏、按流速放行,保护后端系统。

 

2、常见限流算法

 

限流,主要靠两大算法流派:

 

漏桶(Leaky Bucket) 和 令牌桶(Token Bucket)。

 

我们来逐个看看:

 

1)漏桶算法(Leaky Bucket)

 

坚持匀速,不怕狂风暴雨。

 

原理:

 

  • 系统有一个漏桶,水(请求)不断灌进来。
  • 桶底的小洞以固定速率漏水(处理请求)。
  • 桶满了,新的水就溢出,直接丢弃。

 

举个例子:

 

排队办证大厅,一个号一个号叫,叫得再快也要等叫号,急没用。

 

特点总结:

 

适合场景:

 

  • 后台批处理系统
  • 对抖动极为敏感的金融、结算业务

 

2)令牌桶算法(Token Bucket)

 

灵活发牌,允许短时爆发。

 

原理:

 

  • 桶里定时加入令牌(token)。
  • 请求来时,先拿一个令牌,才能被处理。
  • 没令牌的请求,要么排队,要么丢弃。

 

举个例子:

 

麦当劳点单机,后台根据顾客数发小票号,允许一阵快一阵慢。

 

特点总结:

 

 

适合场景:

 

  • 电商秒杀系统
  • 短时高峰的营销活动

 

3)漏桶 VS 令牌桶,对比总结

 

 

实际工程应用

 

一般系统组合使用:

 

  • 网关层:漏桶,兜住整体流速
  •  
  • 业务接口:令牌桶,允许高峰弹性

 

再配合限流框架,比如:

 

  • Sentinel
  • Nginx限速模块

 

建议:

 

  • 别一味丢请求,高并发系统要做智能限流+友好降级提示
  • 比如:“系统繁忙,请稍后再试。” 而不是直接500爆掉。

 

二、缓存

 

为什么缓存是高并发第一护盾?

 

因为内存访问(比如Redis)是纳秒级的,数据库IO是毫秒级的。

 

直接查DB?卡得飞起。

 

打Cache?秒回。

 

所以,缓存搞不好,系统直接废

 

1、缓存和数据库常见问题

 

缓存用得多了,必然碰到这些:

 

  • 缓存与数据库数据不一致
  •  
  • 缓存击穿、穿透、雪崩

 

每一个,都是血泪史。

 

那,怎么做对?

 

2、常见的缓存更新策略

 

1)Cache Aside(旁路缓存)

 

流程:

 

  • 查缓存 → 缓存没命中 → 查数据库 → 写入缓存
  • 更新时 → 更新数据库 → 删除缓存

 

举个例子:

 

  • 你找车钥匙,先翻抽屉(缓存),找不到再去问爸妈(DB),拿到后用完放回抽屉。

 

优点:

 

  • 简单直观

 

  • 读多写少的场景很适合

 

缺点:

 

  • 写更新期间,可能出现缓存和DB短暂不一致

 

2)Write Through(写穿缓存)

 

流程:

 

写的时候直接同步更新缓存和数据库。

 

举个例子:

 

  • 新买了车钥匙,马上放抽屉一份,保险箱也一份。

 

优点:

 

  • 保证一致性更好

 

缺点:

 

  • 写入链路变长,性能开销大

 

3)Refresh Ahead(提前刷新)

 

流程:

 

  • 缓存快过期时,自动后台刷新。

 

举个例子:

 

  • 知道抽屉钥匙快坏了,提前重新配好一把放进去。

 

优点:

 

  • 热点数据持续在线防止击穿

 

缺点:

 

  • 需要定时任务维护

 

3、缓存一致性怎么搞?

 

要记住一条大原则:

 

更新数据库 → 删除缓存 → 允许下次查询时再重建缓存

 

而不是:更新缓存 → 更新数据库(会乱套)

 

高并发下加一招:延迟双删

 

  • 第一次删缓存
  • 延迟几十ms,再次删缓存(兜住并发写冲突)

 

4、如何防击穿、穿透、雪崩?

 

  • 防击穿(单热点爆缓存):缓存空对象+短TTL
  • 防穿透(查不存在的key):布隆过滤器拦截
  • 防雪崩(大批量缓存同时过期):加随机TTL分散过期时间

 

三、消息队列

 

为什么需要消息队列?

 

因为再快的后端,也扛不住一波洪水直接冲。

 

MQ就像一个缓冲池,把高并发的请求拦下来,慢慢处理。

 

1、消息队列应用场景

 

  • 秒杀抢购:下单请求排队
  • 大促结算:异步写账单
  • 积分发放:异步通知

 

2、MQ用得好,三个关键细节

 

1)幂等性(Idempotent)

 

MQ消息可能重复发送,如果消费端不防,业务就炸了。

 

解决方案:

 

  • 消费前先查数据库:看操作是否已完成
  • 每条消息带唯一业务ID(如订单ID)

 

举个例子:

 

  • 像快递签收,不能重复签一份。

 

2)消费重试(Retry)

 

网络闪断、临时异常,导致消费失败。

 

解决方案:

 

  • 设置消费重试机制
  • 达到重试上限,打入死信队列(DLQ)

 

举个例子:

 

  • 快递员三次派件失败,快递放驿站。

 

3)顺序性保障(Ordering)

 

某些场景(比如库存扣减),消息顺序不能乱。

 

解决方案:

 

  • 同一业务key(如订单ID)哈希分到同一个分区
  • 单线程顺序消费

 

举个例子:

 

  • 像考场排队入场,按准考证号顺序一个个进门。

 

3、消息堆积怎么处理?

 

当消费跟不上生产:

 

  • 加消费并发(多线程、多实例)
  • 限速生产(源头削峰)
  • 按优先级消费(重要业务优先)

 

要记住一句话:削峰填谷,才是MQ最大的价值。

 

四、实战案例

 

用一个常规 Spring Boot 项目来演示:

 

环境配置:

 

 

1、创建Spring Boot工程

 

选依赖时勾选:

 

  • Spring Web
  • Sentinel
  • Redis
  • RabbitMQ

 

Maven pom.xml :

 


  
  
    org.springframework.boot
    spring-boot-starter-web
  
  
  
    com.alibaba.csp
    sentinel-spring-boot-starter
    1.8.6
  
  
  
    org.springframework.boot
    spring-boot-starter-data-redis
  
  
  
    org.springframework.boot
    spring-boot-starter-amqp
  

 

2、Sentinel 接入限流接口

 

1)安装 Sentinel Dashboard(控制台),运行:

 

java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -jar sentinel-dashboard-1.8.6.jar

 

2)application.yml 配置:

 

spring:
  cloud:
    sentinel:
      transport:
        dashboard:
          localhost:8080

 

3)定义一个简单接口:

 

@RestController
@RequestMapping("/order")
public class OrderController {
  @GetMapping("/create")
  @SentinelResource(value = "createOrder", blockHandler = "handleBlock")
  public String createOrder() {
    return "订单创建成功!";    
  }
  public String handleBlock(BlockException ex) {
      return "系统繁忙,请稍后再试~";    
  }
}

 

4)启动后,去 Sentinel 控制台添加流控规则:

 

  • 资源名:createOrder
  • 阈值类型:QPS
  • 单机阈值:10(每秒10次)

 

5)效果:

 

  • 正常流量返回“订单创建成功”
  • 超过QPS,返回“系统繁忙”

 

3、Redis 缓存-订单详情

 

1)Redis配置(application.yml):

 

spring:
    redis:
      host: localhost
      port: 6379

 

2)服务类:

 

@Service
public class OrderService {
  @Autowired
  private RedisTemplate redisTemplate;
  private static final String ORDER_CACHE_PREFIX="order:";
  public String getOrderDetail(String orderId) {
    String cacheKey= ORDER_CACHE_PREFIX + orderId;
    // 先查缓存
    Stringdetail= redisTemplate.opsForValue().get(cacheKey);
    if (detail != null) {
      return "【缓存命中】" + detail;        
    }
    // 模拟数据库查询        
    detail = "订单详情 - " + orderId;
    // 写入缓存        
    redisTemplate.opsForValue().set(cacheKey, detail, Duration.ofMinutes(10));
    return "【数据库查询】" + detail;    
  }
}

 

3)控制器调用:

 

@RestController
@RequestMapping("/order")
public class OrderController {
  @Autowired
  private OrderService orderService;
  @GetMapping("/detail/{orderId}")
  public String getOrderDetail(@PathVariable String orderId) {
    return orderService.getOrderDetail(orderId);    
  }
 }

 

4)测试:

 

  • 第一次查 /order/detail/123,走数据库
  • 第二次查 /order/detail/123,走缓存

 

4、RabbitMQ 异步下单

 

1)application.yml 配置:

 

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

 

2)消息模型:

 

  • 队列:order.queue
  •  
  • 生产者发消息
  •  
  • 消费者监听并处理

 

3)创建队列配置:

 

@Configuration
public class RabbitConfig {
  public static final String ORDER_QUEUE="order.queue";
  @Beanpublic 
  Queue orderQueue() {
    return newQueue(ORDER_QUEUE, true);    
  }
}

 

4)生产者发送消息:

 

@Service
public class OrderProducer {
  @Autowired 
  private RabbitTemplate rabbitTemplate;
  public void sendOrder(String orderId) {
    rabbitTemplate.convertAndSend(RabbitConfig.ORDER_QUEUE, orderId);    
  }
}

 

5)消费者监听处理:

 

@Component
public class OrderConsumer {
  @RabbitListener(queues = RabbitConfig.ORDER_QUEUE)
  public void handleOrder(String orderId) {        
    System.out.println("接收到订单处理请求:" + orderId);
    // 异步处理下单逻辑    
  }
}

 

6)控制器触发异步下单:

 

@RestController
@RequestMapping("/order")
public class OrderController {
  @Autowired private OrderProducer orderProducer;
  @GetMapping("/asyncCreate/{orderId}")
  public String asyncCreate(@PathVariable String orderId) {
    orderProducer.sendOrder(orderId);
    return"订单异步创建中,请稍后查看结果~";    
  }
}

 

7)测试:

 

  • 调用 /order/asyncCreate/10001
  • 控制台打印:“接收到订单处理请求:10001”

 

如果你看到这里,恭喜你已经掌握了一套真正能抗住高并发的硬核武器。

 

作者丨沉默聊科技

来源丨公众号:架构师沉默(ID:CM_IT-1024)

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

 

最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告