如何用消息系统避免分布式事务?

前阵子从支付宝转账1万块钱到余额宝,这是日常生活的一件普通小事,但作为互联网研发人员的职业病,我就思考支付宝扣除1万之后,如果系统挂掉怎么办,这时余额宝账户并没有增加1万,数据就会出现不一致状况了。

上述场景在各个类型的系统中都能找到相似影子,比如在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减1吧,怎么保证?!在搜索广告系统中,当用户点击某广告后,除了在点击事件表中增加一条记录外,还得去商家账户表中找到这个商家并扣除广告费吧,怎么保证?!等等,相信大家或多或多少都能碰到相似情景。

本质上问题可以抽象为:当一个表数据更新后,怎么保证另一个表的数据也必须要更新成功。

1 本地事务

还是以支付宝转账余额宝为例,假设有

  • 支付宝账户表:A(id,userId,amount)
  • 余额宝账户表:B(id,userId,amount)
  • 用户的userId=1;

从支付宝转账1万块钱到余额宝的动作分为两步:

  • 1)支付宝表扣除1万:update A set amount=amount-10000 where userId=1;
  • 2)余额宝表增加1万:update B set amount=amount+10000 where userId=1;

如何确保支付宝余额宝收支平衡呢?

有人说这个很简单嘛,可以用事务解决。

非常正确,如果你使用spring的话一个注解就能搞定上述事务功能。

如果系统规模较小,数据表都在一个数据库实例上,上述本地事务方式可以很好地运行,但是如果系统规模较大,比如支付宝账户表和余额宝账户表显然不会在同一个数据库实例上,他们往往分布在不同的物理节点上,这时本地事务已经失去用武之地。

既然本地事务失效,分布式事务自然就登上舞台。

2 分布式事务—两阶段提交协议

两阶段提交协议(Two-phase Commit,2PC)经常被用来实现分布式事务。一般分为协调器C和若干事务执行者Si两种角色,这里的事务执行者就是具体的数据库,协调器可以和事务执行器在一台机器上。

1) 我们的应用程序(client)发起一个开始请求到TC;

2) TC先将<prepare>消息写到本地日志,之后向所有的Si发起<prepare>消息。以支付宝转账到余额宝为例,TC给A的prepare消息是通知支付宝数据库相应账目扣款1万,TC给B的prepare消息是通知余额宝数据库相应账目增加1w。为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证 的效果,如果没有本地日志(凭证),出问题容易死无对证;

3) Si收到<prepare>消息后,执行具体本机事务,但不会进行commit,如果成功返回<yes>,不成功返回<no>。同理,返回前都应把要返回的消息写到日志里,当作凭证。

4) TC收集所有执行器返回的消息,如果所有执行器都返回yes,那么给所有执行器发生送commit消息,执行器收到commit后执行本地事务的commit操作;如果有任一个执行器返回no,那么给所有执行器发送abort消息,执行器收到abort消息后执行事务abort操作。

注:TC或Si把发送或接收到的消息先写到日志里,主要是为了故障后恢复用。如某一Si从故障中恢复后,先检查本机的日志,如果已收到<commit >,则提交,如果<abort >则回滚。如果是<yes>,则再向TC询问一下,确定下一步。如果什么都没有,则很可能在<prepare>阶段Si就崩溃了,因此需要回滚。

现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使用java,那么可以使用开源软件atomikos(http://www.atomikos.com/)来快速实现。

不过但凡使用过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合高并发的系统。为什么?

  • 1)两阶段提交涉及多次节点间的网络通信,通信时间太长!
  • 2)事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!

正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,往往通过其他途径来解决数据一致性问题。

3 使用消息队列来避免分布式事务

如果仔细观察生活的话,生活的很多场景已经给了我们提示。

比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。

还是回到我们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转账服务也是如此,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加 1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。

3.1 如何可靠保存凭证(消息)

有两种方法:

3.1.1 业务与消息耦合的方式

支付宝在完成扣款的同时,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(消息记录表表名为message)。

上述事务能保证只要支付宝账户里被扣了钱,消息一定能保存下来。

