使用消息队列 实现 分布式事务处理

时间:2021-01-07 06:26:50

一、什么情景遇到分布式事务处理问题

以经典的转钱案例:“A帐号向B帐号汇钱”,作为分析案例,一般来说该案例有以下几个过程:

(1)从A帐号中把余额读出来
(2)对A帐号做减法操作
(3)把结果写回A帐号中
(4)从B帐号中把余额读出来
(5)对B帐号做加法操作
(6)把结果写回B帐号中

以上6个步骤必须同时成功,同时失败。在我们的单系统应用中这个问题很好解决,只需要将这一系列操作放入一个事务中即可。
然而在分布式系统中,A账号的扣款,B账号的加钱这两个动作有可能是两组服务独立运行的,而同时我们也需要这两个操作是在同一个事务中进行处理,这样就引入了分布式事务处理的问题。

二、使用消息队列来解决分布式事务

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

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

Begin transaction
update A set amount=amount-10000 where userId=1;

insert into message(userId, amount,status) values(1, 10000, 1);
End transaction
commit;

上述事务能保证只要支付宝账户里被扣了钱,消息一定能保存下来。
当上述事务提交成功后,我们通过实时消息服务将此消息通知余额宝,余额宝处理成功后发送回复成功消息,支付宝收到回复后删除该条消息数据。

1.2 业务与消息解耦方式
上述保存消息的方式使得消息数据和业务数据紧耦合在一起,从架构上看不够优雅,而且容易诱发其他问题。为了解耦,可以采用以下方式。
  1)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送,只有消息发送成功后才会提交事务;
  2)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;
  3)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;
  4)对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。
  优点:消息数据独立存储,降低业务系统与消息系统间的耦合;
  缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口。

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

还有一个很严重的问题就是消息重复投递,以我们支付宝转账到余额宝为例,如果相同的消息被重复投递两次,那么我们余额宝账户将会增加2万而不是1万了。
  为什么相同的消息会被重复投递?比如余额宝处理完消息msg后,发送了处理成功的消息给支付宝,正常情况下支付宝应该要删除消息msg,但如果支付宝这时候悲剧的挂了,重启后一看消息msg还在,就会继续发送消息msg。
  解决方法很简单,在余额宝这边增加消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)。

for each msg in queue
Begin transaction
select count(*) as cnt from message_apply where msg_id=msg.msg_id;

if cnt==0 then
update B set amount=amount+10000 where userId=1;
insert into message_apply(msg_id) values(msg.msg_id);
End transaction
commit;

参考:http://www.cnblogs.com/LBSer/p/4715395.html