前言

对于很多业务系统来说,整个系统其实是由多个独立的系统构成的。这些独立的系统由各自的研发小组进行研发和维护,数据往往也存储在各自独立的数据库中。

我们以下单流程为例。下单往往涉及到订单系统、库存系统、优惠券管理系统、支付系统、物流系统、用户系统等。我们希望下单操作对于这些系统的更新,要么全部成功提交;如果有些步骤失败了,那么所有的改动都能够回滚到最初状态。这个就是我们今天要讨论的分布式事务问题。

本文是分布式事务系列文章的第一篇。主线源自eBay架构师 Dan Pritchett 2008 年发表在 ACM 的文章,Base: An Acid Alternative。
参考:https://zhuanlan.zhihu.com/p/95608046

问题的产生

对于web应用来说,当访问量日益增大,我们就不得不考虑扩容。扩容主要有两个方向,垂直扩容和水平扩容。

垂直扩容,也就是买性能更强大的服务器,更大的存储。垂直扩容很容易遇到瓶颈,所以几乎没有公司采用这种策略。

水平扩容则能够提供更多的灵活性,但同时也会更复杂,对于数据存储来说,水平扩容包含两个维度。

  • 第一个维度是使用不同的数据库来服务不同的业务数据。比如用户数据用一套数据库,产品数据用一套数据库。这种分库方式能完成业务数据的隔离,各业务数据库独立提供服务,相互之间不会有影响。
  • 第二个维度则是将原本属于同一个业务的数据按片存储在不同的数据库上,也就是我们经常提到的sharding(分片)。分片的方式能帮助我们完成最基本的数据库水平扩展。将原本属于单张表的数据分散到多张表,由多个数据库提供服务,增加了并发能力。

水平扩容后,数据分散在不同的数据库中。当我们希望对这些数据的更新操作以事务的方式来执行时,就产生了分布式事务问题。


CAP理论

CAP理论是由Eric Brewer提出来的。

  • Consistency:一致性。写操作后的读操作,必须返回最新写入的值。对于单个节点的系统来说,这点很容易满足。但对于多节点的集群环境来说,如果写操作往节点1写入,而读操作去节点2读取,就无法满足这个要求。
  • Availability:可用性。所有的用户请求都能得到响应。
  • Partition tolerance:分区容错性。即使在某些节点无法响应的情况下,用户操作能让能正确执行。

按照CAP理论,任何系统只能同时满足上面三个特性中的两个。对于水平扩容机制来说,多存储节点的引入已经设定了分区的大前提。所以我们的系统只能在一致性和可用性之间做折衷性原则。对于分布式系统的介绍,请参考另一篇文章:分布式系统简介 by Tim Berglund


ACID方案

ACID数据库事务极大简化了研发人员的工作。它提供了下面的保证:

  • Atomicity。事务中的所有操作要么都成功,要么都失败。
  • Consistency。事务开始前和结束后的数据库状态一致。比如用户A给用户B转账100元,转账前后A账户和B账户总和不变。
  • Isolation。事务之间相互隔离。事务执行时,仿佛当前数据库只有它一个事务在执行。
  • Durability。事务结束后,操作产生的结果不会被回滚。

为了将ACID事务从单一数据库扩展到多数据库。很多数据库供应商引入了2PC机制(两段提交协议)。2PC分为如下两个阶段:

首先,事务协调器会向所有参与当前事务的数据库节点进行问询,看它们是否准备好提交当前事务。如果所有的数据库都准备好了,则进入下一个阶段。事务协调器让所有数据库提交当前事务。如果任何一个数据库节点没能成功提交,则所有已经成功提交的数据库节点都需要回滚本次操作。

2PC有什么缺点呢?

如果CAP理论时对的,我们在得到一致性的同时,肯定会牺牲可用性。一个系统的可用性等于所有子系统可用性的乘积。对于2PC来说,假设有两个数据库参与这个2PC事务,每个数据库的可用性是99.9%,那么整个系统的可用性就变成99.8%了。也就是说数据库越多对于整个系统的可用性越低。


