分布式事务解决方案Part2


分布式事务解决方法part2

设计微服务遵循什么原则

  1. 单一职责原则:让每个服务能独立,有界限的工作,每个服务只关注自己的业务。做到高内聚。
  2. 服务自治原则:每个服务要能做到独立开发、独立测试、独立构建、独立部署,独立运行。与其他服务进行解耦。
  3. 轻量级通信原则:让每个服务之间的调用是轻量级,并且能够跨平台、跨语言。比如采用RESTful风格,利用消息队列进行通信等。
  4. 粒度进化原则:对每个服务的粒度把控,其实没有统一的标准,这个得结合我们解决的具体业务问题。不要过度设计。服务的粒度随着业务和用户的发展而发展。

总结一句话,软件是为业务服务的,好的系统不是设计出来的,而是进化出来的。

CAP定理

概念

  • C:一致性(Consistency) ,数据在多个副本节点中保持一致,可以理解成两个用户访问两个系统A和B,当A系统数据有变化时,及时同步给B系统,让两个用户看到的数据是一致的。
  • A:可用性(Availability) ,系统对外提供服务必须一直处于可用状态,在任何故障下,客户端都能在合理时间内获得服务端非错误的响应。
  • P:分区容错性(Partition tolerance) ,在分布式系统中遇到任何网络分区故障,系统仍然能对外提供服务。网络分区,可以这样理解,在分布式系统中,不同的节点分布在不同的子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,造成整个节点环境被切分成了不同的独立区域,这就是网络分区。

CAP定理:指的是:一个分布式系统最多只能同时满足C一致性(Consistency) 、A可用性(Availability)和P分 区容错性(Partition tolerance)这三项中的两项。

原理解释

来分析下CAP,为什么只能满足两个

用户1和用户2分别访问系统A和系统B,系统A和系统B通过网络进行同步数据。理想情况是:用户1访问系统,A对数据进行修改,将data1改成了data2,同时用户2访问系统B,拿到的是data2数据。

但是实际中,由于分布式系统具有八大谬论:

  • 网络相当可靠
  • 延迟为零
  • 传输带宽是无限的
  • 网络相当安全
  • 拓扑结构不会改变
  • 必须要有一名管理员
  • 传输成本为零
  • 网络同质化

我们知道,只要有网络调用,网络总是不可靠的。我们来一分析。

  1. 当网络发生故障时,系统A和系统B没法进行数据同步,也就是我们不满足P,同时两个系统依然可以访问,那么此时其实相当于是单机系统,就不是分布式系统了,所以既然我们是分布式系统, P必须满足。
  2. 当P满足时,如果用户1通过系统A对数据进行了修改将data1改成了data2,也要让用户2通过系统B正确的拿到data2,那么此时是满足C,就必须等待网络将系统A和系统B的数据同步好,并且在同步期间,任何人不能访问系统B (让系统不可用) ,否则数据就不是一致的。此时满足的是CP。(保证了一致性+分区)
  3. 当P满足时,如果用户1通过系统A对数据进行了修改将data1改成了data2,也要让系统B能继续提供服务,那么此时,只能接受系统A没有将data2同步给系统B (牺牲了一致性) 。此时满足的就是AP。(保证了可用性+分区)

BASE理论

由于CAP中一致性C和可用性A无法兼得, eBay的架构师,提出了BASE理论,它是通过牺牲数据的强一致性,来获得可用性。它由于如下3种特征:

  • Basically Available (基本可用) :分布式系统在出现不可预知故障的时候,允许损失部分可用性,保证核心功能的可用。
  • Soft state (软状态) :软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
  • Eventually consistent (最终一致性) :最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

BASE理论并没有要求数据的强一致性,而是允许数据在一定的时间段内是不一致的,但在最终某个状态会达到一致。在生产环境中,很多公司,会采用BASE理论来实现数据的一致,因为产品的可用性相比强一致性来说更加重要。比如在电商平台中,当用户对一个订单发起支付时,往往会调用第三方支付平台,比如支付宝支付或者微信支付,调用第三方成功后,第三方并不能及时通知我方系统,在第三方没有通知我方系统的这段时间内,我们给用户的订单状态显示支付中,等到第三方回调之后,我们再将状态改成已支付。虽然订单状态在短期内存在不一致,但是用户却获得了更好的产品体验

2PC提交协议

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了便基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm),通常,二阶段提交也被称为是一种协议(Protocoll),在分布式系统中,每个节点虽然可以知晓自己的操作是成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等),因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作,还是中止操作。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)

准备阶段

事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种万事俱备,只欠东风”的状态。

可以进一步将准备阶段分为以下三个步骤:

  1. 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
  2. 参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这 里其实每个参与者已经执行了事务操作)
  3. 各参与者节点响应协调者节点发起的询问,如果参与者节点的事务操作实际执行成功,则它返回一个同意消息;如果参与者节点的事务操作实际执行失败,则它返回一个中止消息

总结:
第一阶段(prepare):事务管理器向所有本地资源管理器发起请求,询问是否是 ready 状态,所有参与者都将本事务能否成功的信息反馈发给协调者;
第二阶段 (commit/rollback):事务管理器根据所有本地资源管理器的反馈,通知所有本地资源管理器,步调一致地在所有分支上提交或者回滚。

