1. 常见的限流算法

1.1. 控制最大并发数限流

以秒杀业务为例,秒杀的商品数量少,但进行秒杀的人很多。如秒杀的商品数量是10,但是却有100万人同时发起请求,最终能够抢到的人也就是前面几个人,后面的基本上都没有希望了,那么可以通过控制并发数来实现,比如并发数控制在10个,其他超过并发数的请求全部拒绝,提示:秒杀失败,请稍后重试。

1.2. 漏桶算法限流

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会则直接溢出(即请求被拒绝),可以看出漏桶算法能强行限制数据的传输速率(QPS)。

                     

日志限流题目java java如何限流_Semaphore

1.3. 令牌桶算法限流

令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求。

令牌桶算法示意图:

日志限流题目java java如何限流_并发编程_02

 

1.4. 应用场景

  • 令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。
  • 漏桶算法用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,调用速度不能超过他的限制,由于不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。

总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证别人的系统不被打垮,用漏桶算法。一些简单场景中,直接控制最大并发数即可。

2. 代码示例

2.1 控制最大并发数

使用Semaphore控制同时访问资源的线程个数,如共有3个秒杀的商品,则同时访问资源的个数是3。如果有人下单失败,则释放一个许可,将其返回给信号量,让商品继续参与秒杀。

static Semaphore semaphore = new Semaphore(3);
    static Random rand = new Random(47);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                boolean flag = false;
                try {
                    flag = semaphore.tryAcquire(100, TimeUnit.MICROSECONDS);
                    if (flag) {
                        boolean isOrderSucc = rand.nextBoolean();
                        if(!isOrderSucc){
                            System.out.println(Thread.currentThread() + "下单失败。。。。。");
                            semaphore.release();
                        }else{
                            System.out.println(Thread.currentThread() + "秒杀成功。。。。。");
                        }
                    } else {
                        System.out.println(Thread.currentThread() + ",秒杀失败,请稍微重试!");
                    }
                } catch (InterruptedException e) {
                }
            }).start();
        }
        TimeUnit.SECONDS.sleep(5);
    }

    程序运行结果如下:

       

日志限流题目java java如何限流_日志限流题目java_03

2.2. 漏桶算法限流

简单实现的代码示例如下:创建了一个容量为10,流水为60/分钟的漏桶,如果请求太多,会导致桶溢出,则请求直接不处理

import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

public class LeakBucketLimitTest {
    public static class LeakBucketLimit {
        AtomicInteger threadNum = new AtomicInteger(1);
        //容量
        private int capcity;
        //流速
        private int flowRate;
        //流速时间单位
        private TimeUnit flowRateUnit;
        private BlockingQueue<Request> queue;
        //漏桶流出的任务时间间隔(纳秒)
        private long flowRateNanosTime;

        public LeakBucketLimit(int capcity, int flowRate, TimeUnit flowRateUnit) {
            this.capcity = capcity;
            this.flowRate = flowRate;
            this.flowRateUnit = flowRateUnit;
            this.start();
        }

        //漏桶线程
        public void start() {
            this.queue = new ArrayBlockingQueue<Request>(capcity);
            //漏桶流出的任务时间间隔(纳秒)
            this.flowRateNanosTime = flowRateUnit.toNanos(1) / flowRate;
            Thread thread = new Thread(this::bucketWork);
            thread.setName("漏桶线程-" + threadNum.getAndIncrement());
            thread.start();
        }

        //漏桶线程开始工作
        public void bucketWork() {
            while (true) {
                Request req = this.queue.poll();
                if (Objects.nonNull(req)) {
                    //唤醒任务线程
                    LockSupport.unpark(req.thread);
                }
                //休眠flowRateNanosTime
                LockSupport.parkNanos(this.flowRateNanosTime);
            }
        }

        //返回一个漏桶
        public static LeakBucketLimit build(int capcity, int flowRate, TimeUnit flowRateUnit) {
            if (capcity < 0 || flowRate < 0) {
                throw new IllegalArgumentException("capcity、flowRate必须大于0!");
            }
            return new LeakBucketLimit(capcity, flowRate, flowRateUnit);
        }