当上述事务提交成功后,我们通过实时消息服务将此消息通知余额宝,余额宝处理成功后发送回复成功消息,支付宝收到回复后删除该条消息数据。

3.1.2 业务与消息解耦方式

上述保存消息的方式使得消息数据和业务数据紧耦合在一起,从架构上看不够优雅,而且容易诱发其他问题。为了解耦,可以采用以下方式。

1)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;

2)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;

3)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;

4)对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。

优点:消息数据独立存储,降低业务系统与消息系统间的耦合;

缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。

3.2 如何解决消息重复投递的问题

还有一个很严重的问题就是消息重复投递,以我们支付宝转账到余额宝为例,如果相同的消息被重复投递两次,那么我们余额宝账户将会增加2万而不是1万了。

为什么相同的消息会被重复投递?比如余额宝处理完消息msg后,发送了处理成功的消息给支付宝,正常情况下支付宝应该要删除消息msg,但如果支付宝这时候悲剧的挂了,重启后一看消息msg还在,就会继续发送消息msg。

解决方法很简单,在余额宝这边增加消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)。

ebay的研发人员其实在2008年就提出了应用消息状态确认表来解决消息重复投递的问题:http://queue.acm.org/detail.cfm?id=1394128

 

参考文献

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

2 28 收藏 20 评论

关于作者:占利军

阿里巴巴RDC长期招聘资深Java研发工程师&技术专家,有意者私信联系!一个专注于后端架构、算法的工程师 个人经历: 2015 至今 阿里巴巴; 2013-2015 美团; 2010-2013 中科院(硕士); 2006-2010 浙大(本科) 个人主页 · 我的文章 · 37 ·  

相关文章

可能感兴趣的话题