提交/回滚操作

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;
否则,发送提交(commit)消息;
参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程。
当协调者节点从所有参与者节点获得的相应消息都为”同意”时;

  1. 协调者节点向所有参与者节点发出正式提交(commity”的请求。
  2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送”完成”消息
  4. 协调者节点受到所有参与者节点反馈的”完成消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时.

  1. 协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
  2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
  3. 参与者节点向协调者节点发送”回滚完成”消息
  4. 协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。

不管最后结果如何,第二阶段都会结束当前事务。

2PC缺点

  1. 同步阻塞问题,执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。(1pc准备阶段,只执行sql,不提交,并且占用数据库连接资源)
  2. 单点故障,由于协潮者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
  3. 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
  4. 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也容机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

处理方法:补偿,手动或者脚本补偿

3PC提交协议

CanCommit阶段 3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  1. 事务询问协调者申参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  2. 响应反馈参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作,根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

  1. 发送预提交请求协调者向参与者发送PreCommit请求,并进入Prepared阶段
  2. 事务预提交参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志
  3. 响应反馈如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之局 协调者都没有接到参与者的响应,那么就执行事务的中断。

  1. 发送中断请求协调者向所有参与者发送abort请求。
  2. 中断事务参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求) ,执行事务的中断。

pre阶段参与者没收到请求, rolback。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交

  1. 发送得交请求协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
  2. 事务提交参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
  3. 响应反馈事务提交完之后,向协洞者发送Ack响应
  4. 完成事务协调者接收到所有参与者的ack响应之后,完成事务。

中断事务协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时) ,那么就会执行中断事务。

  1. 发送中断请求协调者向所有参与者发送abort请求
  2. 回滚参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
  3. 反馈结果参与者完成事务回滚之后,向协调者发送ACK消息
  4. 中断事务协洞者接收到参与者反馈的ACK消息之后,执行事务的中断。

2PC与3PC区别

  1. 3pct2pc多了一个can commit阶段,减少了不要的资源浪费 因为2pc在第一阶段会占用资源,而3pc在这 个阶段不占用资源,只是校验一下sql,如果不能执行,就直接返回,减少了资源占用。
  2. 引入超时机制。同时在协调者和参与者中都引入超时机制。

2pc:只有协调者有超时机制,超时后,发送回滚指令。

3pc:协调者和参与者都有超时机制。

协调者超时:发送中断指令。 参与者超时: pre阶段进行中断,do阶段进行提交

TCC解决方案

TCC (Try-Confirm-Cancel)是一种常用的分布式事务解决方案,它将一个事务拆分成三个步骤:

  • T (Try) :业务检查阶段,这阶段主要进行业务校验和检查或者资源预留;也可能是直接进行业务操作。
  • C (Confirm) :业务确认阶段,这阶段对Ty阶段校验过的业务或者预留的资源进行确认。
  • C (Cancel) :业务回滚阶段,这阶段和上面的C (Confirm)是互斥的,用于释放Try阶段预留的资源或者业务。

TCC的空回滚

在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法。比如当Try请求由于网络延迟或故障等原因,没有执行,结果返回了异常,那么 时Cancel就不能正常执行,因为Try没有对数据进行修改,如果Cancel进行了对数据的修改,那就会导致数据不一致。

解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道Try阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚。建议TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,再额外增加一张分支事务记录表,其中有全局事务ID和分支事务ID,第一阶段Try方法里会插入祭记录,表示Try阶段执行了. Cancel接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚。

TCC幂等问题

为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC的二阶段Confirm和Cancel接口保证幕等,这样不会重复使用或者程放资源。如果幕等控制没有做好,很有可能导致数据不一致等严重问题。

解决思路在上述分支事务记录中增加执行状态,每次执行前都查询该状态。

TCC的悬挂问题

悬挂就是对于一个分布式事务,其二阶段Cancel接口比Try接口先执行。

出现原因是在调用分支事务Try时,由于网络发生拥堵,造成了超时, TM就会通知RM回滚该分布式事务,可能回滚完成后, Try请求才到达参与者真正执行,而一个Try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后无法继续处理。

解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,判断分支事务记录表中是否已经有二阶段事务记录,如果有则不执行Try.

可靠消息服务方案

可靠消息最终一致性方案指的是:当事务的发起方(事务参与者,消息发送者)执行完本地事务后,同时发出一条消息,事务参与方(事务参与者,消息的消费者)一定能够接受消息并可以成功处理自己的事务。 这里面强调两点:

  1. 可靠消息:发起方一定得把消息传递到消费者。
  2. 最终一致性:最终发起方的业务处理和消费方的业务处理得完成,达成最终一致。

最大努力通知方案的关键

  1. 有一定的消息重复通知机制,因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
  2. 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。

分布式系统中的幂等

幕等(idempotent, idempotence)是一个数学与计算机学概念,常见于抽象代数中,

在编程中,一个幕等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幕等函数,或幕等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

