如何在Java中读取超过内存大小的文件

打印 上一主题 下一主题

主题 899|帖子 899|积分 2697

读取文件内容,然后进行处置处罚,在Java中我们通常利用 Files 类中的方法,将可以文件内容加载到内存,并流顺利地进行处置处罚。但是,在一些场景下,我们需要处置处罚的文件可能比我们呆板所拥有的内存要大。此时,我们则需要采用另一种策略:部门读取它,并具有其他结构来仅编译所需的数据。
接下来,我们就来说说这一场景:当遇到大文件,无法一次载入内存时间要如何处置处罚。
模仿场景

假设,当前我们需要开辟一个程序来分析来自服务器的日志文件,并生成一份报告,列出前 10 个最常用的应用程序。
每天,都会生成一个新的日志文件,其中包罗时间戳、主机信息、持续时间、服务调用等信息,以及可能与我们的特定方案无关的其他数据。
  1. 2024-02-25T00:00:00.000+GMT host7 492 products 0.0.3 PUT 73.182.150.152 eff0fac5-b997-40a3-87d8-02ff2f397b44
  2. 2024-02-25T00:00:00.016+GMT host6 123 logout 2.0.3 GET 34.235.76.94 8b97acae-dd36-4e83-b423-12905a4ab38d
  3. 2024-02-25T00:00:00.033+GMT host6 50 payments/:id 0.4.6 PUT 148.241.146.59 ac3c9064-4782-46d9-a0b6-69e4d55a5b38
  4. 2024-02-25T00:00:00.050+GMT host2 547 orders 1.5.0 PUT 6.232.116.248 2285a81e-c511-41b9-b0ea-a475a0a45805
  5. 2024-02-25T00:00:00.067+GMT host4 400 suggestions 0.8.6 DELETE 149.138.227.154 8031b639-700e-4a7c-b257-fcbed0d029ce
  6. 2024-02-25T00:00:00.084+GMT host2 644 login 6.90 GET 208.158.145.204 3906a28c-56e4-4e5f-b548-591eab737aa7
  7. 2024-02-25T00:00:00.101+GMT host5 339 suggestions 0.8.9 PUT 173.109.21.97 c7dfec8a-5ca8-4d0d-b903-aaf65629fdd0
  8. 2024-02-25T00:00:00.118+GMT host9 87 products 2.6.3 POST 220.252.90.140 e5ceef67-2f0f-4c2d-a6d2-c698598aaef2
  9. 2024-02-25T00:00:00.134+GMT host0 845 products 9.4.6 GET 136.79.178.188 f28578c1-c37c-47a3-a473-4e65371e0245
  10. 2024-02-25T00:00:00.151+GMT host4 675 login 0.89 DELETE 32.159.65.239 d27ff353-e501-43e6-bdce-680d79a07c36
复制代码
我们的代码将收到日志文件列表,我们的目的是编制一份报告,列出最常用的 10 个服务。但是,要包罗在报告中,服务必须在提供的每个日志文件中至少有一个条目。简而言之,一项服务必须每天使用才有资格包罗在报告中。
基础实现

办理这个题目的最初方法是考虑业务需求并创建以下代码:
  1. public void processFiles(final List<File> fileList) {
  2.   final Map<LocalDate, List<LogLine>> fileContent = getFileContent(fileList);
  3.   final List<String> serviceList = getServiceList(fileContent);
  4.   final List<Statistics> statisticsList = getStatistics(fileContent, serviceList);
  5.   final List<Statistics> topCalls = getTop10(statisticsList);
  6.   print(topCalls);
  7. }
复制代码
该方法接收文件列表作为参数,核心流程如下:

  • 创建一个包罗每个文件条目的映射,其中Key是 LocalDate,Value是文件行列表。
  • 使用所有文件中的唯一服务名称创建字符串列表。
  • 生成所有服务的统计信息列表,将文件中的数据构造到结构化地图中。
  • 筛选统计信息,获取排名前 10 的服务调用。
  • 打印结果。
可以注意到,这种方法将太多数据加载到内存中,不可制止地会导致 OutOfMemoryError
改进实现

