基于 Redis 的分布式令牌桶、漏桶、滑动窗口实现

  • 漏桶
    • 实现
    • 功能性测试
  • 滑动窗口
    • 实现
    • 功能性测试
  • 令牌桶
    • 单机
    • 分布式
    • 功能性测试
  • 参考

本文的限流工具都只有功能性测试(见正文),未进行过高并发和大流量下的性能测试,生产环境下的性能未知,仅供参考。完整源码详见 github

 

漏桶


漏桶是最简单的限流工具,设计思路为:如果时间间隔达到规定的时间间隔,则允许通过,否则返回失败。


实现

LeakyLimiter 类中有四个属性,最核心的是 intervalNanos,表示时间间隔。如下所示:

    private final RedisService redisService;

    // 漏桶唯一标识
    private final String name;

    // 分布式互斥锁
    private final RLock lock;

    // 每两滴水之间的时间间隔
    private final long intervalNanos;

redisService 用于操作缓存,lock 表示分布式锁。

上锁和解锁的方法如下所示:

    /**
     * 尝试获取锁
     * @return 获取成功返回 true
     */
    private boolean lock() {
   
        try {
   
            // 等待 100 秒,获得锁 100 秒后自动解锁
            return this.lock.tryLock(100, 100, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 释放锁
     */
    private void unlock() {
   
        this.lock.unlock();
    }

在 acquire 方法中检查是否已达到时间间隔,如下所示:

    /**
     * 尝试通过漏桶
     *
     * @return 获取成功返回 true,失败返回 false
     */
    @Override
    public boolean acquire() {
   
        while (true) {
   
            if (lock()) {
   
                try {
   
                    return tryAcquire();
                } finally {
   
                    unlock();
                }
            }
        }
    }

    private boolean tryAcquire() {
   
        long recent = getRecent();
        long now = System.nanoTime();
        if (now - recent >= this.intervalNanos) {
   
            resync(now);
            return true;
        } else {
   
            log.info("Acquire LeakyLimiter[" + this.name + "] failed.");
            return false;
        }
    }

getRecent 的作用是获取当前时间,resync 是同步缓存中的最新时间戳。

    private long getRecent() {
   
        Long recent = redisService.get(LeakyBucketKey.leakyBucket, this.name, Long.class);
        if (recent == null) {
   
            recent = System.nanoTime();
            resync(recent);
            return recent - intervalNanos;
        }
        return recent;
    }

    private void resync(long now) {
   
        redisService.setwe(LeakyBucketKey.leakyBucket, this.name, now, LeakyBucketKey.leakyBucket.expireSeconds());
    }

功能性测试

测试漏桶功能,多线程同时请求通过漏桶,只有一个线程能通过。代码如下所示:

    @Test
    public void getLeakyLimiter() {
   
        LeakyLimiterFactory factory = new LeakyLimiterFactory();
        LeakyLimiterConfig config = new LeakyLimiterConfig("testLeakyLimiter", 1, redissonService.getRLock("testLeakylock"), redisService);
        final LeakyLimiter leakyLimiter = factory.getLeakyLimiter(config);
        final int N = 3;
        Runnable task = new Runnable() {
   
            @Override
            public void run() {
   
                if (leakyLimiter.acquire()) {
   
                    System.out.println(Thread.currentThread().getName() + " passed.");
                } else {
   
                    System.out.println(Thread.currentThread().getName() + " failed.");
                }
            }
        };
        Executor executor = Executors.newFixedThreadPool(N);
        for (int i = 0; i < N; i++) {
   
            executor.execute(task);
        }
        try {
   
            Thread.sleep(2 * 1000);
        } catch (Exception e) {
   
            e.printStackTrace();
        }
        Executor executor2 = Executors.newFixedThreadPool(N);
        for (int i = 0; i < N; i++) {
   
            executor2.execute(task);
        }
    }

 

滑动窗口


计数器限流是统计一段时间间隔内的请求数,如果达到了阈值,则拒绝后面的请求。滑动窗口在此基础上将时间间隔进行细分,让请求更平滑地执行。

如果计数器的时间间隔为 1s,限制请求数为 1000,考虑如下情况:在前一秒的最后 100ms 通过请求数 1000,下一秒的前 100ms 通过请求数 1000,实际上在 200ms 的时间内通过了 2000 个请求,远远超过了限流器的原始设计。

滑动窗口把 1s 的时间段分成更小的部分,例如 10 份,当时间到达后一秒的前 100ms 时,滑动窗口的范围是前一秒的后 900ms 和后一面的前 100ms,这时候窗口范围内已经达到了限制请求数,不会允许此时的 1000 个请求通过。无论何时,窗口范围内都只允许最大 1000 个请求。


实现

滑动窗口使用链表实现,链表的每一个节点是 Node 类的实例。Node 表示一小段时间间隔,类中有三个属性,分别代表“起始时间”、“终止时间”、“时间段内计数”。如果滑动窗口已经完全经过该时间段,可以把该段删除。

Node 节点如下所示:

public class Node {
   
    private long startTime;
    private long endTime;
    private long count;
    // getter and setter
    // ...
}

Window 是在缓存中传递的载体,包括以下属性:

    // 唯一标识
    private String name;
    // 滑动窗口
    private LinkedList<Node> slots;
    // 时间间隔
    
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值