效能設計Performance
交易不做任何 I/O
一筆交易只把帳戶標記為 dirty 然後立刻返回。寫入是非同步的、有去抖動的, 而且只寫變動過的餘額,不是整張表。
200 人的伺服器上,一次商店購買原本代表主執行緒要等一次 200 列的 batch upsert; 現在是一列,而且在別的執行緒。
每筆交易寫入的列數 200 → 1
每筆交易的主執行緒 I/O 阻塞寫入 + 等待網路 → 無
交易具原子性
withdrawPlayer 與 depositPlayer 是對帳戶的單一原子操作,
以 ConcurrentHashMap.compute() 實作 —— 讀取、餘額檢查、寫入在同一把鎖內完成。
兩個插件同時操作同一個帳戶(例如商店與簽到獎勵)不會互相覆寫。 8 執行緒 × 25,000 筆並行扣款的壓力測試下,最終餘額精確無誤差。
名稱解析不掃描目錄
以玩家名稱查詢時,解析順序由便宜到昂貴:
- 線上玩家精確比對
- 本地 name→UUID 快取,玩家加入時寫入,能正確處理改名
- 伺服器 usercache(
getOfflinePlayerIfCached,以反射呼叫以相容舊版 API) - 完整目錄掃描 —— 每 60 秒最多一次,且結果會回填快取
失敗查詢有 60 秒負快取,不存在的名字不會反覆觸發掃描。 實測 210,000 次查詢僅觸發 1 次目錄掃描。
%vault_balance_<player>% 這類變數以及舊版 String 形式的
Vault Economy API 觸發 —— 兩者都可能每秒被呼叫數十次。
調校
唯一需要權衡的是 storage.transaction_flush_delay_ticks:
越低則當機時損失的交易越少,但資料庫往返越頻繁。預設 20 tick(一秒)在多數情況下是合理的。
Transactions perform no I/O
A transaction marks the account dirty and returns immediately. Writes happen asynchronously, debounced, and cover only the balances that changed — never the whole table.
On a 200-player server a shop purchase used to mean the main thread waiting on a 200-row batch upsert. Now it is a single row, off-thread.
Rows written per transaction 200 → 1
Main-thread I/O per transaction blocking write + network wait → none
Transactions are atomic
withdrawPlayer and depositPlayer are single atomic operations on the
account, implemented with ConcurrentHashMap.compute() so the read, the
sufficient-funds check and the write all happen under the same lock.
Two plugins touching the same balance at once — a shop and a daily-reward task, say — cannot overwrite each other. Under a stress test of 8 threads × 25,000 concurrent withdrawals the final balance is exact.
Name lookups do not scan the data directory
Resolving a player name goes from cheapest to most expensive:
- Exact match against online players
- Local name→UUID cache, populated on join so renames stay correct
- The server's own user cache (
getOfflinePlayerIfCached, called reflectively so older APIs still work) - A full directory scan — at most once every 60 seconds, warming the cache as it goes
Failed lookups are negatively cached for 60 seconds, so a name that does not exist cannot trigger repeated scans. In testing, 210,000 lookups triggered a single directory scan.
%vault_balance_<player>% and by the
legacy String form of the Vault Economy API — both of which can run dozens of times per second.
Tuning
The only real trade-off is storage.transaction_flush_delay_ticks: lower loses
fewer transactions in a crash but costs more database round trips. The default of 20 ticks
(one second) suits most servers.