在早期的帳戶系統中,但凡是有帳戶變更,就會執行一次數據庫操做。這樣在有複雜一些業務操做的時候,例如單筆交易涉及多個用戶多個費用的資金劃撥,一個事務內操做數據庫幾十次也就大量的存在。而觀察這樣的場景,其本質可能只涉及少數幾方的帳戶。
這時,在一次處理過程當中,合併同一個帳戶的全部操做,最後只提交一次,就能帶來很大的優化空間。數據庫
1. 初始化一個收集器ExecuteParam,用來存放有變更的帳戶、待新增的資金記錄、待處理的凍結數據和待新增的凍結記錄。併發
final ExecuteParam param = ExecuteParam.instance(); public class ExecuteParam { private final Map<String, FinanceAccount> cache = Maps.newHashMap(); private final List<FinanceLog> financeLogs = Lists.newArrayList(); private final Map<String, AccFundManagementRecord> freezeRecords = Maps.newHashMap(); private final List<AccFundManagementHistory> freezeHistorys = Lists.newArrayList(); public static ExecuteParam instance() { return new ExecuteParam(); } public Map<String, FinanceAccount> getCache() { return cache; } public List<FinanceLog> getFinanceLogs() { return financeLogs; } public Map<String, AccFundManagementRecord> getFreezeRecords() { return freezeRecords; } public List<AccFundManagementHistory> getFreezeHistorys() { return freezeHistorys; } }
2. 根據業務須要,進行增、減、轉帳、凍結、解凍操做。ide
public interface FundTransactionService { /** 調增 */ void addCredit(TransactionCommandParam command, final ExecuteParam param); /** 調減 */ void addDebit(TransactionCommandParam command, final ExecuteParam param); /** 轉帳 */ void addTransfer(TransactionCommandParam command, final ExecuteParam param); /** 凍結 */ String addFreeze(TransactionCommandParam command, final ExecuteParam param); /** 解凍 */ BigDecimal addUnfreeze(TransactionCommandParam command, final ExecuteParam param); /** 更新DB */ void execute(String proofId, ExecuteParam param); } public static TransactionCommandParam createTransfer(...); public static TransactionCommandParam createFreeze(...); public static TransactionCommandParam createUnfreeze(...); public static TransactionCommandParam createCredit(...); public static TransactionCommandParam createDebit(...);
3. 全部資金操做在底層都按照:校驗操做類型->修改帳戶餘額->資金記錄的流程執行優化
@Override public void addCredit(TransactionCommandParam command, final ExecuteParam param) { /** 1.校驗 */ /** 2.調帳 */ FinanceAccount receiverFa = credit(command.getReceiverOwnerId(), command.getReceiverRoleId(), command.getAmount(), param.getCache()); /** 3.資金記錄 */ param.getFinanceLogs().add(...); }
4. 其中修改帳戶餘額的方法,會先嚐試從ExecuteParam中查找該帳戶是否已經被操做過,若是沒有才查詢一次DB。這樣就確保了同一個帳戶在一次處理過程當中,不管有多少資金操做,只會查詢一次DB。this
private FinanceAccount credit(Long ownerId, Long roleId, BigDecimal amount, Map<String, FinanceAccount> cache) { final String cacheKey = getCacheKey(ownerId, roleId); FinanceAccount fa = cache.get(cacheKey); if (fa == null) { // 此處只查詢一次DB fa = getFinanceAccount(ownerId, roleId); cache.put(cacheKey, fa); } // 調增: fa.credit(amount); return fa; }
5. 當全部業務操做完成以後,一次性提交本次處理過程當中的全部帳戶code
fundTransactionService.execute(proof.getProofId(), param); @Override public void execute(String proofId, ExecuteParam param) { /** FinanceAccount統一更新 */ for (FinanceAccount account : param.getCache().values()) { account.setProofId(proofId); // 熱點帳戶延遲更新 if (isHotAccount(account.getId())) { continue; } // DB update this.updateAccount(account); logger.info("帳戶更新[{}]", account); } /** FinanceLog統一批量記錄 */ financeLogDao.addFinanceLog(param.getFinanceLogs()); /** 凍結記錄統一批量更新 */ for (AccFundManagementRecord freezeRecord : param.getFreezeRecords().values()) { if (freezeRecord.getId() != null) { // DB update } else { // DB insert } logger.info(LoggerUtil.createInfoLog("execute","凍結記錄[{}]"), freezeRecord); } /** 凍結歷史統一批量更新 */ for (AccFundManagementHistory history : param.getFreezeHistorys()) { // DB insert } }
此次優化不只大幅減小了數據庫的負擔,並且也由於數據庫訪問次數少了,處理速度也快了(例如還款,原先的處理時間約爲1到2s,優化後的處理時間約爲40ms)。處理速度快了,使用樂觀鎖控制的併發異常也相應減小了。事務
另外值得思考的地方是,在第一步初始化收集器ExecuteParam的時候,將全部容器都建立出來了,並非全部業務都會用到所有的容器,這裏是否有必要?
ci
個人想法是讓步於開發便利性。
誠然是能夠根據不一樣的場景有選擇性的初始化相應的容器,可是這樣開發人員在使用的時候須要思考的更多,須要作選擇,不夠簡單明瞭。並且省去一兩個容器的初始化帶來的好處能夠並不大。開發