1. 为什么后端开发者需要关注并发锁与文件IO?
在真实的后端服务中,并发锁和文件IO就像两个隐藏的"性能杀手"。我曾经历过一个线上事故:一个简单的用户积分更新接口,在促销期间突然出现大量积分错乱。事后排查发现,问题根源在于没有处理好Redis分布式锁的续期机制。而另一个案例是日志服务突然卡死,最终定位到是某个开发者在循环里频繁进行小文件写入操作。
这两个场景揭示了后端开发的核心痛点:
- 并发锁问题往往在流量激增时爆发,且复现困难
- 文件IO操作在本地开发环境难以暴露性能瓶颈
- 两者都可能引发连锁反应,导致服务雪崩
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并发锁实战案例解析
2.1 分布式锁的续期陷阱
在电商优惠券发放系统中,我们使用Redis实现分布式锁。最初的实现是这样的:
java复制// 错误示范
try {
boolean locked = redisTemplate.opsForValue().setIfAbsent("coupon_lock", "1", 10, TimeUnit.SECONDS);
if(locked) {
// 业务处理
}
} finally {
redisTemplate.delete("coupon_lock");
}
这个实现有三个致命缺陷:
- 未考虑业务执行时间可能超过锁有效期(10秒)
- 未实现锁续期机制
- 直接删除锁可能误删其他线程的锁
改进后的方案采用Redisson的看门狗机制:
java复制RLock lock = redissonClient.getLock("coupon_lock");
try {
lock.lock();
// 业务处理
} finally {
lock.unlock();
}
关键经验:永远不要手动实现分布式锁的续期逻辑,使用成熟框架更可靠
2.2 数据库乐观锁的ABA问题
在库存扣减场景中,我们最初使用version字段实现的乐观锁:
sql复制UPDATE inventory SET stock = stock - 1, version = version + 1
WHERE product_id = 1001 AND version = 1
这个方案在秒杀场景下出现了ABA问题:某商品库存从100→99→100,version从1→2→3,虽然最终库存没变,但实际发生了扣减又补货的操作。
解决方案是引入业务流水表,配合CAS机制:
java复制// 使用CAS原子操作
boolean success = redisTemplate.opsForValue()
.setIfAbsent("inventory_cas:"+productId, requestId, 5, TimeUnit.SECONDS);
if(success) {
// 执行库存操作
}
3. 文件IO的隐蔽陷阱
3.1 小文件写入的性能黑洞
某次线上日志服务卡死,排查发现是如下代码导致:
java复制// 错误示范
public void log(String message) {
try (FileWriter writer = new FileWriter("app.log", true)) {
writer.write(message + "\n");
} catch (IOException e) {
e.printStackTrace();
}
}
问题分析:
- 每次写入都打开/关闭文件句柄
- 高频小IO导致磁盘频繁寻道
- 未做缓冲导致系统调用过多
改进方案:
java复制// 使用BufferedWriter + 定时刷新
private static BufferedWriter logWriter;
static {
try {
logWriter = new BufferedWriter(new FileWriter("app.log", true));
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
logWriter.close();
} catch (IOException ignored) {}
}));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void log(String message) {
try {
logWriter.write(message + "\n");
if(++writeCount % 100 == 0) {
logWriter.flush();
}
} catch (IOException e) {
// 错误处理
}
}
3.2 大文件读取的内存风暴
处理CSV文件导入时,出现过OOM问题:
java复制// 危险代码
List<String> lines = Files.readAllLines(Paths.get("huge_file.csv"));
当文件达到GB级别时,这会直接耗尽JVM内存。正确的流式处理方式:
java复制try (Stream<String> stream = Files.lines(Paths.get("huge_file.csv"))) {
stream.limit(1000) // 分批处理
.forEach(line -> process(line));
}
4. 混合场景下的死锁案例
4.1 文件锁与线程锁的嵌套
某次遇到一个诡异死锁,代码如下:
java复制public synchronized void processFile(File file) {
try (FileChannel channel = new RandomAccessFile(file, "rw").getChannel()) {
FileLock lock = channel.lock(); // 阻塞在这里
// 处理文件
}
}
问题根源:
- 线程A获取对象锁后尝试获取文件锁
- 线程B在另一个JVM中持有该文件锁,同时等待线程A释放的对象锁
- 形成跨进程死锁
解决方案:
- 设置文件锁获取超时时间
- 避免同步方法与文件锁混用
- 使用非阻塞的tryLock()
java复制public void processFile(File file) {
try (FileChannel channel = new RandomAccessFile(file, "rw").getChannel()) {
FileLock lock = channel.tryLock();
if(lock != null) {
try {
// 处理文件
} finally {
lock.release();
}
}
}
}
5. 高并发下的文件目录竞争
在临时文件处理场景中,出现过这样的竞态条件:
java复制// 错误代码
if(!tempDir.exists()) {
tempDir.mkdirs();
}
File tempFile = new File(tempDir, UUID.randomUUID().toString());
当并发量高时,可能多个线程同时判断目录不存在,都尝试创建目录,导致异常。正确的做法是:
java复制if(!tempDir.exists() && !tempDir.mkdirs()) {
throw new IOException("Failed to create temp directory");
}
File tempFile = Files.createTempFile(tempDir.toPath(), "tmp", ".dat").toFile();
关键点:Files.createTempFile是原子操作,能避免文件名冲突
6. 文件系统监控的坑
使用WatchService监控目录变化时,遇到过事件丢失问题:
java复制WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("/data/logs");
dir.register(watcher, ENTRY_MODIFY);
while(true) {
WatchKey key = watcher.take(); // 可能丢失快速连续的事件
for(WatchEvent<?> event : key.pollEvents()) {
// 处理事件
}
key.reset();
}
改进方案是结合轮询机制:
java复制// 每5秒全量扫描+事件监听双保险
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
Files.list(dir).forEach(file -> checkModified(file));
}, 0, 5, TimeUnit.SECONDS);
7. 内存映射文件的陷阱
使用MappedByteBuffer处理大文件时,遇到过资源释放问题:
java复制RandomAccessFile raf = new RandomAccessFile("large.bin", "rw");
MappedByteBuffer buffer = raf.getChannel()
.map(FileChannel.MapMode.READ_WRITE, 0, 1GB);
// 使用buffer后...
raf.close(); // 实际上映射未立即释放
正确的清理方式需要显式调用:
java复制// JDK内部API,通过反射调用
Method cleaner = buffer.getClass().getMethod("cleaner");
Cleaner c = (Cleaner)cleaner.invoke(buffer);
c.clean();
8. 文件权限的跨平台问题
在Linux和Windows混合部署环境中,遇到过文件权限问题:
java复制Files.setPosixFilePermissions(path,
PosixFilePermissions.fromString("rw-r-----"));
这在Windows上会抛出UnsupportedOperationException。解决方案:
java复制if(!System.getProperty("os.name").toLowerCase().contains("win")) {
Files.setPosixFilePermissions(path,
PosixFilePermissions.fromString("rw-r-----"));
} else {
path.toFile().setReadable(true, false); // 仅所有者可读
path.toFile().setWritable(true, false);
}
9. 临时文件的安全隐患
创建临时文件时,如果不指定权限,可能被其他用户读取:
java复制// 不安全的临时文件
File temp = File.createTempFile("data", ".tmp");
安全做法是:
java复制Path tempPath = Files.createTempFile("data", ".tmp",
PosixFilePermissions.asFileAttribute(
PosixFilePermissions.fromString("rw-------")));
10. 文件删除的异步陷阱
在文件处理完成后直接删除可能失败:
java复制try (InputStream is = new FileInputStream(file)) {
// 处理文件
}
file.delete(); // 可能失败,因为流未完全关闭
更可靠的做法是:
java复制Path path = file.toPath();
try (InputStream is = Files.newInputStream(path)) {
// 处理文件
}
Files.deleteIfExists(path); // 使用Files工具类
对于Windows系统,还需要考虑文件可能被其他进程锁定:
java复制int retry = 3;
while(retry-- > 0) {
try {
Files.delete(path);
break;
} catch (AccessDeniedException e) {
System.gc(); // 触发finalize释放资源
Thread.sleep(1000);
}
}
