分布式事务的解决方案(一)

前言应用场景

事务必须满足传统事务的特性,即原子性,一致性,分离性和持久性。但是分布式事务处理过程中,

某些场地比如在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减1吧,怎么保证?

在搜索广告系统中,当用户点击某广告后,除了在点击事件表中增加一条记录外,
还得去商家账户表中找到这个商家并扣除广告费吧,怎么保证?

一 本地事务
以用户A转账用户B为例,假设有

  用户A账户表:A(id,userId,amount)  

  用户B账户表:B(id,userId,amount)

  用户的userId=1;

从用户A转账1万块钱到用户B的动作分为两步:

  1)用户A表扣除1万:update A set amount=amount-10000 where userId=1;

  2)用户B表增加1万:update B set amount=amount+10000 where userId=1;

  如何确保用户A用户B收支平衡呢?有人说这个很简单嘛,可以用事务解决。

1
2
3
4
5
<span style= "color: #000000;" >Begin transaction
update A  set  amount</span>=amount-10000  where  userId=1<span style= "color: #000000;" >;
update B  set  amount</span>=amount+10000  where  userId=1<span style= "color: #000000;" >;
End transaction
commit;</span>

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

1
2
3
4
5
@Transactional(rollbackFor=Exception. class )
public  void  update() {
updateATable();  //更新A表
updateBTable();  //更新B表
}

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

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


使用消息队列来避免分布式事务
  如果仔细观察生活的话,生活的很多场景已经给了我们提示。
  比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,往往是给你一张小票,然后让你拿着小票到出货区排队去取。
为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。

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

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

  有两种方法:

4.1.1 业务与消息耦合的方式

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

1
2
3
4
5
<span style= "color: #000000;" >Begin transaction
update A  set  amount</span>=amount-10000  where  userId=1<span style= "color: #000000;" >;
insert  into  message(userId, amount,status) values(</span>1, 10000, 1<span style= "color: #000000;" >);
End transaction
commit;</span>

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

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

4.1.2 业务与消息解耦方式

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

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

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
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;

  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值