在早期的帐户系统中,但凡是有帐户变更,就会执行一次数据库操做。这样在有复杂一些业务操做的时候,例如单笔交易涉及多个用户多个费用的资金划拨,一个事务内操做数据库几十次也就大量的存在。而观察这样的场景,其本质可能只涉及少数几方的帐户。
这时,在一次处理过程当中,合并同一个帐户的全部操做,最后只提交一次,就能带来很大的优化空间。数据库
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
个人想法是让步于开发便利性。
诚然是能够根据不一样的场景有选择性的初始化相应的容器,可是这样开发人员在使用的时候须要思考的更多,须要作选择,不够简单明了。并且省去一两个容器的初始化带来的好处能够并不大。开发