用GraphicsMagick有一段時間了,一直以來都沒有什麼問題(最主要的原因是沒有什麼并發量),前一段時間出現過Empty input file的異常,
應該是下面這種情況導緻的,兩個線程請求縮略圖,其中一個請求先完成操作,生成一張縮略圖,但GraphicsMagick會先生成一張空白的縮略圖檔案,然後再向檔案填充内容,
此時另一個線程判斷縮略圖存在,但還沒有填充完畢,那麼其實這是一個空白檔案,這樣就導緻了EmptyInputFile異常。當時也沒有多加考慮,采用的方法是先将縮略圖寫入一個臨時檔案,
寫入完畢之後再将該臨時檔案移動到目标檔案,但由于當時設計的臨時檔案相對縮略圖是固定位址的,這樣某些情況下移動檔案時還會導緻檔案不存在的異常,最後一怒之下加了一把鎖。
最近由于某些縮略圖生成錯誤,是以幹脆直接删除了整個縮略圖檔案夾,當我通路圖檔較多的文章,比如https://www.qyh.me/space/life/article/dog-life ,那個圖檔生成速度,簡直了。。。
想了一下,把檔案移動這個操作放到了GraphicsMagick操作類中:
IMOperation op = new IMOperation();
op.addImage();
setResize(resize, op);
String ext = FileUtils.getFileExtension(dest.getName());
if (!maybeTransparentBg(ext)) {
setWhiteBg(op);
}
op.strip();
op.p_profile("*");
if (interlace(dest)) {
op.interlace("Line");
}
op.addImage();
File temp = FileUtils.appTemp(ext);
run(op, src.getAbsolutePath(), temp.getAbsolutePath());
FileUtils.move(temp, dest);
這樣看來并發的情況下應該不會有什麼問題了(實際上,在測試過程中,生成同一個檔案的縮略圖時,在windows上依舊會出現各種各樣的IO異常,但在linux上并沒有發現)。
但是部署到伺服器之後,由于同時生成縮略圖的指令過多,又出現了
org.im4java.core.CommandException: org.im4java.core.CommandException: return code: 137
最後,給生成縮略圖的方法單獨設定了一個線程池用來防止過多的縮略圖操作,增加了ConcurrentHashMap用來防止同時生成相同的縮略圖:
~~
package me.qyh.blog.file.local;
import static me.qyh.blog.file.ImageHelper.JPEG;
import static me.qyh.blog.file.ImageHelper.PNG;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import me.qyh.blog.exception.SystemException;
import me.qyh.blog.file.ImageHelper;
import me.qyh.blog.file.Resize;
import me.qyh.blog.file.local.ImageResourceStore.ResizeStrategy;
import me.qyh.blog.util.FileUtils;
public class CachedResizeStrategy implements ResizeStrategy, ApplicationListener{
@Autowired
private ImageHelper imageHelper;
private final Map fileMap = new ConcurrentHashMap<>();
private final Map coverMap = new ConcurrentHashMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(CachedResizeStrategy.class);
private final ExecutorService executor;
public CachedResizeStrategy(int max){
if (max < 0) {
throw new SystemException("最大執行線程數不能小于" + max);
}
executor = Executors.newFixedThreadPool(max);
}
public CachedResizeStrategy(){
this(5);
}
@Override
public void doResize(File local, File thumb, Resize resize) throws IOException{
String ext = FileUtils.getFileExtension(local.getName());
String coverExt = "." + (ImageHelper.maybeTransparentBg(ext) ? PNG : JPEG);
File cover = new File(thumb.getParentFile(), FileUtils.getNameWithoutExtension(local.getName()) + coverExt);
String coverCanonicalPath = cover.getCanonicalPath();
String thumbCanonicalPath = thumb.getCanonicalPath();
coverMap.compute(coverCanonicalPath, (ck, cv) -> {
if (!cover.exists()) {
executeFormat(local,cover)
}
return null;
});
fileMap.compute(thumbCanonicalPath, (k, v) -> {
if (!thumb.exists()) {
executeResize(cover,thumb,resize)
}
return null;
});
}
private void executeFormat(File src, File dest){
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
try {
FileUtils.forceMkdir(dest.getParentFile());
imageHelper.format(src, dest);
} catch (IOException e) {
if (src.exists()) {
LOGGER.error(e.getMessage(), e);
}
}
return null;
}, executor);
future.join();
}
private void executeResize(File src, File dest, Resize resize){
CompletableFuture future = CompletableFuture.supplyAsync(() -> {
try {
FileUtils.forceMkdir(dest.getParentFile());
imageHelper.resize(resize, src, dest);
} catch (IOException e) {
if (src.exists()) {
LOGGER.error(e.getMessage(), e);
}
}
return null;
}, executor);
future.join();
}
@Override
public void onApplicationEvent(ContextClosedEvent event){
executor.shutdown();
try {
executor.awaitTermination(LIVE_MILL, TimeUnit.MICROSECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
上面的compute方法是1.8新增的,如果是1.5+的環境,可以這樣
private final Map fileMap = new ConcurrentHashMap();
if (fileMap.putIfAbsent(cPath, Boolean.TRUE) == null) {
try {
// 進行縮放
executor.submit(resizeTask).get();
} catch (ExecutionException e) {
throw new RuntimeException(e);
} finally {
// 無論縮放是否成功,删除key
fileMap.remove(cPath);
}
} else {
// 如果有線程正在縮放目标圖檔,此時需要判斷該圖檔是否已經生成
// 如果沒有生成完畢(與是否成功無關),等待一段時間
while (fileMap.containsKey(cPath)) {
try {
Thread.sleep(MIN_SLEEP_MILL);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private final Map fileMap = new ConcurrentHashMap();
if (fileMap.putIfAbsent(cPath, new CountDownLatch(1)) == null) {
try {
// 進行縮放
executor.submit(resizeTask).get();
} catch (ExecutionException e) {
throw new RuntimeException(e);
} finally {
fileMap.get(cPath).countDown();
// 無論縮放是否成功,删除key
fileMap.remove(cPath);
}
} else {
// 如果有線程正在縮放目标圖檔,此時需要判斷該圖檔是否已經生成
// 如果沒有生成完畢(與是否成功無關),等待一段時間
CountDownLatch fromMap = fileMap.get(cPath);
if (fromMap != null) {
try {
fromMap.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
~~
2017.05.22 編輯
學藝不精啊,通過 Semaphore 即可友善的控制并發線程數,使用起來也非常簡單:
Semaphore semaphore = new Semaphore(5);
if (!FileUtils.exists(thumb)) {
try {
semaphore.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new SystemException(e.getMessage(), e);
}
try {
FileUtils.forceMkdir(thumb.getParent());
imageHelper.resize(resize, local, thumb);
} finally {
semaphore.release();
}
}
根本用不着上面這麼繁瑣。。。