springboot整合redis及lua脚本实现接口限流

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: springboot整合redis及lua脚本实现接口限流

接口限流说明

接口限流是指在某些场景下,对某个接口的请求进行限制,以避免因请求过多而导致的系统负载过高、资源耗尽等问题。通常情况下,接口限流可以通过一定的算法来实现,比如令牌桶算法、漏桶算法、计数器算法等。这些算法可以根据接口的不同特点和业务需求,对请求进行限制和平滑处理,以达到系统资源的最优化利用。

令牌桶算法

令牌桶算法(Token Bucket Algorithm):令牌桶算法可以通过限制请求的速率,来保护系统免受突发流量的冲击。该算法将请求和令牌都存放在一个桶中,每个请求需要从桶中取出一个令牌,如果桶中没有令牌,则请求将被拒绝。该算法能够平滑限制请求的速率,避免系统被突发流量打垮。

优点:

  • 能够平滑限制请求的速率,适合对流量进行平滑的限制。
  • 对于短时间内的流量突发,可以处理突发请求,保护系统不被打垮。

缺点:

  • 实现相对较为复杂,需要维护令牌桶的状态。
  • 无法应对突发的大量请求。

适用场景:

  • 对于流量比较稳定的系统,需要对请求进行平滑限制的场景。
  • 对于需要对请求进行按照一定速率限制的场景。

漏桶算法

漏桶算法(Leaky Bucket Algorithm):漏桶算法与令牌桶算法相似,也是通过限制请求的速率来保护系统免受突发流量的冲击。该算法将请求放入一个漏桶中,每个请求都需要占据一定的空间,如果漏桶已满,则请求将被拒绝。该算法能够平滑限制请求的速率,但无法应对突发流量。

优点:

  • 能够平滑限制请求的速率,适合对流量进行平滑的限制。
  • 对于流量突发的情况,能够防止系统被过载。

缺点:

  • 无法应对突发的大量请求。
  • 实现相对较为复杂,需要维护漏桶的状态。

适用场景:

  • 对于需要对请求进行按照一定速率限制的场景。

计数器算法

计数器算法(Counting Algorithm):计数器算法是最简单的限流算法,通过对每个接口的请求数进行计数,并对其进行限制,来保护系统。该算法能够很好地限制请求的数量,但无法平滑限制请求的速率。

优点:

  • 实现简单,易于实现。

缺点:

  • 无法平滑限制请求的速率。
  • 无法应对突发的大量请求。

适用场景:

  • 对于需要对请求进行简单计数的场景。
  • 对于不需要进行流量平滑限制的场景。

滑动窗口算法

滑动窗口算法(Sliding Window Algorithm):滑动窗口算法可以通过限制请求的速率,来保护系统免受突发流量的冲击。该算法将请求按照时间顺序放入一个固定大小的窗口中,如果窗口已满,则新的请求将被拒绝。该算法能够平滑限制请求的速率,但无法应对突发流量。

优点:

  • 能够平滑限制请求的速率,适合对流量进行平滑的限制。
  • 对于短时间内的流量突发,可以处理突发请求,保护系统不被打垮。

缺点:

  • 实现相对较为复杂,需要维护窗口的状态。
  • 无法应对长时间的流量突发。

适用场景:

  • 对于流量比较稳定的系统,需要对请求进行平滑限制的场景。
  • 对于需要对请求进行按照一定速率限制的场景。

实现基于令牌桶+redis进行接口限流

这里我是基于令牌桶算法进行了变种,也就是针对不同的用户以及不同的方法在不同的时刻进行了限制,当然这个仅仅看个人业务

1️⃣:引入maven坐标

<!--lua脚本-->
<dependency>
  <groupId>org.luaj</groupId>
  <artifactId>luaj-jse</artifactId>
  <version>3.0.1</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2️⃣:定义接口限制注解

package test.bo.work.redislimit.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * @author xiaobo
 * @date 2023/3/13
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    // 限制名称
    String key();
    // 次数
    int limit();
    // 秒
    int seconds();
}

3️⃣:lua脚本实现

local current = tonumber(redis.call('get', KEYS[1]) or '0')
-- 判断是否还有令牌可用
if current + 1 > tonumber(ARGV[1]) then
    return 0
else
    redis.call('incrby', KEYS[1], 1)
    redis.call('expire', KEYS[1], ARGV[2])
    return 1
end

