写于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达到瓶颈,如何伸缩?