如果要你实现一个支付宝向余额宝转账的功能,比如:账户a从支付宝转出5000余额宝转入5000,该怎么做呢?
可能有些人会说,这还不简单,直接上图
支付宝先给账户a减5000,调用余额宝的接口给余额宝的账号b加5000。
用这种方式正常情况下是可以的,如果出现以下问题该怎么办呢?
有人说:如果调用余额宝api时网络失败了,对接口进行重试不就可以解决问题了。
答:你是用同步重试,还是异步重试呢?
如果用同步重试,即在调用余额宝api时获取返回值,如果发现失败立刻重试3次。调用一次余额宝api的耗时为n秒,重试3次的耗时则为3n秒,接口响应时间增加了两倍,增加了接口超时的风险。如果重试3次之后,还是失败该怎么处理?
如果用异步重试,第一次调用余额宝api时,不管是成功还是失败,都直接给用户返回成功。如果是失败,后台开启一个线程,不断重试一直到成功为止。如果在不断重试的过程中服务器重启了,该怎么办?
又有人说:如果调用余额宝api时网络超时了,不知道上次请求是成功还是失败,再重试一下不行吗?
答:不是不行,第一.余额宝必须做幂等性设计,不然余额宝这边多转入5000怎么办?余额宝肯定不会犯这种错误。第二.同样会面临如果调用余额宝api时网络失败了的问题。
再有人说:如果余额宝api业务逻辑比较复杂,耗时比较长,用户需要长时间的等待才有结果,用户体验不好。改成异步就可以解决这个问题了。
答:改成异步可以提前告知用户结果,然后在后台通过补偿机制不断的重试,让数据达成最终一致性,这种方式对用户体验可能确实要好一些。异步处理又分为:开启线程 和 使用mq。线程处理有比较致命的弊端,如果服务器重启,线程里的数据会丢失。
接下来,我们的重点放在mq上。
余额宝给账户a减了5000之后,给指定topic1发一条消息,然后余额宝从topic1消费这条消息,给账户b加5000。
对于问题1,如果余额宝处理失败了,比如像rocketmq这类消息处理框架会把消息放入重试队列重试16次,不需要业务代码做额外的工作。
对于问题2,如果服务器重启了,由于消息保存在服务端的磁盘上,不会丢失,客户端可以通过offset从服务端重新获取消息,它能够保证消息至少被余额宝消费一次。
对于问题3,支付宝给账户a减了5000发送完消息之后,可以直接返回成功,然后余额宝作为消费者在后台默默执行,一直到成功为止。
那么问题又来了:
如果余额宝消费了消息,业务处理失败了怎么办?这个就是所谓的消息丢失。
要解决消息丢失就需要建一张消息发送表,如图:
支付宝从账户a减5000,接着往本地消息表中写入一条消息记录,confirm_status为待确认,然后发送mq消息。注意,支付宝这边的扣款和写本地消息表要在同一事务中。
余额宝消费消息给账户b加5000之后,调用支付宝消息确认api,修改confirm_status为已确认。
如果余额宝这边消息丢失了,支付宝有个job会每个5分钟扫描一次本地消息表中confirm_status为待确认状态的记录,重新发送一次消息,这样余额宝又可以重新处理了。
那么还有个问题:
余额宝这边处理成功,但是由于调用 支付宝消息确认api失败,导致支付宝的job重新发送消息,余额宝重复消费了。这个就是所谓的重复消息。
重复消费要如何解决呢?
余额宝也增加一个本地消息表,记录业务处理成功的消息。当然余额宝的账号操作和本地消息表也要在同一个事务中。
余额宝消费消息之后,先从余额宝的本地消息表中查一下,该消息有没有消费过,如果已经消费过了,则直接调用支付宝消息确认api,修改confirm_status为已确认,避免下次支付宝的job重复发消息。如果从余额宝的本地消息表中查到没有消费,则给账户b增加5000,同时往本地消息表写一条记录,然后调用支付宝消息确认api。
总结:通过在mq的生产者和消费者两端分别增加本地消息表,并且在生成者端增加定时job扫描待确认状态的记录,重新发送消息,可以解决:消息丢失 和 重复消费 问题。当然实际的支付宝向余额宝转账的场景更复杂,在高并发的情况下,可能需要用分布式锁,防止金额异常。