From c4adcbe5bb4f0bce591556ebb3fe7748688b6b3a Mon Sep 17 00:00:00 2001 From: Arbousier1 Date: Sat, 1 Aug 2026 12:27:06 +0800 Subject: [PATCH] Optimize persistence I/O --- .../ultimateshop/database/SQLDatabase.java | 102 +++++++++------- .../ultimateshop/database/YamlDatabase.java | 48 +++++++- .../objects/caches/ObjectCache.java | 110 +++++++++++++++--- .../caches/ObjectRandomPlaceholderCache.java | 4 +- .../objects/caches/ObjectUseTimesCache.java | 2 +- 5 files changed, 201 insertions(+), 65 deletions(-) diff --git a/core/src/main/java/cn/superiormc/ultimateshop/database/SQLDatabase.java b/core/src/main/java/cn/superiormc/ultimateshop/database/SQLDatabase.java index b17cfc8..ba3dce8 100644 --- a/core/src/main/java/cn/superiormc/ultimateshop/database/SQLDatabase.java +++ b/core/src/main/java/cn/superiormc/ultimateshop/database/SQLDatabase.java @@ -301,15 +301,15 @@ private void loadCustomPlaceholders(Connection conn, ObjectCache cache, String p @Override public void updateData(ObjectCache cache, boolean quitServer) { - long saveVersion = cache.getModificationVersion(); + ObjectCache.SaveRevision saveRevision = cache.captureSaveRevision(quitServer); CompletableFuture.runAsync(() -> { try { boolean saved; synchronized (cache.getSaveLock()) { - saved = saveAll(cache); + saved = saveSections(cache, saveRevision); } if (saved) { - cache.markSaved(saveVersion); + cache.markSaved(saveRevision); } } finally { if (quitServer) { @@ -321,24 +321,61 @@ public void updateData(ObjectCache cache, boolean quitServer) { }, DatabaseExecutor.getExecutor()); } - private boolean saveAll(ObjectCache cache) { - boolean saved = saveUseTimes(cache); - saved &= saveFavourites(cache); - if (!UltimateShop.freeVersion) { - saved &= savePlaceholders(cache); - saved &= saveCustomPlaceholders(cache); + private boolean saveSections(ObjectCache cache, ObjectCache.SaveRevision saveRevision) { + if (!saveRevision.hasSections()) { + return true; + } + + try (Connection conn = dataSource.getConnection()) { + boolean originalAutoCommit = conn.getAutoCommit(); + try { + conn.setAutoCommit(false); + if (saveRevision.includes(ObjectCache.DirtySection.USE_TIMES)) { + saveUseTimes(conn, cache); + } + if (saveRevision.includes(ObjectCache.DirtySection.FAVOURITES)) { + saveFavourites(conn, cache); + } + if (!UltimateShop.freeVersion) { + if (saveRevision.includes(ObjectCache.DirtySection.RANDOM_PLACEHOLDERS)) { + savePlaceholders(conn, cache); + } + if (saveRevision.includes(ObjectCache.DirtySection.CUSTOM_PLACEHOLDERS)) { + saveCustomPlaceholders(conn, cache); + } + } + conn.commit(); + return true; + } catch (SQLException | RuntimeException exception) { + try { + conn.rollback(); + } catch (SQLException rollbackException) { + exception.addSuppressed(rollbackException); + } + exception.printStackTrace(); + return false; + } finally { + try { + if (!conn.isClosed() && conn.getAutoCommit() != originalAutoCommit) { + conn.setAutoCommit(originalAutoCommit); + } + } catch (SQLException exception) { + exception.printStackTrace(); + } + } + } catch (SQLException exception) { + exception.printStackTrace(); + return false; } - return saved; } - private boolean saveFavourites(ObjectCache cache) { + private void saveFavourites(Connection conn, ObjectCache cache) throws SQLException { if (cache.isServer()) { - return true; + return; } String playerUUID = cache.getPlayer().getUniqueId().toString(); - try (Connection conn = dataSource.getConnection(); - PreparedStatement deletePs = conn.prepareStatement(dialect.deleteFavourites()); + try (PreparedStatement deletePs = conn.prepareStatement(dialect.deleteFavourites()); PreparedStatement insertPs = conn.prepareStatement(dialect.insertFavourite())) { deletePs.setString(1, playerUUID); @@ -364,22 +401,17 @@ private boolean saveFavourites(ObjectCache cache) { if (dialect.supportBatch()) { insertPs.executeBatch(); } - return true; - } catch (SQLException e) { - e.printStackTrace(); - return false; } } - private boolean saveUseTimes(ObjectCache cache) { + private void saveUseTimes(Connection conn, ObjectCache cache) throws SQLException { String playerUUID = cache.isServer() ? "Global-Server" : cache.getPlayer().getUniqueId().toString(); String sql = dialect.upsertUseTimes(); - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = conn.prepareStatement(sql)) { for (Map.Entry entry : cache.getSharedUseTimesCache().entrySet()) { writeUseTimesCache(ps, playerUUID, entry.getKey(), entry.getValue()); @@ -388,10 +420,6 @@ private boolean saveUseTimes(ObjectCache cache) { if (dialect.supportBatch()) { ps.executeBatch(); } - return true; - } catch (SQLException e) { - e.printStackTrace(); - return false; } } @@ -465,15 +493,14 @@ private void fillUseTimes(PreparedStatement ps, } } - private boolean savePlaceholders(ObjectCache cache) { + private void savePlaceholders(Connection conn, ObjectCache cache) throws SQLException { String playerUUID = cache.isServer() ? "Global-Server" : cache.getPlayer().getUniqueId().toString(); String sql = dialect.upsertRandomPlaceholder(); - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = conn.prepareStatement(sql)) { for (ObjectRandomPlaceholderCache ph : cache.getRandomPlaceholderCache().values()) { @@ -497,22 +524,17 @@ private boolean savePlaceholders(ObjectCache cache) { if (dialect.supportBatch()) { ps.executeBatch(); } - return true; - } catch (SQLException e) { - e.printStackTrace(); - return false; } } - private boolean saveCustomPlaceholders(ObjectCache cache) { + private void saveCustomPlaceholders(Connection conn, ObjectCache cache) throws SQLException { String playerUUID = cache.isServer() ? "Global-Server" : cache.getPlayer().getUniqueId().toString(); String sql = dialect.upsertCustomPlaceholder(); - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = conn.prepareStatement(sql)) { for (Map.Entry entry : cache.getCustomPlaceholderCache().entrySet()) { @@ -531,10 +553,6 @@ private boolean saveCustomPlaceholders(ObjectCache cache) { if (dialect.supportBatch()) { ps.executeBatch(); } - return true; - } catch (SQLException e) { - e.printStackTrace(); - return false; } } @@ -568,14 +586,14 @@ private void loadHistory(ObjectUseTimesCache cache, String sellHistoryJson, Stri @Override public void updateDataOnDisable(ObjectCache cache, boolean disable) { - long saveVersion = cache.getModificationVersion(); + ObjectCache.SaveRevision saveRevision = cache.captureSaveRevision(true); try { boolean saved; synchronized (cache.getSaveLock()) { - saved = saveAll(cache); + saved = saveSections(cache, saveRevision); } if (saved) { - cache.markSaved(saveVersion); + cache.markSaved(saveRevision); } } finally { CacheManager.cacheManager.removeObjectCache(cache); diff --git a/core/src/main/java/cn/superiormc/ultimateshop/database/YamlDatabase.java b/core/src/main/java/cn/superiormc/ultimateshop/database/YamlDatabase.java index 98a45af..4e058b5 100644 --- a/core/src/main/java/cn/superiormc/ultimateshop/database/YamlDatabase.java +++ b/core/src/main/java/cn/superiormc/ultimateshop/database/YamlDatabase.java @@ -15,6 +15,11 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; @@ -44,7 +49,7 @@ private void loadData(ObjectCache cache) { if (!file.exists()) { YamlConfiguration config = new YamlConfiguration(); config.set("playerName", cache.isServer() ? "global" : cache.getPlayer().getName()); - config.save(file); + writeAtomically(config, file); } } catch (IOException e) { ErrorManager.errorManager.sendErrorMessage( @@ -151,7 +156,7 @@ private void loadData(ObjectCache cache) { @Override public void updateData(ObjectCache cache, boolean quitServer) { - long saveVersion = cache.getModificationVersion(); + ObjectCache.SaveRevision saveRevision = cache.captureSaveRevision(true); CompletableFuture.runAsync(() -> { try { boolean saved; @@ -159,7 +164,7 @@ public void updateData(ObjectCache cache, boolean quitServer) { saved = saveData(cache); } if (saved) { - cache.markSaved(saveVersion); + cache.markSaved(saveRevision); } } finally { if (quitServer) { @@ -173,14 +178,14 @@ public void updateData(ObjectCache cache, boolean quitServer) { @Override public void updateDataOnDisable(ObjectCache cache, boolean disable) { - long saveVersion = cache.getModificationVersion(); + ObjectCache.SaveRevision saveRevision = cache.captureSaveRevision(true); try { boolean saved; synchronized (cache.getSaveLock()) { saved = saveData(cache); } if (saved) { - cache.markSaved(saveVersion); + cache.markSaved(saveRevision); } } finally { CacheManager.cacheManager.removeObjectCache(cache); @@ -232,7 +237,7 @@ private boolean saveData(ObjectCache cache) { } try { - config.save(file); + writeAtomically(config, file); return true; } catch (IOException e) { ErrorManager.errorManager.sendErrorMessage("§cError: Can not save data file: " + file.getName() + "!"); @@ -240,6 +245,37 @@ private boolean saveData(ObjectCache cache) { } } + private void writeAtomically(YamlConfiguration config, File file) throws IOException { + Path temporaryFile = Files.createTempFile( + dataDir.toPath(), + file.getName() + ".", + ".tmp" + ); + try { + Files.writeString(temporaryFile, config.saveToString(), StandardCharsets.UTF_8); + try { + Files.move( + temporaryFile, + file.toPath(), + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING + ); + } catch (AtomicMoveNotSupportedException ignored) { + Files.move( + temporaryFile, + file.toPath(), + StandardCopyOption.REPLACE_EXISTING + ); + } + } finally { + try { + Files.deleteIfExists(temporaryFile); + } catch (IOException ignored) { + // Keep the original write failure if cleanup also fails. + } + } + } + private void writeUseTimesCache(ConfigurationSection root, UseTimesStorageKey key, ObjectUseTimesCache cache) { diff --git a/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectCache.java b/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectCache.java index ceaad58..76456e8 100644 --- a/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectCache.java +++ b/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectCache.java @@ -22,9 +22,49 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicLongArray; public class ObjectCache { + public enum DirtySection { + USE_TIMES, + FAVOURITES, + RANDOM_PLACEHOLDERS, + CUSTOM_PLACEHOLDERS + } + + public static final class SaveRevision { + + private final long cacheVersion; + + private final long[] sectionVersions; + + private final boolean[] includedSections; + + private SaveRevision(long cacheVersion, + long[] sectionVersions, + boolean[] includedSections) { + this.cacheVersion = cacheVersion; + this.sectionVersions = sectionVersions; + this.includedSections = includedSections; + } + + public boolean includes(DirtySection section) { + return includedSections[section.ordinal()]; + } + + public boolean hasSections() { + for (boolean included : includedSections) { + if (included) { + return true; + } + } + return false; + } + } + + private static final DirtySection[] DIRTY_SECTIONS = DirtySection.values(); + private final Map sharedUseTimesCache = new ConcurrentHashMap<>(); private final Map useTimesCache = new ConcurrentHashMap<>(); @@ -51,6 +91,12 @@ public class ObjectCache { private final AtomicLong savedVersion = new AtomicLong(); + private final AtomicLongArray sectionModificationVersions = + new AtomicLongArray(DIRTY_SECTIONS.length); + + private final AtomicLongArray sectionSavedVersions = + new AtomicLongArray(DIRTY_SECTIONS.length); + private final AtomicBoolean autoSaveInProgress = new AtomicBoolean(); // 同一个缓存可能同时遇到自动保存和玩家退出保存,串行化落盘可避免旧任务覆盖新数据。 @@ -233,7 +279,7 @@ public void setRandomPlaceholderCache(ObjectRandomPlaceholder placeholder, ) ); if (!UltimateShop.freeVersion && !"ONCE".equals(placeholder.getMode())) { - markDirty(); + markDirty(DirtySection.RANDOM_PLACEHOLDERS); } } @@ -295,7 +341,7 @@ public void setCustomPlaceholderCache(ObjectCustomPlaceholder placeholder, Strin String normalizedValue = placeholder.normalizeValue(nowValue); String previousValue = customPlaceholderCache.put(placeholder, normalizedValue); if (!UltimateShop.freeVersion && !Objects.equals(previousValue, normalizedValue)) { - markDirty(); + markDirty(DirtySection.CUSTOM_PLACEHOLDERS); } } @@ -335,14 +381,14 @@ public synchronized void setFavouriteProductCache(String menuName, } if (references == null || references.isEmpty()) { if (favouriteProductCache.remove(menuName) != null) { - markDirty(); + markDirty(DirtySection.FAVOURITES); } return; } List newReferences = new ArrayList<>(references); List previousReferences = favouriteProductCache.put(menuName, newReferences); if (!newReferences.equals(previousReferences)) { - markDirty(); + markDirty(DirtySection.FAVOURITES); } } @@ -367,7 +413,7 @@ public synchronized boolean addFavouriteProduct(String menuName, ObjectItem item return false; } references.add(reference); - markDirty(); + markDirty(DirtySection.FAVOURITES); return true; } @@ -405,7 +451,7 @@ public synchronized boolean removeFavouriteProduct(String menuName, FavouritePro favouriteProductCache.remove(menuName); } if (removed) { - markDirty(); + markDirty(DirtySection.FAVOURITES); } return removed; } @@ -421,7 +467,7 @@ public synchronized boolean moveFavouriteProduct(String menuName, int fromIndex, } FavouriteProductReference reference = references.remove(fromIndex); references.add(toIndex, reference); - markDirty(); + markDirty(DirtySection.FAVOURITES); return true; } @@ -430,7 +476,7 @@ public synchronized void clearFavouriteProductCache(String menuName) { return; } if (favouriteProductCache.remove(menuName) != null) { - markDirty(); + markDirty(DirtySection.FAVOURITES); } } @@ -456,7 +502,7 @@ public synchronized Map getResolvedFavour favouriteProductCache.put(menuName, validReferences); } if (validReferences.size() != references.size()) { - markDirty(); + markDirty(DirtySection.FAVOURITES); } return result; } @@ -485,6 +531,10 @@ public void close() { } public void ready() { + for (DirtySection section : DIRTY_SECTIONS) { + int index = section.ordinal(); + sectionSavedVersions.set(index, sectionModificationVersions.get(index)); + } savedVersion.set(modificationVersion.get()); ready = true; if (player != null) { @@ -496,8 +546,9 @@ public boolean canNotModify() { return closed || !ready; } - void markDirty() { + void markDirty(DirtySection section) { if (ready && !closed) { + sectionModificationVersions.incrementAndGet(section.ordinal()); modificationVersion.incrementAndGet(); } } @@ -506,12 +557,43 @@ public boolean isDirty() { return modificationVersion.get() != savedVersion.get(); } - public long getModificationVersion() { - return modificationVersion.get(); + public SaveRevision captureSaveRevision(boolean includeAllSections) { + long cacheVersion = modificationVersion.get(); + long[] sectionVersions = new long[DIRTY_SECTIONS.length]; + boolean[] includedSections = new boolean[DIRTY_SECTIONS.length]; + boolean hasIncludedSection = false; + + for (DirtySection section : DIRTY_SECTIONS) { + int index = section.ordinal(); + long sectionVersion = sectionModificationVersions.get(index); + sectionVersions[index] = sectionVersion; + if (includeAllSections || sectionVersion != sectionSavedVersions.get(index)) { + includedSections[index] = true; + hasIncludedSection = true; + } + } + + // A conservative fallback keeps future unclassified mutations safe. + if (!hasIncludedSection && cacheVersion != savedVersion.get()) { + for (DirtySection section : DIRTY_SECTIONS) { + includedSections[section.ordinal()] = true; + } + } + return new SaveRevision(cacheVersion, sectionVersions, includedSections); } - public void markSaved(long version) { - savedVersion.accumulateAndGet(version, Math::max); + public void markSaved(SaveRevision revision) { + for (DirtySection section : DIRTY_SECTIONS) { + int index = section.ordinal(); + if (revision.includedSections[index]) { + sectionSavedVersions.accumulateAndGet( + index, + revision.sectionVersions[index], + Math::max + ); + } + } + savedVersion.accumulateAndGet(revision.cacheVersion, Math::max); } public void finishAutoSave() { diff --git a/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectRandomPlaceholderCache.java b/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectRandomPlaceholderCache.java index 0bc6118..51f900b 100644 --- a/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectRandomPlaceholderCache.java +++ b/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectRandomPlaceholderCache.java @@ -57,7 +57,7 @@ public LocalDateTime getRefreshDoneTime() { public synchronized void removeRefreshDoneTime() { if (refreshDoneTime != null && !UltimateShop.freeVersion) { - cache.markDirty(); + cache.markDirty(ObjectCache.DirtySection.RANDOM_PLACEHOLDERS); } refreshDoneTime = null; } @@ -139,7 +139,7 @@ public synchronized void setRefreshTime(boolean notUseBungee) { } if (!UltimateShop.freeVersion && !java.util.Objects.equals(previousRefreshDoneTime, refreshDoneTime)) { - cache.markDirty(); + cache.markDirty(ObjectCache.DirtySection.RANDOM_PLACEHOLDERS); } if (ConfigManager.configManager.getBoolean("use-times.auto-reset-mode")) { diff --git a/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectUseTimesCache.java b/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectUseTimesCache.java index 6d55fc4..eb597f6 100644 --- a/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectUseTimesCache.java +++ b/core/src/main/java/cn/superiormc/ultimateshop/objects/caches/ObjectUseTimesCache.java @@ -656,7 +656,7 @@ private UseTimesState state(Direction direction) { } private void touch() { - cache.markDirty(); + cache.markDirty(ObjectCache.DirtySection.USE_TIMES); } public synchronized List getSellHistory() {