首页 抖音热门文章正文

SpringBoot3 中常用限流算法全面解析

抖音热门 2025年08月10日 13:35 1 admin
SpringBoot3 中常用限流算法全面解析

在互联网软件开发领域,随着业务规模的不断扩大和用户量的持续增长,系统面临的流量压力日益增大。对于基于 SpringBoot3 构建的应用系统而言,合理的限流策略成为了保障系统稳定运行、防止因流量过载而导致服务崩溃的关键手段。今天,我们就来深入探讨一下 SpringBoot3 中常用的几种限流算法,帮助各位开发者更好地应对高并发场景下的流量挑战。

固定窗口算法

(一)算法原理

固定窗口算法是最为基础且简单的限流算法。它将时间划分为一个个固定长度的窗口,在每个窗口内,系统会对请求进行计数。当请求到达时,计数器增加 1,如果在当前窗口内计数器的值超过了预先设定的阈值,后续的请求就会被拒绝。直到这个固定窗口结束,计数器清零,新的窗口开始,计数过程重新开始。

例如,设定固定窗口时长为 1 分钟,限流阈值为 100 次请求。在这 1 分钟内,每收到一个请求,计数器就加 1,当计数器达到 100 时,后续请求都将被系统拒绝,直到下一分钟,计数器归零,重新计数。

(二)实现方式

在 SpringBoot3 项目中,可以借助 Redis 来实现固定窗口限流算法。我们可以利用 Redis 的原子自增操作来实现计数器功能。当一个请求到达时,通过 Redis 的 INCR 命令对指定的计数器键值进行自增操作。同时,设置该键值的过期时间为固定窗口的时长。如果自增后的结果超过了限流阈值,则拒绝该请求。

代码示例如下:

@RestControllerpublic class FixedWindowController {    @Autowired    private StringRedisTemplate stringRedisTemplate;    private static final String FIXED_WINDOW_KEY = "fixed_window_limit";    private static final int LIMIT_THRESHOLD = 100; // 限流阈值    private static final long WINDOW_DURATION = 60; // 窗口时长,单位秒    @GetMapping("/fixedWindow")    public String fixedWindow() {        String key = FIXED_WINDOW_KEY;        // 使用Redis的INCR命令对计数器进行自增        Long count = stringRedisTemplate.opsForValue().increment(key, 1);        if (count == 1) {            // 如果是第一次计数,设置键值的过期时间为窗口时长            stringRedisTemplate.expire(key, WINDOW_DURATION, TimeUnit.SECONDS);        }        if (count > LIMIT_THRESHOLD) {            // 如果超过限流阈值,返回提示信息            return "请求过于频繁,请稍后再试";        }        return "请求成功";    }}

(三)优缺点分析

优点

  • 实现简单:算法逻辑清晰,易于理解和实现,对于一些对限流精度要求不高、流量相对平稳的场景较为适用。
  • 资源消耗低:不需要复杂的数据结构和大量的计算资源,仅需简单的计数器和时间窗口设置。

缺点

  • 临界问题明显:在窗口切换的瞬间,可能会出现流量突增的情况。例如,假设限流阈值为 100 次请求每分钟,在第一个窗口的最后一秒内有 90 次请求,而在第二个窗口的开始一秒内又有 90 次请求,那么在这两秒内实际上有 180 次请求,超过了预期的限流效果,可能导致系统在短时间内承受过大压力。
  • 限流不够平滑:流量控制不够细腻,在窗口内要么允许请求,要么拒绝请求,容易造成流量的剧烈波动,对系统的稳定性有一定影响。

滑动窗口算法

(一)算法原理

滑动窗口算法是对固定窗口算法的优化。它将固定窗口进一步细分,把一个大的时间窗口划分为多个小的子窗口,每个子窗口都有自己独立的计数器。随着时间的推移,窗口像滑动一样移动,每次移动一个子窗口的时间间隔。系统通过统计滑动窗口内所有子窗口的计数器之和来判断是否达到限流阈值。

例如,将 1 分钟的大窗口划分为 60 个 1 秒的子窗口。在每个子窗口内,对到达的请求进行计数。当窗口滑动时,最旧的子窗口的计数被移除,新的子窗口加入统计范围。这样可以更精确地控制流量,避免固定窗口算法在窗口切换时出现的临界问题。

(二)实现方式

同样可以基于 Redis 来实现滑动窗口限流算法。我们可以使用 Redis 的有序集合(Sorted Set)来存储每个子窗口的请求计数。有序集合的成员是子窗口的时间戳,分值是该子窗口内的请求数量。

代码示例如下:

@RestControllerpublic class SlidingWindowController {    @Autowired    private StringRedisTemplate stringRedisTemplate;    private static final String SLIDING_WINDOW_KEY = "sliding_window_limit";    private static final int LIMIT_THRESHOLD = 100; // 限流阈值    private static final long WINDOW_DURATION = 60; // 窗口时长,单位秒    private static final long SUB_WINDOW_DURATION = 1; // 子窗口时长,单位秒    @GetMapping("/slidingWindow")    public String slidingWindow() {        String key = SLIDING_WINDOW_KEY;        long now = System.currentTimeMillis() / 1000;        // 当前子窗口的时间戳        long currentSubWindow = now - now % SUB_WINDOW_DURATION;        // 使用Redis的ZINCRBY命令增加当前子窗口的计数        stringRedisTemplate.opsForZSet().incrementScore(key, currentSubWindow, 1);        // 移除过期的子窗口        long startTime = now - WINDOW_DURATION;        stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, startTime);        // 统计滑动窗口内的请求总数        Set<ZSetOperations.TypedTuple<String>> tuples = stringRedisTemplate.opsForZSet()               .rangeByScoreWithScores(key, startTime, now);        long count = tuples.stream().mapToLong(ZSetOperations.TypedTuple::getScore).sum();        if (count > LIMIT_THRESHOLD) {            return "请求过于频繁,请稍后再试";        }        return "请求成功";    }}

(三)优缺点分析

优点