4️⃣:引入lua脚本

package test.bo.work.redislimit;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.file.Files;
/**
 * @author xiaobo
 * @date 2023/3/13
 */
@Component
public class RedisLuaScripts {
    @Value("classpath:/lua/ratelimit.lua")
    private Resource rateLimitScriptResource;
    public RedisScript<Long> getRateLimitScript() throws IOException {
        String script = new String(Files.readAllBytes(rateLimitScriptResource.getFile().toPath()));
        return new DefaultRedisScript<>(script, Long.class);
    }
}

5️⃣: 定义一个接口限制的AOP

package test.bo.work.redislimit.assept;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import test.bo.work.config.exception.BusinessException;
import test.bo.work.redislimit.RedisLuaScripts;
import test.bo.work.redislimit.annotation.RateLimit;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
 * @author xiaobo
 * @date 2023/3/13
 */
@Aspect
@Component
public class RateLimitAspect {
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedisLuaScripts redisLuaScripts;
    @Around("@annotation(rateLimit)")
    public Object checkRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        // 获取请求头中的AREA_TOKEN
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
        String areaToken = request.getHeader("AREA_TOKEN");
        // 获取注解上的参数
        String key = rateLimit.key();
        int limit = rateLimit.limit();
        int seconds = rateLimit.seconds();
        String redisKey = key + areaToken + "-" + LocalDate.now();
        RedisScript<Long> script = redisLuaScripts.getRateLimitScript();
        List<String> keys = Collections.singletonList(redisKey);
        List<String> args = Arrays.asList(String.valueOf(limit), String.valueOf(seconds));
        Long result = redisTemplate.execute(script, keys, args.toArray());
        if (result != null && result == 1) {
            return joinPoint.proceed();
        } else {
            throw new BusinessException(500,"Rate limit exceeded for " + key);
        }
    }
}

🔚:接口实现

package test.bo.work.redislimit.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import test.bo.work.entity.OpResult;
import test.bo.work.redislimit.annotation.RateLimit;
/**
 * @author xiaobo
 * @date 2023/3/13
 */
@RestController
@Slf4j
public class RateLimitController {
    @PostMapping("rateLimit")
    @RateLimit(key = "rateLimit", limit = 10, seconds = 1)
    public OpResult testRateLimit() {
        return OpResult.Ok("success");
    }
}

说明

基于令牌桶算法的变种在正常情况下可以很好地限制接口的访问速率,但在短时间内突然出现大量请求的情况下,该算法可能会出现较大的误差。原因是在突发请求的情况下,桶内已经有很多令牌,但这些令牌并不能很快地被消耗,导致一些请求得到了允许,而另一些请求被拒绝。而漏桶算法则不会出现这个问题,因为它是基于固定速率漏水的方式进行限流,无论突发请求多少,都不会超出限制速率。

⚠️:经过jmeter测试确实出现了这样的情况

如果是需要对大量用户进行限流,建议使用更高效的限流算法,比如漏桶算法,或基于漏桶算法的Token Bucket算法

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
24天前
|
缓存 NoSQL Redis
Redis 脚本
10月更文挑战第18天
31 3
|
1月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
59 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
1月前
|
缓存 分布式计算 NoSQL
大数据-43 Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
大数据-43 Redis 功能扩展 Lua 脚本 对Redis扩展 eval redis.call redis.pcall
29 2
|
1月前
|
NoSQL Java 关系型数据库
阿里 P7二面:Redis 执行 Lua,到底能不能保证原子性?
Redis 和 Lua,两个看似风流马不相及的技术点,为何能产生“爱”的火花,成为工作开发中的黄金搭档?技术面试中更是高频出现,Redis 执行 Lua 到底能不能保证原子性?今天就来聊一聊。 
79 1
|
30天前
|
NoSQL Redis API
限流+共享session redis实现
【10月更文挑战第7天】
35 0
|
NoSQL Redis
Redis下Lua脚本的复制模式
假设我们的Redis选择了主从架构, 和AOF持久化方式。我们执行一条写命令时, 该条命令会被发送到从服务器, 和追加到AOF文件中。当我们执行的不是一条命令, 而是Lua脚本时, 默认情况下, 整个Lua脚本的内容会进行复制, 但是存在一些特殊情况。
2129 0
Redis下Lua脚本的复制模式
|
1月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
|
1月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
74 6
|
6天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
7天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构