目录

  • 一、序言
    • 1、现状
    • 2、问题
  • 二、方案探索
  • 三、根据实际业务进行调整
    • 1、定时补偿扫表改为扫缓存
    • 2、出账异步处理
    • 3、去掉ack_queue
    • 4、入账失败一直重试
  • 四、可能出现的系统瓶颈
    • 1、各地区公账可能会出现抢锁超时
    • 2、出账时异步扣款线程池大小不够用

一、序言

1、现状

最近在做一个跨地区转账的功能,先说一下问题现状,公司业务范围主要分布在新加坡、香港和迪拜,相关交易、卡、账户等数据应各地区监管、合规要求必须分地区物理隔离,关于分库我们选择了中间件Sharding-Proxy,分片键为某个地区的区域码,所有的分片表都会带上区域码这个字段。

如果是同地区转账,动账,交易记录读和写,带上当地区域码,所有的数据库请求都会由Sharding-Proxy路由到该地物理库,所以同地区转账同步在一个事务内跑完其实是没有问题的。

2、问题

如果是跨地区转账,比如从香港到迪拜,或者从新加坡到迪拜,那么问题就来了。虽然Sharding-Proxy本身支持分布式事务,但跨物理库分布式事务之前并没有过实际经验。

同时由于迪拜地区和新加坡、香港地域相隔很远,即使走专线网络传输时间也会比较长,而且转账业务中涉及到两个不同地区多个表的读写,余额变动时也会给出入账账户加上数据库行锁(排它锁),在大事务中很容易超时,同时接口响应慢也会影响用户体验。

于是,我们决定把出账和入账分离,入账通过MQ做成异步处理。

这个时候问题又来了,既然不能通过数据库本地事务保证ACID,那么分布式事务的问题该怎么解决呢?


二、方案探索

开源分布式事务解决方案有Seata,本身提供了ATTCCSAGA还有XA事务模式。但是该方案比较重量级、对外不透明,加上还要部署seata-server,额外增加了维护成本。

最后还是决定选择比较轻量级的解决方案,MQ+本地消息表+重试补发,大致流程如下。

先看出账方,出账记录实际出账本地消息表都在同一个事务,只要出账成功,那么本地消息表中一定会有转账相关的消息。

再看入账方,入账成功后,向ack_queue发送入账成功确认消息。

如果消费成功且入账成功,那么本地消息表中的消息会被逻辑删除,状态置为deleted

如果消息发送失败、消费入账失败,本地消息表中消息状态会一直为undeleted状态,这时出账方会有一个补偿定时任务轮询本地消息表中状态为undeleted的出账消息。为了减少消息积压,当重试到一定次数后,停止发送消息到MQ,同时发送告警邮件给开发人员处理。

注意:入账方消费消息时需要做幂等,否则会重复入账,这里加上分布式锁就可以解决。


三、根据实际业务进行调整

实际我们的系统更加复杂,普通的转账是从A账户到B账户,而我们的转账A账户可能还会共享企业账户的余额。跨地区转账就更复杂了,每个地区都会有一个对公账户,资金变动流程如下:

如果从香港地区的个人账户A转到迪拜地区的个人账户B,个人账户A和个人账户B又分别共享企业账户CACB的余额,那么完整的账户资金变动流程如下:

  1. 企业账户CA自动划账到个人账户A
  2. 个人账户A出账到对公账户PA
  3. 对公账户PB出账到个人账户B
  4. 个人账户B自动划账到企业账户CB

1、定时补偿扫表改为扫缓存

如果是直接轮询本地消息表,由于我们的本地消息表是广播表(PS:各个地区物理库都会有相同的表数据),查询时会随机查询各地区物理库,如果随机查询的是迪拜物理库本地消息表的记录,那么查询就会比较慢了。

因此在写完本地消息表后,我们同时会将本地消息表的记录写到Redis中,后续定时任务补偿直接扫缓存而不扫表。

同时在入账消费者方成功处理入账后,除了逻辑删除本地消息表中的记录(置为deleted),还要删除Redis缓存中的记录。

问题:写本地消息表和写Redis缓存这里可能会出现缓存不一致的情况,如果本地消息表写成功了,但Redis缓存写失败了,那么在做定时任务补偿扫缓存时会丢到该记录,这里缓存一致性需要保证。

2、出账异步处理

可以看到,出账时从企业余额共享账户CA个人账户A对公账户PA,中间会有3个账户的余额变动和出账记录,如果是同步返回,响应时间会比较长,也会影响用户体验,出账操作这里可以通过线程异步。

3、去掉ack_queue

目前系统出账方和入账方并没有跨系统,数据库也没有根据业务做垂直拆分,因此消费者入账成功后可以去掉发确认消息到ack_queue中,直接在消费端操作数据库将本地消息表中的记录状态置为deleted并删除缓存就好。

4、入账失败一直重试

跨地区转账业务这里还比较特别,有可能会出现公账的钱不够扣,导致交易失败。如果转入方公账钱不够扣就会导致入账失败,但转出方实际已经扣款成功了,因此入账操作必须成功,出账方定时任务针对本地消息表中的Pending记录会一直重试,直到入账方公账金额够扣,入账交易成功。

备注:在实际业务中,跨地区转账入账方真正到账是有延迟的,虽然出账和入账操作程序只需要在账面上进行余额的扣和减,但是比如从香港地区到迪拜地区,真正的钱要和当地结算后汇款过去才能到账的。


四、可能出现的系统瓶颈

1、各地区公账可能会出现抢锁超时

目前在每个地区都只设有一个公账账户,而在做跨地区转账时,每次都会操作出账方和入账方的公账余额。而在做余额变动时,将会对指定账户加数据库行锁(排它锁),虽然转账业务并不是很频繁,但是如果并发上来确实会导致过多线程等待数据库行锁释放,可能会出现锁争抢超时。

2、出账时异步扣款线程池大小不够用

由于扣账涉及多个账户的余额变动以及业务表记录,扣账操作响应时间会比较长,所以我们通过线程异步,提升用户体验。同时这里扣张操作核心线程数、阻塞队列长度和最大线程数不是很好把控,还是需要根据实际请求量进行调整,拒绝策略我们设置的是交由主线程执行。