连载中 5/20

单机限流落地:注解、AOP 与那些工程细节

2026-07-08 · 3807 阅读 · 0 评论 · 0 赞

算法不等于工程

前三篇把窗口、滑动窗口、令牌桶的算法备齐了,但每个接口里手写一段 tryAcquire 显然不现实:限流是横切关注点,该用横切的方式接入——AOP 切面统一处理,业务代码只留一个注解。本篇把单机限流从「算法」推进到「工程」,顺手补齐那些注解背后必须想清楚的细节。

一个注解走天下

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String key();                 // 限流标识,支持 SpEL
    double qps() default 100;     // 每秒放行数
    long timeout() default 0;     // 抢不到令牌最多等多久(毫秒),0 直接拒
}
@Aspect
@Component
public class RateLimitAspect {
    private final Map<String, RateLimiter> cache = new ConcurrentHashMap<>();

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        String key = parseKey(rateLimit.key(), pjp);          // 解析 SpEL
        RateLimiter limiter = cache.computeIfAbsent(key,
                k -> RateLimiter.create(rateLimit.qps()));
        boolean ok = rateLimit.timeout() > 0
                ? limiter.tryAcquire(rateLimit.timeout(), TimeUnit.MILLISECONDS)
                : limiter.tryAcquire(0);
        if (!ok) {
            throw new BusinessException(LIMIT_EXCEEDED);       // 统一限流异常
        }
        return pjp.proceed();
    }
}

业务侧用起来只剩一行声明:

@RateLimit(key = "order:create:" + "#{#userId}", qps = 50, timeout = 200)
@PostMapping("/order/create")
public Result createOrder(@RequestParam Long userId) { ... }

四个不能省的细节

细节一:key 要能动态拼。「接口级 200 QPS」往往不够,真正要的是「每个用户 1 QPS、接口整体 200」——key 必须支持 SpEL 取参数(userId、IP、商品 ID),维度在注解里声明,切面里解析。细节二:限流异常要友好。抛 LIMIT_EXCEEDED 专用业务码,前端转成「活动太火爆,稍后再试」,而不是裸 500——限流的本质是主动拒绝,拒绝的姿势决定用户体验的下限。细节三:降级优先于拒绝。能读缓存兜底的活动信息,限流触发时读缓存返回,比硬邦邦的「稍后再试」体面——拒绝是下策,降级是中策(第 13 篇专讲)。细节四:触发率要上报。限流次数是关键指标(Micrometer 打点):触发率长期为零说明阈值虚高白设了;突然飙升说明要么流量异常、要么容量缩水——限流指标是容量问题的烟雾报警器

单机限流的天花板

注解切面再精致,单机限流有一个绕不过的结构性问题:各实例各数各的,总量不可控。想全接口限 1000,10 个实例每台限 100?前提是负载均衡绝对均匀——现实是长尾实例可能分到 200,总量冲到 2000;扩缩容之后实例数一变,每台的阈值全部失真,阈值成了跟实例数耦合的常数。高峰期弹性扩容 10 台变 15 台,每台还是 100,总限额凭空涨了 50%——保护形同虚设。同时,按用户的公平性也碎了:同一用户的两个请求被均衡到不同实例,各自计数互不知晓,「每人 1 QPS」翻倍变 2。

结论水到渠成:单机限流适合「防单实例被打死」的自保型场景(比如每个实例对某个慢下游最多 10 并发),凡是要精确控制「总量」与「每人配额」的,计数器必须集中——放 Redis 里,用 Lua 保证原子。下一篇就写这个:分布式限流的 Redis 加 Lua 完整实现,顺带把令牌桶的分布式版一并解决。

503

10 年全栈工程师 · 503咖啡馆主理人

#单机限流#注解#AOP#SpEL#限流异常#监控打点

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