        //当前线程加入漏桶,返回false,表示漏桶已满;true:表示加入漏桶,并按指定速率处理请求
        public boolean flowInfo(int num) {
            Thread thread = Thread.currentThread();
            Request req = new Request(thread);
            if (this.queue.offer(req)) {
                LockSupport.park();
                return true;
            }
            return false;
        }
    }

    //漏桶中存放的元素
    static class Request {
        private Thread thread;

        public Request(Thread thread) {
            this.thread = thread;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        LeakBucketLimit bucketLimit = LeakBucketLimit.build(10, 60, TimeUnit.MINUTES);
        for (int i = 0; i < 15; i++) {
            final int num = i;
            new Thread(() -> {
                boolean acquire = bucketLimit.flowInfo(num);
                //注意: 如果桶满,则下面的代码立即运行,否则按指定的速率执行
                System.out.println(Thread.currentThread().getName() + " " + acquire);
            }).start();
        }
        TimeUnit.SECONDS.sleep(8);
        System.out.println("finished");
    }
}

2.3. 令牌桶算法限流

令牌桶算法图例

      

日志限流题目java java如何限流_日志限流题目java_04

    a. 按特定的速率向令牌桶投放令牌

    b. 根据预设的匹配规则先对报文进行分类,不符合匹配规则的报文不需要经过令牌桶的处理,直接发送;

    c. 符合匹配规则的报文,则需要令牌桶进行处理。当桶中有足够的令牌则报文可以被继续发送下去,同时令牌桶中的令牌 量按报文的长度做相应的减少;

    d. 当令牌桶中的令牌不足时,报文将不能被发送,只有等到桶中生成了新的令牌,报文才可以发送。这就可以限制报文的流量只能是小于等于令牌生成的速度,达到限制流量的目的。

     示例代码如下:

import com.google.common.base.Preconditions;

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
 */
class TokenBucketTest {
    static class TokenBucket {

        // 默认桶大小个数 即最大瞬间流量是64M
        private static final int DEFAULT_BUCKET_SIZE = 1024 * 1024 * 64;

        // 一个桶的单位是1字节
        private int everyTokenSize = 1;

        // 瞬间最大流量
        private int maxFlowRate;

        // 平均流量
        private int avgFlowRate;

        // 使用队列缓存桶数量:最大的流量峰值就是 = everyTokenSize*DEFAULT_BUCKET_SIZE 64M = 1 * 1024 * 1024 * 64
        private ArrayBlockingQueue<Byte> tokenQueue = new ArrayBlockingQueue<Byte>(DEFAULT_BUCKET_SIZE);

        private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();

        private volatile boolean isStart = false;

        private ReentrantLock lock = new ReentrantLock(true);

        private static final byte A_CHAR = 'a';

        public TokenBucket() {
        }

        public TokenBucket(int maxFlowRate, int avgFlowRate) {
            this.maxFlowRate = maxFlowRate;
            this.avgFlowRate = avgFlowRate;
        }

        public TokenBucket(int everyTokenSize, int maxFlowRate, int avgFlowRate) {
            this.everyTokenSize = everyTokenSize;
            this.maxFlowRate = maxFlowRate;
            this.avgFlowRate = avgFlowRate;
        }

        public void addTokens(Integer tokenNum) {
            // 若是桶已经满了,就不再家如新的令牌
            for (int i = 0; i < tokenNum; i++) {
                tokenQueue.offer(Byte.valueOf(A_CHAR));
            }
        }

        public TokenBucket build() {
            start();
            return this;
        }

        /**
         * 获取足够的令牌个数
         *
         * @return
         */
        public boolean getTokens(byte[] dataSize) {
            Preconditions.checkNotNull(dataSize);
            Preconditions.checkArgument(isStart, "please invoke start method first !");

            int needTokenNum = dataSize.length / everyTokenSize + 1;// 传输内容大小对应的桶个数

            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                boolean result = needTokenNum <= tokenQueue.size(); // 是否存在足够的桶数量
                if (!result) {
                    return false;
                }

                int tokenCount = 0;
                for (int i = 0; i < needTokenNum; i++) {
                    Byte poll = tokenQueue.poll();
                    if (poll != null) {
                        tokenCount++;
                    }
                }

                return tokenCount == needTokenNum;
            } finally {
                lock.unlock();
            }
        }