ACID的替代方案BASE

BASE代表的是Basically Available,Soft state,Eventually consistent。BASE和ACID正好相反。ACID强制每个操作后都要满足一致性,BASE则允许出现暂时的不一致。对于一致性的妥协让BASE能够提供比ACID更为强大的扩展性。

那我们如何在具体的项目中来使用BASE呢?

一致性模式
        BASE要求我们不要一直绷着一致性那根弦。我们需要去寻找那些可以适当放松一致性警惕的机会。这些机会往往需要工程师和业务专家一起来寻找,而且我们往往会听到一些反对的声音。比如,一致性对于我们的业务来说至关重要,我们无法向用户隐藏我们的数据不一致等等。下面这个例子可能会给大家一些启发。

假设我们有两张表,user和transaction。user表存储了总的销售量和总的购买量,这些都是统计数据,由transaction表的单笔交易数据统计得来。transaction表则包含了单笔交易。

user表:id
name
amt_sold,总销售量,由transaction的amount统计得来
amt_bought,总购买量,由transaction的amount统计得来
transaction表:xid
seller_id,卖家id
buyer_id,买家id
amount,数量

如果我们使用ACID来确保user表和transaction表的一致性,会比较简单:

begin transactioninsert into transaction(xid, seller_id, buyer_id, amount);//累加卖家的销售量update user set amt_sold = amt_sold + $amount where id = $seller_id; //累加买家的购买量update user set amt_bought = amt_bought + $amount where id = $buyer_id;
end transaction

user表里的总销售量和总购买量,可以看作transaction表的一个缓存。每次查询时,可以直接从user表读取,而不用去transaction统计。基于这样一个前提,我们认为user表里的总销量、总购买量和transaction表里的数据之间的一致性要求可以做一些妥协。

如何修改上面的SQL语句来反映这样一种妥协,依赖于我们如何定义这种统计关系。如果这种统计关系只是一个估计数据,也就是说即使没能准确统计到某些交易也能接受,那我们很容易就能把上面的SQL改成下面的形式。

begin transactioninsert into transaction(xid, seller_id, buyer_id, amount);
end transactionbegin transactionupdate user set amt_sold = amt_sold + $amount where id = $seller_id;update user set amt_bought = amount_bought + $amount where id = $buyer_id;
end transaction

这样一种改动可能会导致user表数据和transaction表数据出现永久性的不一致。当其中一个事务出错而另一个成功提交时就会发生这种情况。

如果我们不希望user表的数据只是一个估算数据呢?我们希望user表的数据能够和transaction表的数据一致。

我们可以引入一个消息队列来解决这个问题。实现这个消息队列会有很多种方式,但其中最重要的一点时,要让消息的入队和transaction的写操作能通过一个本地事务而不是2pc(两段提交协议)来处理,所以最简单的方式就是用一个消息表来存这些消息,这个消息表和transaction在同一个数据库。

begin transactioninsert into transaction(xid, seller_id, buyer_id, amount);//入队卖家数据更新消息,第一个字段是balance,第二个字段是idqueue message "update user("seller", seller_id, amount)";//入队买家数据更新消息,第一个字段是balance,第二个字段是idqueue message "update user("buyer", buyer_id, amount)";
end transactionfor each message in Queuebegin transactiondequeue message; //操作1if message.balance == "seller"update user set amt_sold = amt_sold + messgae.amount where id = message.id; //操作2elseupdate user set amt_bought = amt_bought + message.amount where id = message.id; //操作2end ifend transaction

上面的解决方案仍然会有一个问题,虽然我们让消息的入队和transaction的写操作可以在一个事务中。但消息的出队(操作1)和user表的更新操作(操作2)则仍然会面临2pc(两段提交协议)问题,因为user表在另外一个数据库。(transaction和user表因为水平扩展分属不同的数据库)

