张伦聪的技术博客 Research And Development

分布式事务

2018-05-08

某些场地比如在电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须减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收支平衡呢?有人说这个很简单嘛,可以用事务解决。

Begin transaction
    update A set amount</span>=amount-10000 where userId=1;
    update B set amount</span>=amount+10000 where userId=1;
End transaction
commit;

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

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

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

两阶段提交协议

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

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

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

如果仔细观察生活的话,生活的很多场景已经给了我们提示。 比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。 还是回到我们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转账服务也是如此,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加 1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。

我们从一个简单的实例入手. 基本所有互联网应用都会有用户注册的功能. 在这个例子中, 我们对于用户注册有两步操作:

  1. 注册成功, 保存用户信息.
  2. 需要给用户发放一张代金券, 目的是鼓励用户进行消费. 如果是一个单一架构应用, 实现这个功能非常简单: 在一个本地事务里, 往用户表插一条记录, 并且在代金券表里插一条记录, 提交事务就完成了. 但是如果我们的应用是用微服务实现的, 可能用户和代金券是两个独立的服务, 他们有各自的应用和数据库, 那么就没有办法简单的使用本地事务来保证操作的原子性了. 现在来看看如何使用事件机制和消息队列来实现这个需求.(我在这里使用的消息队列是kafka, 原理同样适用于ActiveMQ/RabbitMQ等其他队列) 我们会为用户注册这个操作创建一个事件, 该事件就叫做用户创建事件(USER_CREATED). 用户服务成功保存用户记录后, 会发送用户创建事件到消息队列, 代金券服务会监听用户创建事件, 一旦接收到该事件, 代金券服务就会在自己的数据库中为该用户创建一张代金券. 好了, 这些步骤看起来都相当的简单直观, 但是怎么保证事务的原子性呢?增加状态记录表(状态机)。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏博主

上一篇 分布式锁

下一篇 b树和b+树

留言