就如文章开头说的,我们需要采用另一种策略:逐行处置处罚文件的模式。
  1. private void processFiles(final List<File> fileList) {
  2.   final Map<String, Counter> compiledMap = new HashMap<>();
  3.   for (int i = 0; i < fileList.size(); i++) {
  4.     processFile(fileList, compiledMap, i);
  5.   }
  6.   final List<Counter> topCalls =
  7.       compiledMap.values().stream()
  8.           .filter(Counter::allDaysSet)
  9.           .sorted(Comparator.comparing(Counter::getNumberOfCalls).reversed())
  10.           .limit(10)
  11.           .toList();
  12.   print(topCalls);
  13. }
复制代码

  • 首先,它声明一个Map(compiledMap),其中一个String作为键,代表服务名称,以及一个Counter对象(稍后解释),它将存储统计信息。
  • 接下来,它逐一处置处罚这些文件并相应地更新compileMap。
  • 然后,它利用流功能来: 仅过滤具有全天数据的计数器;按调用次数排序;最后,检索前 10 名。
在看整个处置处罚的核心processFile方法之前,我们先来分析一下Counter类,它在这个过程中也起到了至关重要的作用:
  1. public class Counter {
  2.   @Getter private String serviceName;
  3.   @Getter private long numberOfCalls;
  4.   private final BitSet daysWithCalls;
  5.   public Counter(final String serviceName, final int numberOfDays) {
  6.     this.serviceName = serviceName;
  7.     this.numberOfCalls = 0L;
  8.     daysWithCalls = new BitSet(numberOfDays);
  9.   }
  10.   public void add() {
  11.     numberOfCalls++;
  12.   }
  13.   public void setDay(final int dayNumber) {
  14.     daysWithCalls.set(dayNumber);
  15.   }
  16.   public boolean allDaysSet() {
  17.     return daysWithCalls.stream()
  18.         .mapToObj(index -> daysWithCalls.get(index))
  19.         .reduce(Boolean.TRUE, Boolean::logicalAnd);
  20.   }
  21. }
复制代码

  • 它包罗三个属性:serviceName、numberOfCalls 和 daysWithCalls
  • numberOfCalls 属性通过 add 方法递增,该方法为 serviceName 的每个处置处罚行调用。
  • daysWithCalls 属性是一个 Java BitSet,一种用于存储布尔属性的内存高效结构。它使用要处置处罚的天数进行初始化,每个位代表一天,初始化为 false。
  • setDay 方法将 BitSet 中与给定日期位置相对应的位设置为 true。
allDaysSet 方法负责检查 BitSet 中的所有日期是否都设置为 true。它通过将 BitSet 转换为布尔流,然后使用逻辑 AND 运算符减少它来实现此目的。
  1. private void processFile(final List<File> fileList,
  2.                          final Map<String, Counter> compiledMap,
  3.                          final int dayNumber) {
  4.   try (Stream<String> lineStream = Files.lines(fileList.get(dayNumber).toPath())) {
  5.     lineStream
  6.         .map(this::toLogLine)
  7.         .forEach(
  8.             logLine -> {
  9.               Counter counter = compiledMap.get(logLine.serviceName());
  10.               if (counter == null) {
  11.                 counter = new Counter(logLine.serviceName(), fileList.size());
  12.                 compiledMap.put(logLine.serviceName(), counter);
  13.               }
  14.               counter.add();
  15.               counter.setDay(dayNumber);
  16.             });
  17.   } catch (final IOException e) {
  18.     throw new RuntimeException(e);
  19.   }
  20. }
复制代码

  • 该过程使用Files类的lines方法逐行读取文件,并将其转换为流。这里的关键特性是lines方法是惰性的,这意味着它不会立即读取整个文件;相反,它会在流被消耗时读取文件。
  • toLogLine 方法将每个字符串文件行转换为具有用于访问日志行信息的属性的对象。
  • 处置处罚文件行的重要过程比预期的要简单。它从与serviceName关联的compileMap中检索(或创建)Counter,然后调用Counter的add和setDay方法。
正如我们所看到的,在 Java 中处置处罚大文件而不将整个文件加载到内存中并不是什么复杂的事情。 Files类提供了逐行处置处罚文件的方法,我们还可以在文件处置处罚过程中利用哈希来存储数据,这有助于节省内存。
欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

惊雷无声

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表