Java 文件下載限流算法

也可以看我的CSDN的博客
https://blog.csdn.net/u013332124/article/details/88043761

在做文件下載功能時,為了避免下載功能將服務器的帶寬打滿,從而影響服務器的其他服務。我們可以設計一個限流器來限制下載的速率,從而限制下載服務所占用的帶寬。

一、算法思路

定義一個數據塊chunk(單位 bytes)以及允許的最大速率 maxRate(單位 KB/s)。通過maxRate我們可以算出,在maxRate的速率下,通過一個數據塊大小的字節流所需要的時間 timeCostPerChunk

之后,在讀取/寫入字節時,我們維護已經讀取/寫入的字節量 bytesWillBeSentOrReceive

bytesWillBeSentOrReceive達到一個數據塊的大小時,檢查期間消耗的時間(nowNanoTime-lastPieceSentOrReceiveTick)

如果期間消耗的時間小于timeCostPerChunk的值,說明當前的速率已經超過了 maxRate的速率,這時候就需要休眠一會來限制流量

如果速率沒超過或者休眠完后,將 bytesWillBeSentOrReceive=bytesWillBeSentOrReceive-chunkSize

之后在讀取/寫入數據時繼續檢查。

下面該算法的Java代碼實現:

    public synchronized void limitNextBytes(int len) {
        //累計bytesWillBeSentOrReceive
        this.bytesWillBeSentOrReceive += len;
        //如果積累的bytesWillBeSentOrReceive達到一個chunk的大小,就進入語句塊操作
        while (this.bytesWillBeSentOrReceive > CHUNK_LENGTH) {
            long nowTick = System.nanoTime();
            //計算積累數據期間消耗的時間
            long passTime = nowTick - this.lastPieceSentOrReceiveTick;
            //timeCostPerChunk表示單個塊最多需要多少納秒
            //如果missedTime大于0,說明此時流量進出的速率已經超過maxRate了,需要休眠來限制流量
            long missedTime = this.timeCostPerChunk - passTime;
            if (missedTime > 0) {
                try {
                    Thread.sleep(missedTime / 1000000, (int) (missedTime % 1000000));
                } catch (InterruptedException e) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
            this.bytesWillBeSentOrReceive -= CHUNK_LENGTH;
            //重置最后一次檢查時間
            this.lastPieceSentOrReceiveTick = nowTick + (missedTime > 0 ? missedTime : 0);
        }
    }

二、限流的完整java代碼實現

限流器的實現

public class BandwidthLimiter {

    private static final Logger LOGGER = LoggerFactory.getLogger(BandwidthLimiter.class);
    //KB代表的字節數
    private static final Long KB = 1024L;
    //一個chunk的大小,單位byte。設置一個塊的大小為1M
    private static final Long CHUNK_LENGTH = 1024 * 1024L;

    //已經發送/讀取的字節數
    private int bytesWillBeSentOrReceive = 0;
    //上一次接收到字節流的時間戳——單位納秒
    private long lastPieceSentOrReceiveTick = System.nanoTime();
    //允許的最大速率,默認為 1024KB/s
    private int maxRate = 1024;
    //在maxRate的速率下,通過chunk大小的字節流要多少時間(納秒)
    private long timeCostPerChunk = (1000000000L * CHUNK_LENGTH) / (this.maxRate * KB);

    public BandwidthLimiter(int maxRate) {
        this.setMaxRate(maxRate);
    }

    //動態調整最大速率
    public void setMaxRate(int maxRate) {
        if (maxRate < 0) {
            throw new IllegalArgumentException("maxRate can not less than 0");
        }
        this.maxRate = maxRate;
        if (maxRate == 0) {
            this.timeCostPerChunk = 0;
        } else {
            this.timeCostPerChunk = (1000000000L * CHUNK_LENGTH) / (this.maxRate * KB);
        }
    }

    public synchronized void limitNextBytes() {
        this.limitNextBytes(1);
    }

    public synchronized void limitNextBytes(int len) {
        this.bytesWillBeSentOrReceive += len;

        while (this.bytesWillBeSentOrReceive > CHUNK_LENGTH) {
            long nowTick = System.nanoTime();
            long passTime = nowTick - this.lastPieceSentOrReceiveTick;
            long missedTime = this.timeCostPerChunk - passTime;
            if (missedTime > 0) {
                try {
                    Thread.sleep(missedTime / 1000000, (int) (missedTime % 1000000));
                } catch (InterruptedException e) {
                    LOGGER.error(e.getMessage(), e);
                }
            }
            this.bytesWillBeSentOrReceive -= CHUNK_LENGTH;
            this.lastPieceSentOrReceiveTick = nowTick + (missedTime > 0 ? missedTime : 0);
        }
    }
}

有了限流器后,現在我們要對下載功能做限流。因為java的io流的設計是裝飾器模式,因此我們可以方便的封裝一個我們自己的InputStream

public class LimitInputStream extends InputStream {

    private InputStream inputStream;
    private BandwidthLimiter bandwidthLimiter;

    public LimitInputStream(InputStream inputStream, BandwidthLimiter bandwidthLimiter) {
        this.inputStream = inputStream;
        this.bandwidthLimiter = bandwidthLimiter;
    }

    @Override
    public int read() throws IOException {
        if (bandwidthLimiter != null) {
            bandwidthLimiter.limitNextBytes();
        }
        return inputStream.read();
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (bandwidthLimiter != null) {
            bandwidthLimiter.limitNextBytes(len);
        }
        return inputStream.read(b, off, len);
    }

    @Override
    public int read(byte[] b) throws IOException {
        if (bandwidthLimiter != null && b.length > 0) {
            bandwidthLimiter.limitNextBytes(b.length);
        }
        return inputStream.read(b);
    }
}

后面我們使用這個LimitInputStream來讀取文件,每次讀取一塊數據,限流器都會檢查當前的速率是否超過指定的最大速率。這樣就能間接的達到限制下載速率的目的了。

附上SpringMVC的一個下載限流的demo:

    @GetMapping("/limit")
    public void limitDownloadFile(String file, HttpServletResponse response) throws IOException {
        LOGGER.info("download file");
        if (file == null) {
            file = "/tmp/test.txt";
        }
        File downloadFile = new File(file);
        FileInputStream fileInputStream = new FileInputStream(downloadFile);

        response.setContentType("application/x-msdownload;");
        response.setHeader("Content-disposition", "attachment; filename=" + new String(downloadFile.getName()
                .getBytes("utf-8"), "ISO8859-1"));
        response.setHeader("Content-Length", String.valueOf(downloadFile.length()));
        ServletOutputStream outputStream = null;
        try {
            LimitInputStream limitInputStream = new LimitInputStream(fileInputStream, new BandwidthLimiter(1024));

            long beginTime = System.currentTimeMillis();
            outputStream = response.getOutputStream();
            byte[] bytes = new byte[1024];
            int read = limitInputStream.read(bytes, 0, 1024);
            while (read != -1) {
                outputStream.write(bytes);
                read = limitInputStream.read(bytes, 0, 1024);
            }
            LOGGER.info("download use {} ms", System.currentTimeMillis() - beginTime);
        } finally {
            fileInputStream.close();
            if (outputStream != null) {
                outputStream.close();
            }
            LOGGER.info("download success!");
        }
    }

三、注意點

使用這個算法要注意一個問題,就是chunk的塊大小不能設置的太小,即CHUNK_LENGTH不能設置的太小。否則容易造成明明maxRate設置的很大,但是實際下載速率卻很小的問題

假設CHUNK_LENGTH就設置為1024 bytes,每次讀取的塊大小也是1024 bytes,maxRate 為 64M/s。那么我們可以計算出timeCostPerChunk約等于15258納秒。

再如果真正的速率是100M/s,也就是每秒差不多會調用limitNextBytes方法100000次,由于每次讀取消耗的時間極短,因此每次進入該方法都要sleep 15258納秒之后再讀取下一個塊的數據。如果沒有算上線程調度的時間,就算1秒內休眠100000次也完全沒什么問題。但是線程的休眠和喚醒都需要內核來進行,線程上下文切換的時間應該遠大于15258納秒,這時候頻繁的休眠就會導致線程暫停運行的時間和我們預期的不符。由于休眠時間過長,最終導致實際的下載速率大大的低于maxRate。

因此,我們需要調大CHUNK_LENGTH,盡量讓timeCostPerChunk的值遠大于線程調度的時間,減少線程調度對限流造成的影響。

四、具體demo的github地址

https://github.com/kongtrio/download-limit

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉載來自開濤的聊聊高并發系統限流特技-2 上一篇《聊聊高并發系統限流特技-1》講了限流算法、應用級限流、分布式限流...
    meng_philip123閱讀 4,143評論 0 10
  • 摘要:上一篇《聊聊高并發系統限流特技-1》講了限流算法、應用級限流、分布式限流;本篇將介紹接入層限流實現。 接入層...
    落羽成霜丶閱讀 948評論 0 5
  • 摘要:GFS在設計上有很多值得學習的地方,最近重讀了一下GFS的設計論文,試圖從架構設計的角度對GFS進行剖析,希...
    架構禪話閱讀 4,491評論 0 2
  • 感官遲鈍,以為香港的熱情會連綿不絕。 猝不及防,冷風掃落葉。 不覺此季值得傷懷,反而甚喜涼風拂面。 爬上眉眼,撫過...
    東流水酌月閱讀 249評論 6 9
  • 當你有一肚子的心里話,想要找個人傾訴的時候 , 卻突然發現 、 翻遍了通訊錄,盡然找不到一個可以任你隨時打擾的人 ...
    玉墨老師閱讀 144評論 0 0