帳戶變更合併提交方案

原由及介紹

在早期的帳戶系統中,但凡是有帳戶變更,就會執行一次數據庫操做。這樣在有複雜一些業務操做的時候,例如單筆交易涉及多個用戶多個費用的資金劃撥,一個事務內操做數據庫幾十次也就大量的存在。而觀察這樣的場景,其本質可能只涉及少數幾方的帳戶。
這時,在一次處理過程當中,合併同一個帳戶的全部操做,最後只提交一次,就能帶來很大的優化空間。數據庫

處理方法

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

個人想法是讓步於開發便利性。
誠然是能夠根據不一樣的場景有選擇性的初始化相應的容器,可是這樣開發人員在使用的時候須要思考的更多,須要作選擇,不夠簡單明瞭。並且省去一兩個容器的初始化帶來的好處能夠並不大。開發

相關文章
相關標籤/搜索