在介绍具体解决方案之前,我们介绍下幂等性。对于一个操作,如果执行一次和执行多次的结果一样,我们就说这个操作具有幂等性。幂等性操作的一个优点就是它们允许部分失败,对于这种情况,重复执行该操作并不会改变系统的最终状态。

对于上面这个例子,从幂等性角度来看是有问题的。上面的更新操作,每次会给总销售量和总购买量增加一笔交易数据,如果重复执行这个操作,显然会得到错误的结果。

要解决这个问题,关键点是要能够记录哪些交易记录已经反映到user表了,哪些还没有。

这里我们引入一张updates_applied表来跟踪这个更新操作。

update_applied表:trans_id, 对应于transaction.xid
balance, seller or buyer
user_id, 对应于user.id

然后我们重写上面的逻辑:

begin transactioninsert into transaction(xid, seller_id, buyer_id, amount);//入队卖家数据更新消息,第一个字段是balance,第二个字段是idqueue message "update user("seller", seller_id, amount)"; //入队买家数据更新消息,第一个字段是balance,第二个字段是idqueue message "update user("buyer", buyer_id, amount)";
end transactionfor each message in queuepeek messagebegin transaction//如果当前消息在update_applied表中有记录,说明已经处理过了,否则没有处理过select count(*) from update_applied as processed where trans_id = message.trans_id and balance = message.balance and user_id = message.user_id; if proccessed == 0if message.balance == "seller"update user set amt_sold = amt_sold + messgae.amount where id = message.id; //操作2elseupdate user set amt_bought = amt_bought + message.amount where id = message.id; //操作2end if//插入记录,表示这条消息处理过了insert into updates_applied(message.trans_id, message.balance, message.id);end ifend transactionif transaction successful//如果上面的事务成功处理,则把消息从队列删除。这个操作执行失败也没关系,上面的事务已经变成幂等操作了remove message from queue; 。end if
end for

上面的peek message操作需要说明下,如果这个队列的实现方案是用和transaction表一样的数据库表,那这个peek操作就是一个数据库读操作,最后的出队操作就是一个数据库删除操作。而如果队列的解决方案是其它方式,则需要这种解决方案支持peek操作,也就是可以查看数据,但不会把数据从队列移除。

上面的方案已经解决了user数据库和transaction数据库之间的一致性问题。但在某些情况下,上面的方案还会存在一些问题。

对于user表,假如我们不光是要记录总的销售量和总的购买量,我们还想记录最后一次购买或者销售的发生时间。

我们给user表增加两个字段。

user表:id
name
amt_sold,总销售量
amt_bought,总购买量
last_sale,最后一次销售时间
last_purchage,最后一次购买时间
transaction表:xid
seller_id,卖家id
buyer_id,买家id
amount,数量

对于这种情况,上面的解决方案就会存在一个问题,那就是oder的处理顺序不同可能会导致user表的更新操作不再幂等。对于两笔发生时间非常接近的交易,在上面的消息查询操作不能保证消息顺序的情况下,消息可能会乱序执行。那么last_sale和last_purchage的值就可能会错乱。

有两种方案可以处理这个问题:

1. 限制xid递增,处理的时候递增进行处理。更新user表时,限制transaction的时间必须要大于user的最后一次时间,才能执行更新操作。
        2.采用第二种方案对上述逻辑修改如下:

begin transactioninsert into transaction(xid, seller_id, buyer_id, amount);//入队卖家数据更新消息,第一个字段是balancequeue message "update user("seller", seller_id, amount)"; //入队买家数据更新消息queue message "update user("buyer", buyer_id, amount)";
end transaction
for each message in queuepeek messagebegin transaction//如果当前消息在update_applied表中有记录,说明已经处理过了,否则没有处理过select count(*) from update_applied as processed where trans_id = message.trans_id and balance = message.balance and user_id = message.user_id; if proccessed == 0if message.balance == "seller"//增加了last_purchase和trans_date的比较update user set amt_sold = amt_sold + messgae.amount, last_purchase = message.trans_datewhere id = message.seller_id and last_purchase < message.trans_dateelse//增加了last_sale和trans_date的比较update user set amt_bought = amt_bought + message.amount, last_sale = message.trans_datewhere id = message.buyer_id and last_sale < message.trans_dateend if//插入记录,表示这条消息处理过了insert into updates_applied(message.trans_id, message.balance, message.user_id);end ifend transactionif transaction successful//如果上面的事务成功处理,则把消息从队列删除。如果这个操作失败也没关系,上面的事务已经变成幂等操作了remove message from queue; 。end if
end for

当然,这种方案也可能会有一个小问题,就是如果后发生的交易先被处理,那先前的交易就可能会被漏掉,而不会体现在user表中了。


柔性事务/最终一致性

截至目前,我们的讨论主要还是聚焦在如何牺牲一定的一致性来换取可用性。而另一方面,我们需要理解柔性事务和最终一致性对于系统设计的影响。

作为软件工程师,我们习惯于将我们自己的系统看作一个闭环。我们会考虑这个闭环系统的可预测性,亦即可预测的输入产生可预测的输出。这一点对于构建正确的软件系统时很必要的。好消息是,BASE并不会改变软件的可预测性。

比如,考虑一个转账系统,用户可以把一笔钱转给另一个用户。而且基于我们之前的讨论,付款方和收款方可能是不同的银行,所以我们没办法用ACID来处理这个事务。

可能会存在一个时间窗口,钱已经转出去了,但没有到账。在这个时间窗口内,两个用户都不持有这笔钱。

但从用户的角度来考虑,这个延迟是可以接受并且对用户可能是不可见的,除非付款方和收款方做实时沟通。否则他们挺难感知到付款时间和收款时间之间的时差。


事件驱动

假如我们一定要知道什么时候状态变成最终一致了呢?比如我们希望在到账时,给用户发送一条到账短信。一个比较简单的方式就是采用event driven。对于上面的例子,我们只需要在收款方账户被更新的事务里,同时创建一条event,比如插入event表,然后就可以用这个条event来驱动后续的短信发送流程了。

今天关于BASE的介绍就到这里,希望能给大家带来一些帮助。

参考:https://queue.acm.org/detail.cfm?id=1394128

