【问题标题】:Is my attemp to solve a concurrency issue when using java FileWriter correct?使用 java FileWriter 时我尝试解决并发问题是否正确?
【发布时间】:2021-10-05 01:04:12
【问题描述】:

我有一个使用 java.io.FileWriter 编写文件的 java 程序。在生产环境中,该程序会被多次实例化,每个实例都会在 NFS 中的同一目录上写入一个唯一文件。有时同时有太多实例,其中一些(只有少数)因 IOException 而失败,并且消息显示为“错误:权限被拒绝”。
我认为这里的问题是目录上的并发,我想通过添加重试逻辑来解决它,无论如何它都必须写入文件。
我的想法如下,但我不知道它是否会起作用,因为我无法在我的开发环境中进行测试:
try {
    fw.write(str);
} catch (IOException e) {
    boolean retry = true;
    while(retry) {
        TimeUnit.MILLISECONDS.sleep(2);
        fw.write(str);
        retry = false;
    }
} finally {
    fw.close();
}

如果您能给我任何帮助,我将不胜感激。

【问题讨论】:

  • 如果第二次写入失败并抛出IOException,这确实会失败,因为它周围没有try。而且您应该考虑不要构建无限循环,而是最多重试 5 或 10 次。
  • 谢谢@luk2302。是的,我考虑过。我也想过用 try-catch 块包围第二个写入,但我不知道在嵌套的 catch 中做什么。
  • try 需要 inside 循环,在 throwing 调用周围,没有别的:for (int i = 0; i < 5; i++) { try { write... } catch(IOException e){ sleep... } }
  • 我应该在 for 循环内的某处添加一个中断吗?如果5次write语句都成功了怎么办?
  • 啊,是的,当然,你应该在write... 之后休息。

标签: java concurrency permission-denied nfs filewriter


【解决方案1】:

有时同时存在太多实例,其中一些(只是少数)因 IOException 而失败,并且消息显示为“错误:权限被拒绝”。

我想通过添加重试逻辑来解决它,无论如何它都必须写入文件。

你在这里为灾难做好准备。

您的理论(这很合理)是在底层操作系统和/或文件系统驱动程序中有 一些 机制确实允许您创建新的文件句柄但最终会抛出错误,如果太许多句柄同时启动。

这意味着您的系统有时会“发送超过系统处理能力”,而您提出的解决方案只是每 2 毫秒重复尝试这些写入,直到它们成功。如果系统处于发送超过底层系统可以处理的状态,这意味着您有大量的“东西生产者”同时每 2 毫秒重新发送一次,并且考虑到“接收系统”(操作系统 /文件系统驱动程序)显然可能会不知所措,你......没有帮助它例如100 个线程每 2 毫秒触发一次写入请求。

一个更好的系统是您采用排队机制:与其让 100 个线程都在操作系统/文件系统上反复推送写入请求,以 2 毫秒的比率重试,您应该有一个队列,其中单个线程或硬- 有限的线程池正在尝试。

阅读 java.util.concurrent 和朋友,了解如何创建处理作业的池 + 提供作业并等待处理的方法。

或者,如果您真的想走这条路并承担后果,那么粘贴的代码有 4 个问题:

“重试”不受保护

catch 块不适用于自身。它内部发生的任何异常都会发生,它们不会被“捕获”。将其全部重写为while循环; catch 块本身不应触发write,它应该导致循环再次运行。

无指数退避

如果您重试 @ 2 毫秒,但几次都没有成功,那么最好开始等待超过 2 毫秒的时间。你应该继续等待越来越长的时间,这也有助于引入一个随机因素。 (见Exponential Backoff

无限制计数器

最终经过足够多的尝试后,您的应用最好放弃并崩溃(根本没有发生写入),而不是永远挂起。

不重新创建 FileWriter

一旦fw.write 开始抛出异常,它很可能会永远继续这样做,即使底层 OS/fs 现在有时间/空间来处理您的写入请求。最好每次都重新创建整个东西。

把它们放在一起

int count = 0;
while (true) {
    try {
        try (FileWriter fw = ...) {
          fw.write(str);
        }
        break; // break out of the while.
    } catch (IOException e) {
        // you may want to test if that IOException indeed
        // indicates a problem that 'retry' can fix.
        // because if it isn't, this code runs a very very
        // long time before the 1000 hard-limit kicks in!

        // after 1000 retries, give up.
        if (count++ >= 1000) throw e;

        TimeUnit.MILLISECONDS.sleep(rnd.nextInt(count * 4));
    }
}

【讨论】:

  • 总是喜欢 AWS 博客关于退避策略(线性 -> 指数 + 抖动):aws.amazon.com/blogs/architecture/…
  • 这很有趣@rzwitserloot,非常感谢。在尝试任何事情之前,我会确保阅读您的建议。只是为了确保,rnd 是在 catch 块内的某处定义的 java.util.Random 变量吗?
  • 是的,它是Random 的一个实例,您可以将其设为一个字段(我很确定 Random 现在是线程安全的)。不需要特别安全的随机选择,所以只需 new Random().nextInt() 也可以。
猜你喜欢
  • 2022-01-15
  • 2022-10-17
  • 2020-09-03
  • 2022-06-13
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多