java实现日志文件自定义断点续传

30岁了,一直做游戏开发,最近想去做电商之类,却发现好难。一切都限于没有电商经验,挺对自己抱不平的,游戏开发早些年的技术能力是优于Web开发的,但这几年游戏行业的技术却没什么大的创新,不过,业务驱动技术的增长嘛,业务没变,那技术就自然这样了。
但是,Web技术也没啥,最近学了一大圈,发现都离不开游戏里面早几百年都玩过的概念。
负载均衡->游戏多服;人数一达限制,由玩家选择一个新的服。
消息队列->游戏内部事件系统;模块解耦,防止阻止玩家线程的解决办法嘛;
Redis缓存之类->内存缓存;玩家数据缓存在内存,降低数据库IO;
而这些系统独立出去,又离不开网络IO,文件IO的各种封装,这有啥啊,我们做游戏的早就对其各种把玩了。
哎,无可奈何,心之叹息;

场景

由于要对接某数据平台,而数据平台提供的上报数据的方式中,也只有Filebeat加Logstash之类,这两种工具在给运维增加部署复杂度的同时,也难以自定义其对错误的处理方式。
并且,Filebeat此类,当初始启动的时候,会瞬间读取大量的日志数据,同时传给Logstash解析,这种方式会在某一瞬间增加服务器的压力。而我们的服务器场景,是对Cpu以及网络及其敏感的,如果部署到正式服务器,可能会对线上情况造成不稳定性。
另一方面,使用第三方工具难以对流程控制,比如,某些数据格式解析错误,导致数据平台返回异常,这种情况,难以直接控制停止解析,或者做其他兼容。
综合上面提到的点,自己实现一套反而来的比较直接与简单。

工程路径