分布式事务系列一:BASE,一种ACID的替代方案(eBay分布式事务解决方案)相关推荐

  1. Base:一种 Acid 的替代方案

    原文链接: BASE: An Acid Alternative 数据库 ACID,都不陌生:原子性.一致性.隔离性和持久性,这在单台服务器就能搞定的时代,很容易实现,但是到了现在,面对如此庞大的访问量 ...

  2. 【分布式事务系列九】聊聊分布式事务

    为什么80%的码农都做不了架构师?>>>    #0 系列目录# 分布式事务 [分布式事务系列一]提出疑问和研究过程 [分布式事务系列二]Spring事务管理器PlatformTra ...

  3. 强势解析 eBay BASE 模式、去哪儿及蘑菇街分布式架构

    互联网行业是大势所趋,从招聘工资水平即可看出,那么如何提升自我技能,满足互联网行业技能要求?需要以目标为导向,进行技能提升. 本文主要针对分布式系统设计.架构(数据一致性)做了分析,祝各位早日走上属于 ...

  4. 强势解析eBay BASE模式、去哪儿及蘑菇街分布式架构

    互联网行业是大势所趋,从招聘工资水平即可看出,那么如何提升自我技能,满足互联网行业技能要求?需要以目标为导向,进行技能提升,本文主要针对高并发分布式系统设计.架构(数据一致性)做了分析,祝各位早日走上 ...

  5. 解析eBay BASE模式、去哪儿及蘑菇街分布式架构

    目录: 问题分析 概念解读 Most Simple原理解读 eBey.去哪儿.蘑菇街分布式事务案例分析 参考资料 1.问题解析     要想做架构,必须识别出问题,即是谁的问题,什么问题. 明显的,分 ...

  6. 不懂这些高并发分布式架构、分布式系统的数据一致性解决方案,你如何能找到高新互联网工作呢?强势解析eBay BASE模式、去哪儿及蘑菇街分布式架构...

    互联网行业是大势所趋,从招聘工资水平即可看出,那么如何提升自我技能,满足互联网行业技能要求?需要以目标为导向,进行技能提升,本文主要针对高并发分布式系统设计.架构(数据一致性)做了分析,祝各位早日走上 ...

  7. 谷粒商城项目篇13_分布式高级篇_订单业务模块(提交订单幂等性、分布式事务、延时MQ实现定时任务)

    目录 一.订单业务模块 订单流程 购物车跳转订单确认页 登录拦截器 封装vo Feign远程调用丢失请求头信息 Feign远程异步调用丢失上下文信息 提交订单接口幂等性 令牌token机制 各种锁机制 ...

  8. 事务例子_Redis事务系列之一Redis事务详解

    一.前言 本章是redis事务系列知识第一章,redis事务系列主要讲解以下内容: redis 事务 redis乐观锁讲解 redis乐观锁实现秒杀 我们一步一步来,本章主要讲解事务. 二.事务 2. ...

  9. 分布式事务系列02--分布式事务定义,理论基础--CAP,BASE,酸碱平衡

    https://blog.csdn.net/u010425776/article/details/79516298 目录 一.什么是分布式事务? 二.CAP理论 三.BASE理论 酸碱平衡----AC ...

最新文章

  1. EL:谁说N素含量高就不固氮了(本研究反而“多多益善”)
  2. java poi 如何合并多个sheet 为一个sheet_Java POI组件实现多个Excel文件整合成一个多Sheet的Excel文件...
  3. 我如何想成为Java
  4. 定时任务 Scheduled quartz
  5. navicat 的查询功能
  6. Mysql之inner join,left join,right join详解
  7. 【java】统计英文文本中某些字母出现的次数
  8. C语言中整型常量的表达方式
  9. iOS开发之Mac安装软件时,xx.app文件损坏,无法安装,解决方法,亲测可用
  10. 中心极限与大数定理律的关系_中心极限定理的最最通俗解释
  11. 第一、二章 引论、算法分析
  12. 计算机科学数学背景,计算机科学中的数学教育.pdf
  13. ipad一直卡在白苹果_近万字多图带你玩转iPad——iPad指南
  14. exchange服务器维护,EXCHANGE故障排除步骤简述
  15. RNA-seq数据分析
  16. TMS570捕获多路PWM的可行性
  17. MKR:协同过滤算法效果不佳,知识图谱来帮忙
  18. led灯条维修_LED灯坏了别着急,三种故障的解决方案在这里
  19. 虚拟机使用的是此版本 VMware Workstation 不支持的硬件版本。 模块“Upgrade”启动失败。 未能启动虚拟机。
  20. [8th of series6] Step1: Label Each Block’s Mining Pool

热门文章

  1. 苏科大的四年里,我都学会了啥?
  2. Python程序员爬出百套美女写真集,同样是爬虫,他为何如此突出?
  3. 怎么把照片变年轻?这两个照片变年轻小妙招教给你
  4. 2023最新SSM计算机毕业设计选题大全(附源码+LW)之java面向中小学生的植物科普网站6s4f9
  5. 华为云sql工程师评测答题[青铜+白银]
  6. 关于iOS推送中点击通知的几点备忘
  7. c语言逐语句调试和逐过程调试,逐语句调试和逐过程调试的区别
  8. routeDone with a webviewId 12 that is not the current page(env: Windows,mp,1.06.2301040; lib: 2.30.0
  9. r1笔记第9天 逻辑英语随堂笔记 (01)
  10. VScode常用插件下载