漏桶算法的灵感源于漏斗,如下图所示:

641.webp

滑动时间算法有一个问题就是在一定范围内,比如 60s 内只能有 10 个请求,当第一秒时就到达了 10 个请求,那么剩下的 59s 只能把所有的请求都给拒绝掉,而漏桶算法可以解决这个问题。

漏桶算法类似于生活中的漏斗,无论上面的水流倒入漏斗有多大,也就是无论请求有多少,它都是以均匀的速度慢慢流出的。当上面的水流速度大于下面的流出速度时,漏斗会慢慢变满,当漏斗满了之后就会丢弃新来的请求;当上面的水流速度小于下面流出的速度的话,漏斗永远不会被装满,并且可以一直流出。

漏桶算法的实现步骤是,先声明一个队列用来保存请求,这个队列相当于漏斗,当队列容量满了之后就放弃新来的请求,然后重新声明一个线程定期从任务队列中获取一个或多个任务进行执行,这样就实现了漏桶算法。

上面我们演示 Nginx 的控制速率其实使用的就是漏桶算法,当然我们也可以借助 Redis 很方便的实现漏桶算法。

我们可以使用 Redis 4.0 版本中提供的 Redis-Cell 模块,该模块使用的是漏斗算法,并且提供了原子的限流指令,而且依靠 Redis 这个天生的分布式程序就可以实现比较完美的限流了。

Redis-Cell 实现限流的方法也很简单,只需要使用一条指令 cl.throttle 即可,使用示例如下:

> cl.throttle mylimit 15 30 60
1) (integer) 0 # 0 表示获取成功,1 表示拒绝
2) (integer) 15 # 漏斗容量
3) (integer) 14 # 漏斗剩余容量
4) (integer) -1 # 被拒绝之后,多长时间之后再试(单位:秒)-1 表示无需重试
5) (integer) 2 # 多久之后漏斗完全空出来

其中 15 为漏斗的容量,30 / 60s 为漏斗的速率。

redis-cell + springboot实现接口限流

pom.xml配置依赖如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

具体的 Java 实现代码如下:

@Slf4j
@Component
public class ApiLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * API调用频率
     *
     * @param apiName  接口名称
     * @param capacity 漏斗初始容量
     * @param rate     单位时间内访问频率/秒
     * @param seconds  单位时间(秒)
     * @param tokenNum 每次请求漏斗容量减少的数量
     * @return
     */
    public boolean apiRate(String apiName, int capacity, int rate, int seconds, int tokenNum) {
        try {
            DefaultRedisScript script = new DefaultRedisScript();
            script.setResultType(Long.class);
            /*
                cl.throttle命令返回的是一个table,这里只关注第一个元素0表示正常,1表示过载
                KEYS[1]需要设置的key值,结合业务需要可以是接口名称+用户ID+购买的资源包等等等等
                ARGV[1]漏斗初始容量
                ARGV[2]频率次数,结合ARGV[3]一起使用
                ARGV[3]周期(秒),结合ARGV[2]一起使用
                ARGV[4]每次请求漏斗容量减少的数量,默认为1
             */
            script.setScriptText("return redis.call('cl.throttle',KEYS[1], ARGV[1], ARGV[2], ARGV[3], ARGV[4])[1]");
            Long rst = (Long) redisTemplate.execute(script, Arrays.asList(apiName), capacity, rate, seconds, tokenNum);
            return rst == 0;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return false;
        }
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApiLimitTest {

    // 接口API
    private static final String API_NAME = "/user/list";

    // 漏斗初始容量
    private static final int CAPACITY = 5;

    // 单位时间内允许访问的频率
    private static final int RATE = 8;

    // 单位时间(秒),即每秒允许有8次访问
    private static final int SECONDS = 1;

    // 每次请求漏斗容量减少的数量
    private static final int TOCKEN_NUM = 1;


    @Autowired
    private ApiLimiter apiLimiter;

    @Test
    public void testCell() {
        for (int i = 0; i < 10; i++) {
            boolean allow = apiLimiter.apiRate(API_NAME, CAPACITY, RATE, SECONDS, TOCKEN_NUM);
            if (allow) {
                System.out.println("正常执行请求:" + i);
            } else {
                System.out.println("被限流了:" + i);
            }
        }
    }
}