雜-文件按行讀取

背景

  • 實際開發(fā)過程中,需要去實現(xiàn)一個API接口調(diào)用審計日志的存儲 & 讀取匯總功能。很容易想到這類數(shù)據(jù)可以用文件存儲,但是涉及到監(jiān)控,需要將存儲的數(shù)據(jù)讀取出來匯總給前端圖表。這里我們記錄下一些工具類。

寫文件并返回最后一行的行號

 public static Long write(File file, String content, boolean append) {
    try(FileWriter fileWriter = new FileWriter(file.getPath(), append)) {
        fileWriter.write(content);
        fileWriter.flush();
        long lineCount;
        try (Stream<String> stream = Files.lines(Paths.get(file.getPath()))) {
            lineCount = stream.count();
        }
        return lineCount;
    } catch (IOException exception) {
        log.error("Sink log error! msg: {}", exception.getMessage());
    }
    return -1L;
}   

按行讀取&處理(文件全部內(nèi)容)

  • 這里是使用 RandomAccessFile.readLine方法,來一行行的讀取文件內(nèi)容,并寫入list中緩存下來。
public static List<String> lineScan(File file) {
    List<String> res = new ArrayList<>();
    try (RandomAccessFile fileR = new RandomAccessFile(file, "r")) {
        String str = null;
        while ((str = fileR.readLine()) != null) {
            res.add(str);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return res;
}
    
  • 如果你的處理不需要整個文件內(nèi)容(例如只需要讀取每行做一定的計算。那么可以加個handler,直接處理每行,省去數(shù)據(jù)緩存的消耗).
// FileUtils.java
public static void lineScan(File file, RowHandler handler) {
    try (RandomAccessFile fileR = new RandomAccessFile(file, "r")) {
        String str = null;
        while ((str = fileR.readLine()) != null) {
            handler.handle(str);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return res;
}
// RowHandler.java
@FunctionalInterface
public interface FileLineFilter {

    void filter(String line);
}
// eg.
FileUtils.linScan(file, (lineContent) ->{
    // 這里寫你每行的數(shù)據(jù)處理邏輯
} )

  • 如果使用上面的linescan,如果我的日志文件有100萬行,那么當我想要讀取第90萬行是,勢必會先將前(90萬 - 1)行都scan一遍,而后才可以找到第90萬行開始處理,性能肯定會大打折扣。腦海中浮現(xiàn)的第一個idea就是“如果能夠飛到那一行就好了”。
  • RandomAccessFile 提供一個seek方法,可以使游標移動到你提供的offset處.
// * Sets the file-pointer offset, measured from the beginning of this
// * file, at which the next read or write occurs.  The offset may be
// * set beyond the end of the file. Setting the offset beyond the end
// * of the file does not change the file length.  The file length will
// * change only by writing after the offset has been set beyond the end
// * of the file.
RandomAccessFile.seek(long pos);
  • 找到了飛的方法,還得想辦法獲取“飛的位置”,目前的條件下,我們的目的是飛到指定行號,只知道行號怎么準確的計算出行號對應字節(jié)長度偏移量呢(就是從文件起始位置到你要讀取的那一行共有多少個byte)。唯一的辦法就是讓每一行的長度都相同為rowLength,這樣我們就可以通過rowLength * (startRowNum - 1)拿到要讀取行的開始offset了。

  • 回到一開始的背景,我們是接口請求的審計日志。那么怎么做到定長。

//  日志中一條請求的詳細信息存儲如下
<SESSION_ID>|<TIMESTAMP>|<COST>|<STATUS>|<IP>

// SESSION_ID是定長的,不用考慮
// TIMESTAMP 預估這十幾年都會是這個長度,不用考慮
// COST:  接口請求耗時(MS),這個長度不一定需要做一下處理,假設API接口請求耗時最大為 8位數(shù)字,少于8位的我們來補0轉(zhuǎn)換為字符串。
public static String getFormatCostStr(Long cost) {
    return String.format("%08d", this.cost);
}

// STATUS: 接口請求狀態(tài),成功/不成功。這里我們用 0 1表示 不成功/成功。完美定長
// IP: 地址,這里還是采取補0的策略(一個簡陋版的補全。。)
public static String ipCompletion(String ip) {
    String[] arr = ip.split("\\.");
    if (arr.length == 4) {
        for (int i = 0; i < arr.length; i++) {
            if (arr[i].length() == 1) {
                arr[i] = "00" + arr[i];
            } else if (arr[i].length() == 2) {
                arr[i] = "0" + arr[i];
            }
        }
        return String.join(".", arr);
    } else {
        return "000.000.000.000";
    }
}
  • 既然每一行飛長度確定了,那么按行讀取也就變得非常容易了。
// 先來個讀取文件首行獲取一行長度的方法

// 這個長度未算上 換行符(\r\n)
public static long lineLength(File file) {
    long length = -1L;
    String str = null;
    try(RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r")) {
        str = randomAccessFile.readLine();
        if (StringUtils.isNotEmpty(str)) {
            length = str.getBytes().length;
        }
    }catch (Exception e) {
        log.error("read file first line length failed ! msg: {}", e.getMessage());
        log.error("", e);
    }
    return length;
}

// 而后在來個按行讀取的方法
public static void lineScan(File file, int start, int end, Long rowLength, FileLineHandler handler) {
    try(RandomAccessFile fileR = new RandomAccessFile(file,"r")){
        fileR.seek((start-1) * rowLength); // fly!!!!!
        long line = start;
        String str = null;
        while ((str = fileR.readLine())!= null) {
            line++;
            if (line > end) {
                break;
            }
            handler.handle(line, str);
        }
    } catch (IOException e) {
        log.error("read file line failed ! msg: {}", e.getMessage());
        log.error("", e);
    }
}

// FileLineHandler
@FunctionalInterface
public interface FileLineHandler {

    void handler(String line);
}

// 實際的調(diào)用效果
Long lineRange = FileUtils.lineLength(auditFile) + 2;
FileUtils.lineScan(auditFile, rowIndex, lineRange, (line, rowContext) -> {
    // 你的邏輯?。?!
});

  • 分享下我的監(jiān)控日志的實現(xiàn)策略。每天的請求審計日志存儲在兩個文件
  1. <DATE>.log(按行存儲每次的請求詳情。)
asdfasdfasdfasdf|1603643538000|00000012|1|172.016.002.015
asdfasdfasdfasdf|1603643538000|00000012|1|172.016.002.015
  1. <DATE>.log.index(分為1440行,即每天共有1440分鐘,每行存放當前分鐘發(fā)生請求日志在log文件中的行號,方便檢索)
// 在當天的第973分鐘發(fā)生了兩次請求,兩次請求的日志分別在log文件中的第1,2行,index文件中第972行內(nèi)容如下,
1|2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容