  • 解决临界问题:通过将窗口细分并滑动,有效避免了固定窗口算法在窗口切换时可能出现的流量突增问题,限流效果更加平滑和精确。
  • 适应流量变化:能够更好地适应流量的动态变化,对于一些流量波动较大的场景有较好的限流效果。

缺点

  • 实现复杂度增加:相比于固定窗口算法,滑动窗口算法需要维护多个子窗口的计数器以及进行窗口的滑动操作,代码实现相对复杂,对开发者的要求更高。
  • 资源消耗增加:由于需要存储和处理多个子窗口的数据,在 Redis 中占用的内存空间相对较大,对系统资源有一定的要求。

令牌桶算法

(一)算法原理

令牌桶算法的核心思想是系统以固定的速率向一个虚拟的令牌桶中放入令牌,每个请求在执行前需要从令牌桶中获取一个令牌。如果令牌桶中有足够的令牌,请求可以顺利通过并消耗一个令牌;如果令牌桶为空,没有令牌可供请求获取,那么该请求将被拒绝或者等待,直到有新的令牌被放入桶中。

令牌桶具有一定的容量限制,当令牌生成的速率大于请求消耗令牌的速率时,令牌桶会逐渐被填满,直到达到桶的最大容量。此时,新生成的令牌将被丢弃,以保证令牌桶中的令牌数量不会超过其容量。

例如,设定令牌生成速率为每秒 10 个,令牌桶容量为 100 个。系统会每秒向令牌桶中放入 10 个令牌,当有请求到达时,请求尝试从桶中获取一个令牌。如果桶中有令牌,请求获取令牌后继续执行;如果桶为空,请求将被限流。

(二)实现方式

在 SpringBoot3 项目中,可以借助 Google Guava 库来实现令牌桶算法。Guava 库提供了 RateLimiter 类来方便地实现令牌桶限流。

代码示例如下:

@RestControllerpublic class TokenBucketController {    private static final RateLimiter rateLimiter = RateLimiter.create(10); // 每秒生成10个令牌    @GetMapping("/tokenBucket")    public String tokenBucket() {        if (rateLimiter.tryAcquire()) {            // 如果成功获取令牌,处理请求            return "请求成功";        } else {            // 如果获取令牌失败,返回提示信息            return "请求过于频繁,请稍后再试";        }    }}

(三)优缺点分析

优点

  • 支持突发流量:允许在短时间内处理大量请求,只要令牌桶中有足够的令牌。这对于一些存在突发流量的业务场景,如秒杀活动、热点新闻访问等非常适用,能够保证在高并发瞬间系统仍能正常响应部分请求。
  • 限流效果平滑:通过令牌的生成和消耗机制,能够使请求的处理速率相对平稳,避免了因流量突然变化而对系统造成的冲击,有利于维持系统的稳定性。

缺点

