起因及介绍
在早期的账户系统中,但凡有账户变动,就会执行一次数据库操作。这样在有复杂一些业务操作的时候,例如单笔交易涉及多个用户多个费用的资金划拨,一个事务内操作数据库几十次也就大量的存在。而观察这样的场景,其本质可能只涉及少数几方的账户。
这时,在一次处理过程中,合并同一个账户的所有操作,最后只提交一次,就能带来很大的优化空间。
处理方法
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. 根据业务需要,进行增、减、转账、冻结、解冻操作。
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。
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. 当所有业务操作完成之后,一次性提交本次处理过程中的所有账户
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的时候,将所有容器都创建出来了,并不是所有业务都会用到全部的容器,这里是否有必要?
我的想法是让步于开发便利性。
诚然是可以根据不同的场景有选择性的初始化相应的容器,但是这样开发人员在使用的时候需要思考的更多,需要做选择,不够简单明了。而且省去一两个容器的初始化带来的好处可以并不大。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。