赞
踩
传统事务ACID是基于单数据库的本地事务,仅支持单机事务,并不支持跨库事务。但随着微服务架构的普及,业务的分库分表导致一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立的数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成,这种事务即为“分布式事务”。当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用"XA协议"和"两阶段提交"处理。分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。
对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。
1.3.1、CAP: Consistency Acailability Partition tolerance 的简写
Consistency:一致性
对某个客户端来说,读操作能够返回最新的写操作结果
Acailability:可用性
非故障节点在合理的时间内返回合理的响应
Partition tolerance:分区容错性
当出现网络分区后,系统能够继续提供服务 你知道什么是网络分区吗 ~~
因为分布式系统中系统肯定部署在多台机器上,无法保证网络做到100%的可靠,所以网络分区一定存在,即P一定存在;在出现网络分区后,就出现了可用性和一致性的问题,我们必须要在这两者之间进行取舍,因此就有了两种架构:CP架构,AP架构;
1.3.2、AP和CP的选取: 由于网络分区一定存在,所以要根据业务场景选取保证强一致性或可用性。
1.3.3、BASE: BASE理论本质上是对CAP理论的延伸,是对 CAP 中 AP 方案的一个补充。BASE理论指的是基本可用 Basically Available,软状态 Soft Stat,最终一致性 Eventual Consistency,核心思想是即便无法做到强一致性,但应该可以有采用适合的方式保证最终一致性,BASE:Basically Available Soft Stat Eventual Consistency的简写。
BA:Basically Available 基本可用
分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用
S:Soft Stat 软状态
允许系统存在中间状态,而该中间状态不会影响系统整体可用性
E:Consistency 最终一致性
系统中的所有数据副本经过一定时间后,最终能够达到一致的状态
1.3.4、柔性事务和刚性事务: 提供强一致性的事务称之为刚性事务,把提供最终一致性的事务称之为柔性事务。刚性事务可以完全满足ACID四个特性,柔性事务一般遵循的是分布式领域中的BASE理论。
BASE
理论,传统的 ACID
事务对隔离性的要求非常高,在事务执行过程中,必须将所有的资源对象锁定,因此对并发事务的执行极度不友好,柔性事务(比如分布式事务)的理念则是将锁资源对象操作从本地资源对象层面上移至业务逻辑层面,再通过放宽对强一致性要求,以换取系统吞吐量的提升。此外,虽然柔性事务遵循的是 BASE
理论,但是还需要遵循部分 ACID
规范。ACID
规范,即数据库事务的四大基本特性ACID, 刚性事务满足ACID理论, 柔性事务满足BASE理论,追求基本可用,最终一致。二阶段提交(Two-phase Commit),是指,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol)。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。2PC是强一致性的算法。
二阶段提交算法的成立基于以下假设:
二阶段提交分为两阶段:第一阶段:投票阶段,第二阶段:提交阶段
二阶段提交优点:尽量保证了数据的强一致,但不是100%一致
缺点:
单点故障
由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞,尤其时在第二阶段,协调者发生故障,那么所有的参与者都处于锁定事务资源的状态中,而无法继续完成事务操作
同步阻塞
由于所有节点在执行操作时都是同步阻塞的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态
数据不一致
在第二阶段中,当协调者想参与者发送提交事务请求之后,发生了局部网络异常或者在发送提交事务请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了提交事务请求。而在这部分参与者接到提交事务请求之后就会执行提交事务操作。但是其他部分未接收到提交事务请求的参与者则无法提交事务。从而导致分布式系统中的数据不一致。
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,将业务表和消息表放在一个数据库事务里,保证两者的原子性,是最终一致性的算法。执行完事务后系统不直接给消息中间件发消息,而是通过后台的定时任务来扫描消息表来进行发送,定时任务会不断的失败重试,直到消息中间件成功返回 ack 消息并更细消息表状态,从而保证消息的不丢失。
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后通过轮询的方式去查询消息表,将消息推送到mq,消费者业务方去消费mq。消息最终会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。要注意消费的幂等性,消息不能被多次消费。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。后台需要运行一个定时任务来定期扫描事务状态表,对于没有完成的事务操作重新发起调用,或者执行回滚,或者在失败重试指定次数后触发告警让人工介入进行修复。
这种方案遵循BASE理论,采用的是最终一致性,是一种异步确保AP架构的方案。 该种模式的核心思想是事务的发起方维护一个本地消息表,业务执行和本地消息表的执行处在同一个本地事务中。业务执行成功,则同时记录一条“待发送”状态的消息到本地消息表中。系统中启动一个定时任务定时扫描本地消息表中状态为“待发送”的记录,并将其发送到MQ系统中,如果发送失败或者超时,则一直发送,直到发送成功后,从本地消息表中删除该记录。有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多步骤需要处理。
实例1:
执行流程:
订单系统中的消息有可能由于业务问题会一直重复发送,所以为了避免这种情况可以记录一下发送次数,当达到次数限制之后报警,人工接入处理;库存系统需要保证幂等,避免同一条消息被多次消费造成数据不一致;
本地消息表这种方案实现了最终一致性,需要在业务系统里增加消息表,业务逻辑中多一次插入的DB操作,所以性能会有损耗,而且最终一致性的间隔主要有定时任务的间隔时间决定。
实例2: 用户在支付服务完成了支付订单支付成功后,会调用会计服务的接口生成一条原始的会计凭证到会计数据库中。
1)在支付库中引入一张消息表来记录支付消息,即用户支付成功后同时往这张消息表插入一条支付成功的消息,状态为“发送中”。注意支付逻辑和插入消息表的代码要包裹在一个事务里面,这里保证了本地事务的强一致性。即支付逻辑和插入消息表的消息组成了一个强一致性的事务,要么同时成功,要么同时失败。
2)完成 1)步的逻辑后,此时再向mq的PAY_QUEUE队列中投递一条支付消息,这条支付消息的内容跟保存在支付库消息表的消息内容一致。
3)mq接收到消息后,此时会计服务也监听到这条消息了,此时会计服务处理消费逻辑即开始生成会计凭证。
4)会计凭证生成后,再反向向mq投递一条消费成功的消息到ACC_QUEUE队列
5)同时支付服务又来监听这个会计服务消费成功的消息,当支付服务监听到这个消费成功的消息后,此时再将本地消息表的消息状态改为“已发送”。
6)经过前面5步后,整个业务就已经完成了
如何保证消息不会丢失?
概念: 接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。实现幂等性,就是对一个接口多次发起同一个请求,这个接口得保证结果是准确的。
案例: 支付场景比如用户购买商品后进行支付,支付扣款成功,但是返回结算结果的时候网络异常,用户再次点击按钮,那么此时会尝试进行第二次扣款,这就需要进行接口的幂等性保证,否则会二次付款。
解决方案:
1、每个请求必须有一个唯一的标识,数据库表加唯一索引,防止新增脏数据。比如对订单号进行加唯一索引,防止生成重复订单。如果不加索引的后果是:当根据订单号去支付,支付表生成两条重复的订单号,然后去支付宝、微信、易宝支付去支付,付款完成后,第三方异步回调接口,本地接口首先根据订单号查询实体,发现查询到两条,系统就会抛出异常。每个请求必须有一个唯一的标识,比如每个订单支付请求都包含订单 id,订单id是唯一的,且每个订单只能支付一次。每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录这个订单的支付流水。每次接收请求需要进行判断,判断之前是否处理过。如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,如果Id 已经存在,唯一键约束生效,报错,插入失败,那么就不用再扣款了。
2、分布式锁,利用redis,在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路。
3、token机制,防止重复提交, 业务要求页面的数据只能被点击提交一次, 但是可能由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。
利用 Token + Redis 验证
来防止重复提交
可以看到,生成的每一次 Token 的有效期都是一次业务未执行到执行完期间,每一次业务操作对应着一个唯一的 Token。
概念:为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。
基于数据库实现分布式锁;
基于缓存(Redis等)实现分布式锁;
基于Zookeeper实现分布式锁;
分布式锁应该具备哪些条件:
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁实现的三个核心要素: 加锁、解锁、锁超时。如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住(死锁)。所以锁必须设置一个超时时间。
实现方案:
1、基于数据库实现分布式锁;基于数据库的实现方式的核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
2、基于单机Redis实现分布式锁。 实现思想:利用 Redis 的 setnx
命令。此命令同样是原子性操作,只有在 key
不存在的情况下,才能 set
成功。为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。释放锁时先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。SETNX(SETNX key val):当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
缺点:
3、基于集群Redis实现分布式锁 -Redlock算法。 针对N个相互独立的Master节点,采用上述的单节点方式获取或释放锁。一个客户端需要做如下操作来获取锁:
(1)获取当前时间(单位是毫秒)。
(2)轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
(3)客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
(4)如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
(5)如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁(lua 脚本),即便是那些他认为没有获取成功的锁。
4、基于Zookeeper实现分布式锁。 ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:
(1)创建一个目录mylock;
(2)线程A想获取锁就在mylock目录下创建临时顺序节点;
(3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
的分布式资源锁实现
领导者选举就是要保证每个组件的高可用性,例如,在 Kubernetes 集群中,允许同时运行多个 kube-scheduler 节点,其中正常工作的只有一个 kube-scheduler 节点(即领导者节点),其他 kube-scheduler 节点为候选(Candidate)节点并处于阻塞状态。在领导者节点因某些原因而退出后,其他候选节点则通过领导者选举机制竞选,有一个候选节点成为领导者节点并接替之前领导者节点的工作。领导者选举机制如下图所示。
生产环境中为了保障业务的稳定性,集群都需要高可用部署,k8s 中 apiserver 是无状态的,可以横向扩容保证其高可用,kube-controller-manager 和 kube-scheduler 两个组件通过 leader 选举保障高可用,即正常情况下 kube-scheduler 或 kube-manager-controller 组件的多个副本只有一个是处于业务逻辑运行状态,其它副本则不断的尝试去获取锁,去竞争 leader,直到自己成为leader。如果正在运行的 leader 因某种原因导致当前进程退出,或者锁丢失,则由其它副本去竞争新的 leader,获取 leader 继而执行业务逻辑。
当我们通过创建 Deployment 间接创建 ReplicaSet 时,我们有时候并不想所有的 ReplicaSet 中的 Pod 运行统一的逻辑。这时候我们就需要一种方式来选择(通知)某一个 Pod ,来确定这个 Pod 提供特殊功能,其他的 Pod 提供普通功能。
leaderelection 主要的工作原理是,通过 kubernetes 的 endpoints 或 configmaps 实现一个分布式锁,抢到锁的节点成为 leader,并定期更新,而抢不到的节点会一直等待。当 leader 因为某些异常原因挂掉后,租约到期,其他节点会尝试抢锁,成为新的 leader。leader-elect-resource-lock是k8s分布式资源锁的资源对象,目前只支持endpoints和configmaps。Leader Election 的过程本质上就是一个竞争分布式锁的过程。在 Kubernetes 中,这个分布式锁是以创建 Endpoint 或者 ConfigMap 资源的形式进行:谁先创建了某种资源,谁就获得锁。获取到锁的 Pod 将会在对应 Namespace 下创建对应的 Endpoint 对象,并在其 Annotations 上记录 Pod 的信息。在 Annotations 中以设置了 key 为 control-plane.alpha.kubernetes.io/leader
,value 为对应 Leader 信息的 JSON 数据。
领导者选举机制是分布式锁机制的一种,实现分布式锁有多种方式,例如可通过 ZooKeeper、Redis、Etcd 等存储服务。Kubernetes 系统依赖于 Etcd 做存储服务,系统中其他组件也是通过 Etcd 实现分布式锁的。kube-scheduler 组件在 Etcd 上实现分布式锁的原理如下。
分布式锁依赖于 Etcd 上的一个 key,key 的操作都是原子操作, 将 key 作为分布式锁,它有两种状态——存在和不存在。
key(分布式锁)不存在时:多节点中的一个节点成功创建该 key(获得锁)并写入自身节点的信息,获得锁的节点被称为领导者节点。领导者节点会定时更新(续约)该 key 的信息。
key(分布式锁)存在时:其他节点处于阻塞状态并定时获取锁,这些节点被称为候选节点。候选节点定时获取锁的过程如下:定时获取 key 的数据,验证数据中领导者租约是否到期,如果未到期则不能抢占它,如果已到期则更新 key 并写入自身节点的信息,更新成功则成为领导者节点。
代码实现: 完整的 Leader Election 过程可以简单描述为:
每个 Pod 在启动的时候都会创建 LeaderElector 对象,然后执行 LeaderElector.Run() 循环;
在循环中,Pod 会定期(RetryPeriod)去不断尝试创建资源,如果创建成功,就在对应资源的字段中记录 Pod 相关的 Id(比如节点的 hostname);
在循环周期中,Leader 会不断 Update 资源锁的对应时间信息,从节点则会不断检查资源锁是否过期,如果过期则尝试更新资源,标记资源所有权。这样一来,一旦 Leader 不可用,则对应的资源锁将得不到更新,过期之后其他从节点会再次创建新的资源锁成为 Leader;
paxos:
raft:
zookeeper:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。