例如, “getUsername()和setTrue()”函数就是一个幕等函数,更复杂的操作幕等保证是利用唯一交易号(流水号实现我的理解:幕等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的

幂等性的解决方案

1.查询操作

查询一次和查询多次,在数据不变的情况下,查询结果是一样的。 select是天然的幕等操作;

2.删除操作

删除操作也是幕等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个。

3.唯一索引

防止新增脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建多个资金账户,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可。

4.token机制

防止页面重复提交。

业务要求:页面的数据只能被点击提交一次;

发生原因:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交;

解决办法:集群环境采用token加redistredis单线程的,处理需要排队);单JVM环境:采用token加redis或 token加jvm锁。

处理流程:

  1. 数据提交前要向服务的申请token, token放到redis或jvm内存, token有效时间;
  2. 提交后后台校验token,同时删除token,生成新的token返回。

token特点:要申请,一次有效性,可以限流

注意: redis要用删除操作来判断token,删除成功代表token校验通过。

对外提供的API如何保证幂等性

举例说明:银联提供的付款接口:需要接入商户提交付款请求时附带: source来源, seq序列号。

source-seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求)

重点:对外提供接口为了支持幕等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。

注意,为了幕等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理。

认证和授权的区别

Authentication (认证)是验证您的身份的凭据(例如用户名/用PID和密码) ,通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以, Authentication被称为身份/用户验证。

Authorization (授权)发生在Authentication (认证)之后。授权,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。

这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。

cookie和session区别?如何使用session身份验证?

Session的主要作用就是通过服务端记录用户的状态。

Cookie数据保存在客户端(浏览端), Session数据保存在服务器端。相对来说Session安全性更高。如果使用Cookie的话,一些敏感信息不要写入Cookie中,最好能将Cookie信息加密然后使用到的时候再去服务器误解密。

那么,如何使用Session进行身份验证?

很多时候我们都是通过SessionlD来指定特定的用户, SessionlD一般会选择存放在服务端,举个例子:用户成功登陆系统,然后返回给客户端具有SessionlD的Cookie,当用户向后端发起请求的时候会把SessionlD带上,这样后端就知道你的身份状态了。关于这种认证方式更详细的过程如下:

  1. 用户向服务器发送用户名和密码用于登陆系统。
  2. 服务器验证通过后,服务器为用户创建一个Session,并将Session信息存储起来。
  3. 服务器向用户返回一个SessionD,写入用户的Cookie,
  4. 当用户保持登录状态时, Cookie将与每个后续请求一起被发送出去。
  5. 服务器可以将存储在Cookie上的Session ID与存储在内存中或者数据库中的Session信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。

使用Session的时候需要注意下面几个点:

  1. 依赖Session的关键业务一定要确保客户端开启了Cookie.
  2. 注意Session的过期时间

为什么cookie无法仿制CSRF攻击,而token可以?

CSRF (Cross Site Request Forgery)一般被翻译为跨站请求伪造,那么什么是跨站请求伪造呢?说简单一点用你的身份去发送一些对你不友好的请求。举个简单的例子:

小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着”科学理财,年收益率70%”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的Cookie向银行发出请求。<a src-http://www.mybank.com/ransfer?bankld-11&money=10000>科学理财,年收益率70%</>

原因是进行Session认证的时候,我们一般使用Cookie来存储Sessionld,当我们登陆后后端生成一个Sessionld放在Cookie中返回给客户端,服务端通过Redis或者其他存储工具记录保存着这个Sessionid,客户端登录以后每次请 求都会带上这个Sessionld,服务端通过这个Sessionld来标示你这个人。如果别人通过cookie拿到了Sessionld后就可以代营你的身份访问系统了.

Session认证中Cookie中的Sessionld是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。

但是,我们使用token的话就不会存在这个问题,在我们登录成功获得token之后,一般会选择存放在localstorage中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个token,这样就不会出现CSRF漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带token的,所以这个请求将是非法的。

分布式架构下,session共享又什么方案?

  1. 不要有session:但是确实在某些场景下,是可以没有session的,其实在很多接口类系统当中,都提倡【API无状态服务】 ;也就是每一次的接口访问,都不依赖于session、不依赖于前一次的接口访问,用jwt的token;
  2. 存入cookie中:将session存储到cookie中,但是缺点也很明显,例如每次请求都得带着session,数据存储在客户端本地,是有风险的;
  3. session同步:对个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
  4. 我们现在的系统会把session放到Redis中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:实现session共享,可以水平扩展(增加Redis服务器) ,服务器重启session不丢失(不过也要注意session在Redis中的刷新/失效机制) ,不仅可以跨服务器session共享,甚至可以跨平台(例如网页端和APP端)进行共享。
  5. 使用Nginx (或其他复杂均衡软硬件)中的ip绑定策略,同一个ip只能在指定的同一个机器访问,但是这样做风险也比较大,而且也是去了负载均衡的意义;



Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • 2379. Minimum Recolors to Get K Consecutive Black Blocks
  • 2471. Minimum Number of Operations to Sort a Binary Tree by Level
  • 1387. Sort Integers by The Power Value
  • 2090. K Radius Subarray Averages
  • 2545. Sort the Students by Their Kth Score