记录一次Java文件锁引起的并发写文件问题

背景

刚接手新项目,该项目是高并发的游戏日志服务端存储,一个项目适配多个游戏,很多特殊需求要兼容,刚开始接手,需要修复很多管道的数据,存储管道有两个,分别是MySQL和HDFS,数据消费自Kafka,从Kafka拉数据后,备份到本地,线上数据异常,从备份恢复数据,这套流程已经被设计好,玩的很6。

修复一个时间跨度大的备份时,经常报解析数据异常,排查后发现有些数据写乱了。
备份按行分割,每行数据用json存储,有的行json被截取了,导致无法解析,数据错误概率大概为万分之二三。

埋点抓取异常数据

发现备份数据错乱后,捕获了异常,将错误json,写到一个单独的文件中。
捕获json解析失败的行数据,发现行数据如下:

516"}
"}
}
437484_1193"}

524"}

_5758"}

5_9103487_8283"}
}
6276"}
_2528"}
8_7289"}
2"}
4"}
075_2467"}
"}
"}
029591_4539"}

很明显,json无法解析这些行数据,进一步分析发现,这些数据是前些行没写完的数据。
数据写乱了,最直观的想法是,线程同步问题。

源代码分析

下面贴出多线程并发写文件的代码:

public static void writeFileLock(String content, String filePath) {
        File file = new File(filePath);
        RandomAccessFile fout = null;
        FileChannel fcout = null;
        try {
            fout = new RandomAccessFile(file, "rw");
            fcout = fout.getChannel();//打开文件通道
            FileLock flout = null;
            while (true) {
                try {
                    flout = fcout.tryLock();//不断的请求锁,如果请求不到,等一秒再请求
                    break;
                } catch (Exception e) {
                    log.debug("等待锁" + filePath, "数据:" + content);
                    Thread.sleep(30);
                }
            }
            long filelength = fout.length();//获取文件的长度
            fout.seek(filelength);//将文件的读写指针定位到文件的末尾
            fout.write(content.getBytes());//将需要写入的内容写入文件
            flout.release();
            fcout.close();
            fout.close();
        } catch (IOException e) {
            e.printStackTrace();
            log.error("file no find ..." + filePath, "数据:" + content);
        } catch (InterruptedException e1) {
            log.error("写入异常文件:" + filePath, "数据:" + content);
            e1.printStackTrace();
        } finally {
            if (fcout != null) {
                try {
                    fcout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    fcout = null;
                }
            }
            if (fout != null) {
                try {
                    fout.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    fout = null;
                }
            }
        }
    }

仔细研究tryLock的代码发现三个特性:
1、tryLock非阻塞,调用后立刻返回。
2、tryLock()无参方法,实际调用有参方法:tryLock(0L, Long.MAX_VALUE, false),前两个参数表示锁住整个文件,第三个参数表示独占所。
3、tryLock()方法调用后,有3中可能,第一、拿到文件索返回对象;第二、没有拿到文件索返回null;第三、抛出异常。

很显然,上述代码没有经过斟酌,忽略了文件锁可能返回null,导致劝告锁失效,多线程没有卡住,逃出while循环,这个在本地环境复现,测试中发现抛出异常的概率很大,因此做出以下两点优化。

优化

第一、文件所判空。
第二、巧用Thread.sleep(0),让没有拿到锁的线程重新回到可执行队列,等待cpu的时间分片切换。

修改后:

public static void writeFileLock(String content, String filePath) {
        File file = new File(filePath);
        RandomAccessFile fout = null;
        FileChannel fcout = null;
        FileLock flout = null;
        try {
            fout = new RandomAccessFile(file, "rw");
            fcout = fout.getChannel();//打开文件通道
            while (true) {
                try {
                    flout = fcout.tryLock();
                    if (flout != null) {
                        break;
                    }
                } catch (Exception e) {
                    Thread.sleep(0);
                }
            }
            long filelength = fout.length();//获取文件的长度
            fout.seek(filelength);//将文件的读写指针定位到文件的末尾
            fout.write(content.getBytes());//将需要写入的内容写入文件
            flout.release();
            fcout.close();
            fout.close();
        } catch (Exception e) {
            log.error("写文件异常", e);
            log.error("写入文件路径:{}, 文件内容:{}", filePath, content);
        } finally {
            try {
                if (flout != null && flout.isValid()) {
                    flout.release();
                }
                if (fcout != null) {
                    fcout.close();
                }
                if (fout != null) {
                    fout.close();
                }
            } catch (Exception e) {
                log.error("关闭文件流异常", e);
            }
        }
    }

优化后,测试数据不再错乱。

总结

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 10,795评论 0 9
  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 11,122评论 1 19
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 13,806评论 1 32
  • 爱有时很明白 一针见血的人 总是大惊小怪 你说不爱就不爱 你说分开就分开 难过了受伤了回来了 还想要我真心以待 我...
    倾花似雪阅读 1,683评论 0 2
  • 又一场花开,我愿等一本书,一杯茶,一縷炊煙和一個未歸的你。
    雪嬌阅读 884评论 0 0