写于2017-10-19

背景:
交易系统账户服务,存在着A与B之间互相转账的需求,如一笔转账交易
(A+100元,B-100元),A和B其中一方可以为普通用户,也可以为商户,两种角色都可以加款,可以扣款,余额不允许扣成负数。

方案1:
每次按账户ID更新余额表。
为了防止对某账户余额的并发更新,可以采用悲观锁机制,如:

  • 锁定行记录 for update
  • 使用账户Redis分布式锁
  • 使用账户Zookeeper分布式锁
    也可以改进为采用乐观锁机制,如对余额表增加version字段,每次更新时version+1,比如当前版号为5,则sql为:
       update 余额表 set amount = amount + 100 where account_id = xxx and version = 5;

缺点:

  • update并发不高
  • 涉及到A、B转账操作时,假如A是热点账户(商户),则并发冲突急剧增加
    方案2:
    开发童鞋参考了某大型电商库存方案并多次讨论后得出以下架构

缺点:

  • 热点账户余额查询业务上需忍受非实时
  • 热点账户可能被扣成负数(超扣),也可能会扣不完(少扣)
  • 复杂性提升相当高,开发、运维成本陡增

方案3:

查询余额实现:

  select amount from 余额表 where account_id = xxx;
  +
  select sum(amount) from 在途余额表  where is_handle = 0 and account_id = xxx;

可能存在的问题(假设存在以下时序场景):

  select amount from 余额表 where account_id = xxx;
  update 余额表 set amount = amount + 1 where account_id = xxx;
  select sum(amount) from 在途余额表  where is_handle = 0 and account_id = xxx;

即在查询完余额表,在未查询在途额余额表前那一刻,刚好有一次update操作,即数据版本发生了变更,会导致查询出来的余额不准。
待优化方案:增加版本号,保证余额表和在途余额表是同一个版本

  select amount, version from 余额表 where account_id = xxx;
  +
  select sum(amount) from 在途额余额表 where version >= $version and account_id = xxx;

注:
1、在途余额表在insert时使用Long.MAX_VALUE
2、后台定时任务在处理在途额余额表时将当前余额表的version更新到在途余额表的version字段,同时将余额表version+1

总结: 架构设计不能脱离具体的业务场景,技术架构服务于具体业务。另外即使网上找到的适合大厂的方案,也要根据公司现有开发人力、业务量、运维能力等进行综合考量。

遗留问题思考: 假如单表insert达到瓶颈,如何伸缩?

12-24 10:50