资讯专栏INFORMATION COLUMN

账户变动合并提交方案

MoAir / 3121人阅读

摘要:起因及介绍在早期的账户系统中,但凡有账户变动,就会执行一次数据库操作。这时,在一次处理过程中,合并同一个账户的所有操作,最后只提交一次,就能带来很大的优化空间。根据业务需要,进行增减转账冻结解冻操作。

起因及介绍

在早期的账户系统中,但凡有账户变动,就会执行一次数据库操作。这样在有复杂一些业务操作的时候,例如单笔交易涉及多个用户多个费用的资金划拨,一个事务内操作数据库几十次也就大量的存在。而观察这样的场景,其本质可能只涉及少数几方的账户。
这时,在一次处理过程中,合并同一个账户的所有操作,最后只提交一次,就能带来很大的优化空间。

处理方法

1. 初始化一个收集器ExecuteParam,用来存放有变动的账户、待新增的资金记录、待处理的冻结数据和待新增的冻结记录。

</>复制代码

  1. final ExecuteParam param = ExecuteParam.instance();
  2. public class ExecuteParam {
  3. private final Map cache = Maps.newHashMap();
  4. private final List financeLogs = Lists.newArrayList();
  5. private final Map freezeRecords = Maps.newHashMap();
  6. private final List freezeHistorys = Lists.newArrayList();
  7. public static ExecuteParam instance() {
  8. return new ExecuteParam();
  9. }
  10. public Map getCache() {
  11. return cache;
  12. }
  13. public List getFinanceLogs() {
  14. return financeLogs;
  15. }
  16. public Map getFreezeRecords() {
  17. return freezeRecords;
  18. }
  19. public List getFreezeHistorys() {
  20. return freezeHistorys;
  21. }
  22. }

2. 根据业务需要,进行增、减、转账、冻结、解冻操作。

</>复制代码

  1. public interface FundTransactionService {
  2. /** 调增 */
  3. void addCredit(TransactionCommandParam command, final ExecuteParam param);
  4. /** 调减 */
  5. void addDebit(TransactionCommandParam command, final ExecuteParam param);
  6. /** 转账 */
  7. void addTransfer(TransactionCommandParam command, final ExecuteParam param);
  8. /** 冻结 */
  9. String addFreeze(TransactionCommandParam command, final ExecuteParam param);
  10. /** 解冻 */
  11. BigDecimal addUnfreeze(TransactionCommandParam command, final ExecuteParam param);
  12. /** 更新DB */
  13. void execute(String proofId, ExecuteParam param);
  14. }
  15. public static TransactionCommandParam createTransfer(...);
  16. public static TransactionCommandParam createFreeze(...);
  17. public static TransactionCommandParam createUnfreeze(...);
  18. public static TransactionCommandParam createCredit(...);
  19. public static TransactionCommandParam createDebit(...);

3. 所有资金操作在底层都按照:校验操作类型->修改账户余额->资金记录的流程执行

</>复制代码

  1. @Override
  2. public void addCredit(TransactionCommandParam command, final ExecuteParam param) {
  3. /** 1.校验 */
  4. /** 2.调账 */
  5. FinanceAccount receiverFa = credit(command.getReceiverOwnerId(), command.getReceiverRoleId(), command.getAmount(), param.getCache());
  6. /** 3.资金记录 */
  7. param.getFinanceLogs().add(...);
  8. }

4. 其中修改账户余额的方法,会先尝试从ExecuteParam中查找该账户是否已经被操作过,如果没有才查询一次DB。这样就确保了同一个账户在一次处理过程中,无论有多少资金操作,只会查询一次DB。

</>复制代码

  1. private FinanceAccount credit(Long ownerId, Long roleId, BigDecimal amount, Map cache) {
  2. final String cacheKey = getCacheKey(ownerId, roleId);
  3. FinanceAccount fa = cache.get(cacheKey);
  4. if (fa == null) {
  5. // 此处只查询一次DB
  6. fa = getFinanceAccount(ownerId, roleId);
  7. cache.put(cacheKey, fa);
  8. }
  9. // 调增:
  10. fa.credit(amount);
  11. return fa;
  12. }

5. 当所有业务操作完成之后,一次性提交本次处理过程中的所有账户

</>复制代码

  1. fundTransactionService.execute(proof.getProofId(), param);
  2. @Override
  3. public void execute(String proofId, ExecuteParam param) {
  4. /** FinanceAccount统一更新 */
  5. for (FinanceAccount account : param.getCache().values()) {
  6. account.setProofId(proofId);
  7. // 热点账户延迟更新
  8. if (isHotAccount(account.getId())) {
  9. continue;
  10. }
  11. // DB update
  12. this.updateAccount(account);
  13. logger.info("账户更新[{}]", account);
  14. }
  15. /** FinanceLog统一批量记录 */
  16. financeLogDao.addFinanceLog(param.getFinanceLogs());
  17. /** 冻结记录统一批量更新 */
  18. for (AccFundManagementRecord freezeRecord : param.getFreezeRecords().values()) {
  19. if (freezeRecord.getId() != null) {
  20. // DB update
  21. } else {
  22. // DB insert
  23. }
  24. logger.info(LoggerUtil.createInfoLog("execute","冻结记录[{}]"), freezeRecord);
  25. }
  26. /** 冻结历史统一批量更新 */
  27. for (AccFundManagementHistory history : param.getFreezeHistorys()) {
  28. // DB insert
  29. }
  30. }
总结和思考

这次优化不仅大幅减少了数据库的负担,而且也因为数据库访问次数少了,处理速度也快了(例如还款,原先的处理时间约为1到2s,优化后的处理时间约为40ms)。处理速度快了,使用乐观锁控制的并发异常也相应减少了。

另外值得思考的地方是,在第一步初始化收集器ExecuteParam的时候,将所有容器都创建出来了,并不是所有业务都会用到全部的容器,这里是否有必要?

我的想法是让步于开发便利性。
诚然是可以根据不同的场景有选择性的初始化相应的容器,但是这样开发人员在使用的时候需要思考的更多,需要做选择,不够简单明了。而且省去一两个容器的初始化带来的好处可以并不大。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77292.html

相关文章

  • 热点账户解决方案

    摘要:全局热点账户平台支出账户平台收入账户过渡账户。。那么亲,这和热点账户没关系,即使你查询一个非常普通的账户,碰巧该账户同时在更新,你也查不准。。 问题描述 在某一瞬间,单个账户集中的发生资金变动,若不加控制,其账户余额会因发生脏读、覆盖更新等情况而错误记录。如果简单的以悲观锁、乐观锁的方式限制,虽然不会发生数据错误,但会造成服务不可用(该账户的更新请求全部失败)。而请求重试、再次网络通信...

    MycLambert 评论0 收藏0
  • 【强烈推荐】程序猿开发工具(第二期)

    摘要:这允许开发人员以逻辑区间建立并提交变动,以防止当部分提交成功时出现的问题纳入版本控管的元数据每一个文件与目录都附有一組属性关键字并和属性值相关联。 代码管理 Git...

    KevinYan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<