[github地址](https://github.com/yourwafer/logbeat) https://github.com/yourwafer/logbeat

日志解析与断点重传 LogTask

File file = new File(path);
        if (!file.exists()) {
            // 找不到文件,则认定今天日志已经消费完成
            logPosition.updateTime(lastExecute);
            logPosition.setPosition(-1);
            save.accept(logPosition);

            return path;
        }

        try {
            // 使用只读的方式打开文件
            this.randomAccessFile = new RandomAccessFile(path, "r");
            this.filePath = path;
        } catch (FileNotFoundException e) {
            // 文件暂时不存在,添加一个-1的标记,用于标识由于日志不存在而没有继续解析
            log.debug("日志[{}]不存在", path);
            logPosition.updateTime(lastExecute);
            logPosition.setPosition(-1);
            save.accept(logPosition);

            return path;
        }
        // 更新为是第一次读取文件,标记日志读取位置为0
        LocalDate pre = logPosition.getLastExecute();
        if (!pre.equals(lastExecute)) {
            logPosition.updateTime(lastExecute);
        }
        if (logPosition.getPosition() < 0) {
            logPosition.setPosition(0);
        }

开始解析处理

// 拿到日志上次解析的时间
LocalDate lastExecute = logPosition.getLastExecute();
        LocalDate now = LocalDate.now();
        // 如果是前一天
        for (; !lastExecute.isAfter(now); lastExecute = lastExecute.plusDays(1)) {
            // 最外层控制当前任务运行状态
            if (!running) {
                log.info("任务终止[" + this + "]");
                return;
            }
            // 初始化日志路径以及文件句柄
            String path = initAndClosePreFile(lastExecute);
            if (path != null) {
                log.info("日志文件不存在,忽视[{}]", path);
                continue;
            }
            // 拿到上次解析的文件位置
            long position = logPosition.getPosition();
            if (position < 0) {
                log.error("异常逻辑代码,文件[{}][{}]", this.filePath, position);
                break;
            }
            int read = -1;
            try {
                // 重置文件解析位置,这里是核心
                randomAccessFile.seek(position);
                log.info("重置位置[{}],开始解析处理文件[{}],", filePath, position);
            } catch (IOException e) {
                log.error("文件位置异常[{}]", position, e);
                break;
            }
            // 这里可以理解为网络的粘包和拆包,由于一次性读取的数据可能不是一个完整的数据行,此时需要将数据缓存起来
            //与下一次读取的数据组合起来,构成一个完整的数据行
            ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE * 2);
            do {
                if (!running) {
                    log.info("任务终止[" + this + "]");
                    return;
                }
                // 初始化buffer,这一行其实逻辑上是不需要的,但是为了断点方便,看内存数据,可以去掉的
                Arrays.fill(buffer, (byte) 0);
                long curFilePosition = 0;
                long startTime = System.nanoTime();
                try {
                    // 保留文件句柄读取前的位置,并读取数据
                    curFilePosition = randomAccessFile.getFilePointer();
                    read = randomAccessFile.read(buffer);
                } catch (IOException e) {
                    log.error("读取日志数据异常[{}]", filePath, e);
                }
                ReportUtils.readBytes(read, (System.nanoTime() - startTime));
                if (read == -1) {
                    break;
                }
                int start = 0;
                int cur;

                long newPosition = logPosition.getPosition();
                List lines = new ArrayList<>();
                for (int i = 0; i < read; ++i) {
                    byte b = buffer[i];
                    // 判断是否为换行符,如果是,那么代表读取到了一行
                    // \n\r
                    if (b != N && b != R) {
                        continue;
                    }
                    cur = i;

                    int size = cur - start;
                    if (byteBuffer.position() > 0) {
                        size += byteBuffer.position();
                    }
                    if (size == 0) {
                        // 排除当前字节(因为是换行符)
                        start = i + 1;
                        continue;
                    }
                    String line;
                    if (byteBuffer.position() > 0) {
                        // 判断是否有上一个包遗留的字节数据,如果有,跟此次数据合并组合起来
                        byteBuffer.put(buffer, start, (cur - start));
                        byteBuffer.flip();
                        byte[] bytes = new byte[byteBuffer.remaining()];
                        byteBuffer.get(bytes);
                        line = new String(bytes, StandardCharsets.UTF_8);
                        byteBuffer.clear();
                    } else {
                        line = new String(buffer, start, (cur - start), StandardCharsets.UTF_8);
                    }

                    start = i + 1;
                    // 解析到一行
                    lines.add(line);

                    int newPos = start;
                    if ((i + 1) < read) {
                        // 忽视下一个空格,windows下一般\r\n都是组合用的
                        if (buffer[i + 1] == N || buffer[i + 1] == R) {
                            newPos += 1;
                        }
                    }
                    // 记录新的位置
                    newPosition = curFilePosition + newPos;

                    if (!running) {
                        log.info("任务终止[" + this + "]");
                        return;
                    }
                }
                // 交给消费者消费
                lineConsumer.accept(lines);

                logPosition.setPosition(newPosition);
                logPosition.addRow(lines.size());
                log.trace("变更文件位置[{}][{}]", logPosition.getPosition(), filePath);
                save.accept(logPosition);

                if (start < read) {
                    byteBuffer.put(buffer, start, read - start);
                }
            } while (read > 0);
            if (byteBuffer.position() > 0 && !lastExecute.equals(now)) {
                // 如果当前文件日期不是今天,那么代表文件已经读取到最后,此时数据又没有换行符,说明日志记录那边的问题,但是此时,依然要把最后的数据做解析处理
                byteBuffer.flip();
                int remaining = byteBuffer.remaining();
                byte[] remainBytes = new byte[remaining];
                byteBuffer.get(remainBytes);
                String line = new String(remainBytes, StandardCharsets.UTF_8);
                log.info("[{}]日志文件[{}]最后一行[{}]没有换行符", filePath, remaining, line);
                lineConsumer.accept(Collections.singletonList(line));
                logPosition.addRow(1);
                save.accept(logPosition);
            }
        }

数据消费者

使用配置的方式确定激活哪种消费方式
暂时实现的方式有,控制台,文件,按大小拆分的文件,http

@Configuration(proxyBeanMethods = false)
public class PushConfiguration {

    public final static String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "xa.config.pushType.console", havingValue = "true")
    @Order(Ordered.HIGHEST_PRECEDENCE)
    ConsoleEventPush console() {
        return new ConsoleEventPush();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "xa.config.pushType.fileDebug", havingValue = "true")
    @Order(Ordered.HIGHEST_PRECEDENCE)
    FileDebugEventPush fileDebug() {
        return new FileDebugEventPush();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "xa.config.pushType.file", havingValue = "true")
    @Order(Ordered.HIGHEST_PRECEDENCE)
    FileEventPush file() {
        return new FileEventPush();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnProperty(value = "xa.config.pushType.http", havingValue = "true")
    @Order(Ordered.HIGHEST_PRECEDENCE)
    HttpEventPush http() {
        return new HttpEventPush();
    }

    @Bean
    @ConditionalOnMissingBean(EventPush.class)
    @Order
    ConsoleEventPush consoleDefault() {
        return new ConsoleEventPush();
    }
}

你可能感兴趣的:(java实现日志文件自定义断点续传)