直接登录
最新评论
  • 乒乓球鸡蛋 Java开发工程师 2015/08/13

    阿里的JAVA技术,真的很牛逼!

  • xiao....li JAVA 2015/08/13

    我想问,假如余额宝处理消息事务失败(不管什么问题,就是提交不了),那么,支付宝里的用户少了钱,还一直没出现在余额宝里,会不会跟你急了。

    • 占利军 研发 2015/08/13

      假设你用两阶段分布式事务来解决,也会出现你说的问题。在第二个阶段,假设一边事务提交成功了,而另一边事务没提交成功(比如机器挂了),这时候你还得等机器恢复吧;
      前阵子还有新闻报道某大妈从A银行转账一笔钱给B银行用于炒股,结果A银行早已经扣款了,B银行过了半天都没到帐,影响大妈炒股。
      其实这种延迟在银行支付领域都不鲜见的,很多时候确实出现客户和银行或支付公司急的新闻,但是只要有机制保证凭证在,钱不丢,最终都好办!

      • xiao....li JAVA 2015/08/14

        恩,有道理。传统银行业这方面对安全性高于性能和体验,互联网金融业通过一些技术手段改善,但安全性还是首要的。

      • xiao....li JAVA 2015/08/14

        个人做互联网金融相关,请教一个实际业务场景问题。A网站是一个理财网站,B网站是第三方支付。A网站用户中的账户资金是与B中他充值的资金同步(业内称托管)。A网站用户投资时,A网站采用类似你文章所说的记录消息凭证方式,并冻结相应资金,之后会跳到B网站网关支付划款,支付成功B网站有相关重试接口确认后,消息凭证解锁资金也划拨。但是用户如果各种原因页面没有支付或关闭页面,那么那个冻结的资金只能定时去给解冻,体验很差。
        不知道你有什么好的建议。

        • 占利军 研发 2015/08/14

          “但是用户如果各种原因页面没有支付或关闭页面,那么那个冻结的资金只能定时去给解冻”,这个问题的关键是否能快速解冻,比如B网站支付失败了,这边发送一个解冻的消息,然后马上执行,这样也不会影响用户体验

      • 风中雪 JAVA 2015/08/17

        那如果是订单呢,这边订单提交成功了,那边扣库存的指令发过去了,但是其实库存已经没有了,你这边不晓得没库存,这样这边的事物是一直执行不了的!那接下来的处理措施是:1、和已经下单的客户沟通,退单? 2、等。。好久,新的商品来的,在发货?

        • 占利军 研发 2015/08/17

          一般需要在下订单前去库存那边锁定了,如果库存没有,显然订单都不应该被创建。
          当然了,超售这种事情是比较难以避免的,随便搜索下各种电商的超售相关的微博,挺多的。

          • 风中雪 JAVA 2015/08/18

            其实我想表达的是一种类似场景,而不是具体的订单场景,第二阶段的特殊业务操作是否被允许,要做提前检查吗?先检查下第二阶段的业务能不能执行,不符合条件,第一阶段的业务也不被执行,如果检查了,在检查的时候没问题,但是执行业务的时候,又被操作了,不符合了,该怎么处理?您刚才说的“超卖”的情况,我大概明白思路了,晓得怎么处理了,谢谢大神。。

      • 王海龙   2016/04/25

        请问对于余额宝处理事务,系统自动回滚,还是转人工处理?

  • GCoverage java 2015/08/14

    那弱弱问一下,支付宝现在是采用这种方式的吗还是有其他的方案? 消息应用状态表(message_apply)里是会保存所有的转账消息呢?【这样的话表数据就是累增的】;还是余额宝这边操作成功之后,该条消息是会在 消息应用状态表 中删除吧??

    • 占利军 研发 2015/08/14

      1)支付宝具体采用的方法不清楚哦;
      2)消息应用状态表里的数据可以只保存几天或者几个礼拜的哦,几天前的数据可备份到另外的数据库中,因为几天前的消息一般很少被用到;
      3)需要删除,或设置个状态标示已经成功处理即可

  • 请问时效性有什么好的解决办法么?比如我支付成功了,页面跳转到了我的账户,但是这时账户服务还没有消费掉消息,或者还没处理完,这时账户的金额暂时就会不对

  • 迷恋叮叮   2016/02/23

    1)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;

    到底是“只有提交事务成功后才会消息发送”对还是“只有消息发送成功后才会提交事务”对?
    貌似写反了吧?

    • 王海龙   2016/04/25

      只有提交事务成功后才会消息发送-->指真正发送。根据阿里的消息中间件ons看,本地事务不再拆分执行与提交的,类似TCC方式https://help.aliyun.com/document_detail/ons/tcp/java-sdk/tranc-send.html?spm=5176.7946988.229223.3.Da8SzC

  • 晴天豆 初级架构师 2016/08/19

    支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;

    这段话看得好晕,到现在我还是没有理解到,向实时消息服务请求发送消息、只记录消息数据、而又不真正发送、消息成功发送再提交事务,这是啥意思。实时消息服务这是指MQ吗?还是什么意思?

  • 老兄,如何处理事务消息了,数据库业务数据和mq如何保证事务了,往mq推消息和消费消息 如何保证和mq在同一事务了

  • 分布式事务问题!尤其是在订单业务、资金业务等系统核心业务流程中,一定要有可靠的分布式事务解决方案来保证业务数据的可靠性和准确性。

    分享一篇参考文章以电商购物支付流程中,在各大参与者系统中可能会遇到分布式事务问题的场景进行详细的分析!http://www.roncoo.com/article/detail/124243

  • 请问在3.1.2 的 4) 里提到的消息状态确认系统需要如何设计? 比如支付宝里的事务提交后,此时系统挂了,没有将消息状态更新为确认发送。 之后系统重启后,消息状态确认系统会定时查询那些未确认的消息或者取消的消息,但是如何通过消息的内容去支付宝系统里求证这个事务是否已经完成了提交?这里应该如何设计,请指点一下。 还有一种情况就是如果说支付宝扣了两笔款提交了两次事务但是由于宕机两个消息都没有完成确认,这种情况是否会出现?会不会对消息状态的确认增加困难?

  • 森林X JAVA 04/07

    分布式事务中,能使用消息解决的只是其中一种场景,并非万能,有时还需结合补偿,TCC等手段,参考 https://github.com/QNJR-GROUP/EasyTransaction

跳到底部
返回顶部