  • 实现复杂度较高:相较于固定窗口算法,令牌桶算法的实现涉及到令牌的生成、存储和消耗等多个环节,需要更复杂的代码逻辑和数据结构来支持。
  • 对时间精度要求高:令牌是以固定速率生成的,这就要求系统的时间精度较高。如果系统时间不准确,可能会导致令牌生成的速率与预期不符,从而影响限流效果。

漏桶算法

(一)算法原理

漏桶算法可以将请求看作是水流,将系统处理能力看作是一个底部有小孔的固定容量的水桶。请求像水流一样不断地流入漏桶,而漏桶则以固定的速率从底部的小孔流出,即系统以固定的速率处理请求。当流入的请求速率超过漏桶的流出速率时,由于漏桶的容量有限,多余的请求(即溢出的水)将被丢弃或者排队等待处理。

例如,假设漏桶的容量为 100,流出速率为每秒 10 个请求。当请求以每秒 15 个的速率流入时,漏桶会以每秒 10 个的速率处理请求,多余的每秒 5 个请求会被限流,要么被丢弃,要么进入队列等待。

(二)实现方式

在 SpringBoot3 中,可以通过自定义过滤器和队列来模拟漏桶算法的实现。我们可以使用一个阻塞队列来存储请求,通过控制从队列中取出请求的速率来模拟漏桶的流出速率。

代码示例如下:

@Componentpublic class LeakyBucketFilter implements Filter {    private static final int BUCKET_CAPACITY = 100; // 漏桶容量    private static final int LEAK_RATE = 10; // 漏桶流出速率,单位:请求/秒    private final BlockingQueue<ServletRequest> requestQueue = new ArrayBlockingQueue<>(BUCKET_CAPACITY);    private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();    @Override    public void init(FilterConfig filterConfig) throws ServletException {        // 定时从队列中取出请求进行处理,模拟漏桶的流出        executorService.scheduleAtFixedRate(() -> {            try {                requestQueue.take();            } catch (InterruptedException e) {                Thread.currentThread().interrupt();            }        }, 0, 1000 / LEAK_RATE, TimeUnit.MILLISECONDS);    }    @Override    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)            throws IOException, ServletException {        if (requestQueue.offer((ServletRequest) servletRequest)) {            filterChain.doFilter(servletRequest, servletResponse);        } else {            ((HttpServletResponse) servletResponse).sendError(HttpServletResponse.SC_TOO_MANY_REQUESTS, "请求过于频繁,请稍后再试");        }    }    @Override    public void destroy() {        executorService.shutdown();    }}

(三)优缺点分析

优点

  • 有效平滑流量:能够将突发的流量转化为稳定的流出速率,避免了流量的剧烈波动对系统造成的冲击,保证系统处理请求的速率相对稳定,有利于系统的稳定运行。
  • 简单易懂:算法原理直观,易于理解和实现,对于一些对流量稳定性要求较高、对突发流量处理能力要求不高的场景较为适用。

缺点

  • 无法应对突发流量:由于漏桶的流出速率是固定的,在面对突发的大量请求时,超出漏桶容量的请求会被立即丢弃或者排队等待,可能导致部分业务请求无法及时得到处理,影响用户体验。
  • 资源利用率较低:在流量较低时,漏桶仍以固定速率处理请求,可能会造成系统资源的浪费,因为此时系统有能力处理更多的请求,但受限于漏桶算法的机制无法充分利用资源。

算法对比与选择

不同的限流算法各有优缺点,在实际应用中,我们需要根据具体的业务场景和需求来选择合适的限流算法。以下是对这几种常用限流算法的对比总结:

限流算法

优点

缺点

适用场景

固定窗口算法

实现简单、资源消耗低

临界问题明显、限流不够平滑

流量相对平稳、对限流精度要求不高的场景

滑动窗口算法

解决临界问题、适应流量变化

实现复杂度增加、资源消耗增加

流量波动较大、对限流精度要求较高的场景

令牌桶算法

支持突发流量、限流效果平滑

实现复杂度较高、对时间精度要求高

存在突发流量、对系统稳定性要求较高的场景

漏桶算法

有效平滑流量、简单易懂

无法应对突发流量、资源利用率较低

对流量稳定性要求高、对突发流量处理能力要求不高的场景

例如,如果是一个电商平台的秒杀活动,由于会在短时间内产生大量的突发请求,并且需要保证系统能够在高并发下稳定运行,此时令牌桶算法可能是比较合适的选择;而对于一个提供稳定数据查询服务的接口,流量相对平稳,对系统资源消耗较为敏感,固定窗口算法或漏桶算法可能就能够满足需求。

总结

在 SpringBoot3 开发中,合理选择和应用限流算法对于保障系统的稳定性和可靠性至关重要。通过对固定窗口算法、滑动窗口算法、令牌桶算法和漏桶算法的深入了解,我们可以根据不同的业务场景和流量特点,灵活选择最合适的限流策略,有效应对高并发流量带来的挑战,为用户提供更加稳定、高效的服务。希望本文能够帮助各位互联网软件开发人员在实际项目中更好地运用限流算法,提升系统的性能和用户体验。

发表评论

泰日号Copyright Your WebSite.Some Rights Reserved. 网站地图 备案号:川ICP备66666666号 Z-BlogPHP强力驱动