        public void start() {
            // 初始化桶队列大小
            if (maxFlowRate != 0) {
                tokenQueue = new ArrayBlockingQueue<Byte>(maxFlowRate);
            }

            // 初始化令牌生产者
            TokenProducer tokenProducer = new TokenProducer(avgFlowRate, this);
            scheduledExecutorService.scheduleAtFixedRate(tokenProducer, 0, 1,
                    TimeUnit.SECONDS);
            isStart = true;

        }

        public void stop() {
            isStart = false;
            scheduledExecutorService.shutdown();
        }

        public boolean isStarted() {
            return isStart;
        }

        //令牌桶生产者,按指定速率在桶中放令牌
        class TokenProducer implements Runnable {

            private int avgFlowRate;
            private TokenBucket tokenBucket;

            public TokenProducer(int avgFlowRate, TokenBucket tokenBucket) {
                this.avgFlowRate = avgFlowRate;
                this.tokenBucket = tokenBucket;
            }

            @Override
            public void run() {
                tokenBucket.addTokens(avgFlowRate);
            }
        }

        public static TokenBucket newBuilder() {
            return new TokenBucket();
        }

        public TokenBucket everyTokenSize(int everyTokenSize) {
            this.everyTokenSize = everyTokenSize;
            return this;
        }

        public TokenBucket maxFlowRate(int maxFlowRate) {
            this.maxFlowRate = maxFlowRate;
            return this;
        }

        public TokenBucket avgFlowRate(int avgFlowRate) {
            this.avgFlowRate = avgFlowRate;
            return this;
        }

        private String stringCopy(String data, int copyNum) {
            StringBuilder sbuilder = new StringBuilder(data.length() * copyNum);
            for (int i = 0; i < copyNum; i++) {
                sbuilder.append(data);
            }

            return sbuilder.toString();
        }

    }

    public static void main(String[] args) throws IOException,
            InterruptedException {
        tokenTest();
    }

    private static void tokenTest() throws InterruptedException, IOException {
        TokenBucket tokenBucket = TokenBucket.newBuilder().avgFlowRate(512).maxFlowRate(1024).build();

        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("D:/ds_test")));
        String data = "xxxx";// 四个字节
        for (int i = 1; i <= 1000; i++) {
            Random random = new Random();
            int i1 = random.nextInt(100);
            boolean tokens = tokenBucket.getTokens(tokenBucket.stringCopy(data, i1).getBytes());
            TimeUnit.MILLISECONDS.sleep(100);
            if (tokens) {
                bufferedWriter.write("token pass --- index:" + i1);
                System.out.println("token pass --- index:" + i1);
            } else {
                bufferedWriter.write("token rejuect --- index" + i1);
                System.out.println("token rejuect --- index" + i1);
            }

            bufferedWriter.newLine();
            bufferedWriter.flush();
        }
        bufferedWriter.close();
    }
}

2.4. 使用工具类RateLimiter限流

Google开源工具包Guava提供了限流工具类RateLimiter,可以非常方便的控制系统每秒吞吐量,示例代码如下:从运行日志看,当QPS从5调整到10,阻塞睡眠的时间明显变短

public static void main(String[] args) throws InterruptedException {

     /**
     * permitsPerSecond为每秒生成的令牌
     *
     * 平衡稳定 
     * * 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
     * * 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
     * * 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
     * 平衡预热
     * 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
     * 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
     * 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
     * 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
     * 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
     */
        RateLimiter rateLimiter = RateLimiter.create(5,2L,TimeUnit.SECONDS);//设置QPS为5
        for (int i = 0; i < 10; i++) {
            double sleep = rateLimiter.acquire();
            System.out.println(String.format("请求%s被处理,sleep了%s秒",i,sleep));
        }
        System.out.println("----------");
        //可以随时调整速率,我们将qps调整为10
        rateLimiter.setRate(10);
        for (int i = 0; i < 10; i++) {
            double sleep = rateLimiter.acquire();
            System.out.println(String.format("请求%s被处理,sleep了%s秒",i,sleep));
        }
    }

    

日志限流题目java java如何限流_限流算法_05