java 写文件 并发_记录一次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);

}

}

}

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

总结

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值