我有點不清楚如何以及是否使用記憶體快取。
我正在開發一個 Java 應用程式(Minecraft Spigot 插件),它創建了一個中世紀 MMO 型別的游戲,其中有很多物件(城鎮、結構和具有特定屬性的專案等)。
許多這些物件被讀取和修改了很多,這讓我研究了一種快取解決方案,以防止這些物件開始從 MySQL 資料庫中獲取(這可能不是 MMO 的最佳解決方案,但我想堅持下去目前)。
因此,對于我的特定情況,我認為記憶體中快取方法似乎是正確的選擇。我一直在搜索和挖掘有關快取和快取提供程式的大量資訊,但我仍然不清楚記憶體快取是否需要在每次修改物件時檢索和快取物件。
這對我來說很重要,因為有些物件會不斷修改。我們以城門為例。Gate 物件具有 health 屬性,當玩家損壞門時該屬性會發生變化。在我的理解中,每次玩家損壞門時獲取門物件、更改健康屬性和快取修改后的物件似乎有點麻煩。特別是考慮到大多數門物件的默認生命值為 500,而玩家的最大傷害輸出可以是 20。這意味著如果玩家希望摧毀門物件,則物件必須是在幾分鐘內獲取、修改和快取回 25 次。
是否有可能只從快取中獲取物件(假設該物件已從資料庫中檢索并快取)修改它并保持原樣?這當然意味著快取將保存對物件的參考,并且在修改時快取將參考修改后的實體。如果這是可能的,它是一種性能方面的好方法嗎?
總結我的問題:
- 考慮到我的情況(經常修改大量物件),記憶體中(鍵值)快取是否合乎邏輯?
- 在快取中保存對快取物件的參考是否可能且高效(性能方面)?
在此先感謝您的幫助!
uj5u.com熱心網友回復:
有趣的問題,因為它涉及性能工程、并發編程的許多方面,尤其是通過性能權衡的一致性。
可以在 Hibernate 中啟用快取并配置寫入快取,例如,可以使用 EHCache。然而,仍然有相當大的開銷,因為 Hibernate 和資料庫是為事務性作業負載和一致性而設計的。
我將提出一個基于cache2k的潛在最佳解決方案。我確實利用了 cache2k 中其他快取中不存在的一些功能,但是,我在這里使用的一些技術可以與其他快取實作一起使用,例如 EHCache、Guava Cache、Caffeine 或支持按參考存盤的 JCache/JSR107 .
由于您在單個服務器上運行,因此使用記憶體資料是最有效的。可以跳過或延遲寫入,因為您沒有銀行賬戶等交易資料。在服務器崩潰的情況下,丟失一點點狀態更新是可以容忍的。在發生崩潰時,它始終是更新性能和潛在資料丟失之間的權衡。
您可以在地圖或快取中保存當前狀態,然后更新現有物件。例子:
class Gate {
final AtomicInteger health = new AtomicInteger(100);
}
Cache<Id, Gate> cache = ....;
void decreaseHealth(Id id, int damage) {
Gate gate = cache.get(id);
gate.health.addAndGet(-damage);
}
我提供了稍后與資料庫互動的附加代碼,并首先關注記憶體中的更新。
如果您使用地圖而不是快取,則需要使用執行緒安全的地圖,例如ConcurrentHashMap.
由于更改可能同時發生,您需要使用一種方法來自動更新健康狀況。上面我用過AtomicInteger。另一種可能性是原子更新程式或 var 句柄。如果你只更新一個值,這種方法是最有效的,因為它轉換為硬體上的單個 CAS 操作。如果您更新物件中的多個值,請synchronized對快取/映射條目使用鎖或原子操作。例子:
class Gate {
int health = 100;
// ....
}
cache.asMap().compute(id, (unused, gate) -> {
gate.health -= damage;
if (gate.health == 0) {
// more changes to the object if destroyed totally
}
return gate;
});
這是一個基于 cache2k 的作業解決方案的想法,它會在發生重大變化時安排寫入。
// mocks
class Id {}
Gate readFromDb() { return null; }
void writeDb(Gate g) { }
class Gate {
final AtomicInteger health = new AtomicInteger(100);
volatile boolean writeScheduled = false;
final AtomicInteger persistentHealth = new AtomicInteger(100);
boolean isDirty() {
return persistentHealth.get() != health.get();
}
}
Cache<Id, Gate> cache =
new Cache2kBuilder<Id, Gate>() {}
.loader((id, l, cacheEntry) -> {
if (cacheEntry == null) { return readFromDb(); }
Gate gate = cacheEntry.getValue();
return gate;
})
.addListener((CacheEntryExpiredListener<Id, Gate>) (cache, cacheEntry) -> {
writeIfModified(cacheEntry.getValue());
})
.refreshAhead(true)
.keepDataAfterExpired(true)
.expireAfterWrite(5, TimeUnit.MINUTES)
.loaderExecutor(Executors.newFixedThreadPool(30))
.build();
void writeIfModified(Gate gate) {
if (!gate.isDirty()) { return; }
int persistentHealth = gate.health.get();
writeDb(gate);
gate.writeScheduled = false;
gate.persistentHealth.set(persistentHealth);
}
long writeBehindDelayMillis = 500;
int changePercentage = 10;
public void decreaseHealth(Id id, int damage) {
Gate gate = cache.get(id);
int persistentHealth = gate.persistentHealth.get();
int newHealth = gate.health.addAndGet(-damage);
if (!gate.writeScheduled) {
int percentage = persistentHealth * 10 / 100;
if (newHealth > persistentHealth percentage ||
newHealth < persistentHealth - percentage) {
cache.invoke(id, entry -> {
entry.setValue(entry.getValue());
entry.setExpiryTime(entry.getStartTime() writeBehindDelayMillis);
entry.getValue().writeScheduled = true;
return null;
});
}
}
}
這以讀取模式操作快取,因此cache.get()觸發初始資料庫加載。此外,如果需要,它使用到期和提前重繪 來安排延遲寫入。這有點棘手,因為通常和“記錄在案”的提前重繪 用例是不同的。如果有興趣,我可以在博客文章中解釋詳細的機制。Stack Overflow 的答案有點太重了。
當然,您也可以將一些想法用于其他快取實作。
最后一個注意事項:如果地圖計算用于原子性,如果并發同步資料庫寫入正在進行,操作可能會阻塞。要么進行異步寫入,要么對更新使用不同的鎖定。
通過 JPA 快取對寫回方法進行性能比較仍然很有趣。
uj5u.com熱心網友回復:
是的,需要快取。發出 SQL 請求太長。比如它保存在RAM中,它顯然更快。
你怎么能救他們?
- 只需使用地圖。
您必須根據資料型別選擇地圖型別,特別是鍵。
您可以在此處或此處獲得有關它的更多資訊。
使用快取管理器,例如使用 MapMaker解釋here(來自番石榴)
您可以制作自己的快取管理器。有時特別清潔,像這樣:
public HashMap<UUID, Data> datas = new HashMap<>(); // here you have to choose the good map type
public void onEnable() {
getServer().getScheduler().runTaskTimerAsynchronously(this, () -> {
synchronized(datas) { // prevent used from others thread
datas.values().forEach(Data::save); // save all data
datas.clear();
}
}, 20 * 60, 20 * 60); // in ticks, 20 ticks = 1s so 20*60 = 1min
}
在這里,您每分鐘保存所有物件并清除它們。清除是為了防止資料過多。
我建議你結合多件事:使用快取,并在長時間不使用時保存。
轉載請註明出處,本文鏈接:https://www.uj5u.com/gongcheng/347730.html
下一篇:洗掉url末尾的所有斜杠
