杂-文件按行读取

背景

  • 实际开发过程中,需要去实现一个API接口调用审计日志的存储 & 读取汇总功能。很容易想到这类数据可以用文件存储,但是涉及到监控,需要将存储的数据读取出来汇总给前端图表。这里我们记录下一些工具类。

写文件并返回最后一行的行号

 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;
}   

按行读取&处理(文件全部内容)

  • 这里是使用 RandomAccessFile.readLine方法,来一行行的读取文件内容,并写入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;
}
    
  • 如果你的处理不需要整个文件内容(例如只需要读取每行做一定的计算。那么可以加个handler,直接处理每行,省去数据缓存的消耗).
// 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) ->{
    // 这里写你每行的数据处理逻辑
} )

  • 如果使用上面的linescan,如果我的日志文件有100万行,那么当我想要读取第90万行是,势必会先将前(90万 - 1)行都scan一遍,而后才可以找到第90万行开始处理,性能肯定会大打折扣。脑海中浮现的第一个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);
  • 找到了飞的方法,还得想办法获取“飞的位置”,目前的条件下,我们的目的是飞到指定行号,只知道行号怎么准确的计算出行号对应字节长度偏移量呢(就是从文件起始位置到你要读取的那一行共有多少个byte)。唯一的办法就是让每一行的长度都相同为rowLength,这样我们就可以通过rowLength * (startRowNum - 1)拿到要读取行的开始offset了。

  • 回到一开始的背景,我们是接口请求的审计日志。那么怎么做到定长。

//  日志中一条请求的详细信息存储如下
<SESSION_ID>|<TIMESTAMP>|<COST>|<STATUS>|<IP>

// SESSION_ID是定长的,不用考虑
// TIMESTAMP 预估这十几年都会是这个长度,不用考虑
// COST:  接口请求耗时(MS),这个长度不一定需要做一下处理,假设API接口请求耗时最大为 8位数字,少于8位的我们来补0转换为字符串。
public static String getFormatCostStr(Long cost) {
    return String.format("%08d", this.cost);
}

// STATUS: 接口请求状态,成功/不成功。这里我们用 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);
}

// 实际的调用效果
Long lineRange = FileUtils.lineLength(auditFile) + 2;
FileUtils.lineScan(auditFile, rowIndex, lineRange, (line, rowContext) -> {
    // 你的逻辑!!!
});

  • 分享下我的监控日志的实现策略。每天的请求审计日志存储在两个文件
  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分钟,每行存放当前分钟发生请求日志在log文件中的行号,方便检索)
// 在当天的第973分钟发生了两次请求,两次请求的日志分别在log文件中的第1,2行,index文件中第972行内容如下,
1|2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343