- 分布式系统中出现哪些故障会导致数据不一致
- 分布式理论基础
- 常见的分布式解决方案
分布式锁
为什么需要分布式锁
- 效率:使用分布式锁可以避免不同节点重复相同的工作,这些工作会浪费资源
- 正确性:加分布式锁同样可以避免破坏正确性的发生
常见的分布式锁实现方案
- 基于MySQL来实现
- 悲观锁
- 悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
- 乐观锁
- 乐观锁在操作数据时非常乐观,认为别人不会同时修改数据。因此乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。
- 实现
- CAS机制
- 操作数:需要读写的内存位置(V)、进行比较的预期值(A)、拟写入的新值(B)
- CAS操作逻辑:如果内存位置V等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS操作是自旋的:如果不成功,会一直重试,知道操作成功为止
- 版本号机制
- 数据添加一个字段version,表示该数据版本号,每当数据被修改,version+1。更新前查询数据和版本号,更新时如果版本号一致,才会更新。
- CAS机制
- 优缺点
- 简单,不要额外组件(维护成本低)
- 性能差
- 悲观锁
分布式锁需要解决的问题
互斥性:任意时刻只能有一个客户端拥有锁,不能同时多个客户端获取
安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除
死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生
容错:当部分节点宕机,客户端仍能获取锁或释放锁
基于
Redis
的分布式锁- 利用
set key value [EX seconds][PX milliseconds][NX|XX]
命令- 设置的值由当前线程生成,防止其他线程删除,保证安全性
- 当键不存在时才能设置成功
- 使用expire完成超时机制,避免死锁
- 保证整个操作的原子性
- 存在问题
- 当前线程如果没有执行完,key过期了
- 释放锁的时候,需要
get
和delete
两步操作,不能保证原子性
python-redis-lock
- 使用Lua脚本方式进行
redis
操作,保证原子性 - 启动一个线程,当过期时间超过2/3后,自动续租。
- 使用Lua脚本方式进行
- 优点
- 性能高
- 简单
redis
维护成本低
- 缺点
- 依赖了第三方组件
- 单机
redis
稳定性差 -redis
的cluster
、sentinel
redis
的cluster
的引入可能导致redis
的锁出现问题- 问题:主库异常宕机,SET命令未同步到从库,哨兵把从库设置为新主库。
- 红锁(分布式锁算法)
- 前提:
- 不在需要部署从库和主库,只部署实例
- 主库要部署多个,官方推荐至少5个(即
redis
部署5个实例,都是主库)
- 过程
- 客户端获取当前时间戳T1
- 客户端一次向这N个实例发起加锁请求(SET命令),每个请求设置超时时间(毫秒级。远小于锁的有效时间),如果一个实例加锁失败,就立即向先一个实例申请加锁
- 如果成功客户端大于N/2个,则再次获取当前时间戳T2,如果T2-T1<锁的过期时间,认为客户端加锁成功,否则失败
- 加锁成功,去操作共享资源
- 加锁失败,向全部节点发送释放锁请求(Lua脚本释放)
- 前提:
- 利用
分布式系统中出现哪些故障会导致数据不一致
- 网络问题 - 硬件故障、网络抖动、网络拥塞
- 消息发送失败
- 消息发送成功,接收返回失败
- 程序出错
- 代码异常
- 宕机,服务器异常
- 断电
- 系统问题,磁盘满了等
分布式理论基础
cap理论
cap理论是分布式系统的理论基石
Consistency(一致性)
- “all nodes see the same data at the same time”。
- 更新操作成功并放回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。
- 对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。
- 从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
Availability(可用性)
- “reads and writes always succeed”
- 服务一致可用,而且正常响应。
Partition Tolerance(分区容错性)
- 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
- 分区容错性要求能够使应用虽然是一个分布式系统,而看上去好像是一个可以运转正常的整体。
取舍策略
- CA:
- 如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的
- 但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,违背分布式系统设计初衷
Oracle
、MySQL
- CP:
- 不要求A(可用),相当于每个请求都需要在服务器之间保持强一致性。而P(分区)会导致同步时间无限延长。
- 最典型的就是分布式数据,如
Redis
、HBase
等,对于这些分布式数据库,数据一致性是最基本的要求 NoSQL
、Mongo DB
、HBase
、Redis
- AP:
- 高可用并允许分区,则需分放弃一致性
- 一旦分区发生,节点之前可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致
NoSQL
、Coach DB
、Cassandra
、DynamoDB
- CA:
Base理论
- BASE是
Basically Available
(基本可用)、Soft state
(软状态)、Eventually consistent
(最终一致性) - BASE理论是对CAP中一致性和可用性权衡的结果,是基于CAP理论逐步演化而来的
- BASE理论核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
- BASE理论三要素:
- 基本可用
- 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但不等价于系统不可用
- 软状态
- 软状态是指允许系统中的数据存在中间状态,并认为该中间状态的存在不影响系统整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
- 最终一致性
- 最终一致性强调的是所有数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
- 基本可用
- 总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事务ACID特性是相反的,它通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一直状态。
- 同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。
常见的分布式解决方案
两阶段提交(2PC,Two-phase Commit)
两阶段提交又称2PC,2PC是一个非常经典的
中心化的原子提交协议
中心化是指协议中有两类节点:一个是中心化
协调者节点(coordinator)
和N个参与者节点(partcipant)
两个阶段
- 第一阶段:投票阶段
- 第二阶段:提交/执行阶段
举例:
订单服务A
,需要调用支付服务B
去支付,支付成功则处理购物订单状态为待发货状态,否则就需要将购物订单处理为失败状态。第一阶段:投票阶段
第一阶段分为三步
- 事务询问:
- 协调者向所有的参与者发送事务预处理请求,称为Prepare,并开始等待各参与者的响应
- 执行本地食物:
- 各个参与者执行本地事务操作,但是执行完成后不会真正的提交数据库本身事务(commit),而是先向协调者报告说:是否可以处理
- 各参与着向协调者反馈事务询问的响应:
- 如果参与者成功执行事务,则反馈Yes,表示事务可以执行,否则返回No。
第一阶段执行完后,有两种可能:1、都返回Yes 2、有一个或者多个返回No
- 事务询问:
第二阶段:提交/执行阶段(前提:第一阶段都返回Yes)
- 第二阶段分为两步
- 所有参与者返回Yes,那么就会执行事务提交
- 协调者向所有参与者发出Commit请求
- 事务提交
- 参与者收到Commit请求之后,就会正式执行本地食物Commit操作,并在完成提交之后释放整个事务执行期间占用资源
- 所有参与者返回Yes,那么就会执行事务提交
- 第二阶段分为两步
第二阶段:提交/执行阶段(第一阶段有参与者返回No,或者等待超时之后,没有返回)
- 异常流程第二阶段也分为两步
- 发送回滚请求
- 协调者向所有参与者节点发出
RollBack
请求
- 协调者向所有参与者节点发出
- 事务回滚
- 参与者收到
RollBack
请求,回滚本地事务
- 参与者收到
- 发送回滚请求
- 异常流程第二阶段也分为两步
2PC缺点
- 性能问题
- 所有参与者资源和协调者字段都是被锁住的,只有当所有节点准备完毕,事务协调者才会通知全局提交
- 单节点故障
- 由于协调者重要性,一旦协调者发生故障,参与者会一直阻塞下去
- 尤其在第二阶段,协调者故障,会导致所有参与者出于锁定事务资源的状态中,而无法继续完成事务操作
- 性能问题
2PC出现单点问题的三种情况
- 协调者正常,参与者宕机
- 由于协调者无法收集到参与者的反馈,会陷入阻塞情况
- 解决方案
- 引入超时机制
- 协调者宕机,参与者正常
- 无论出于那个阶段,协调者宕机,无法发送提交请求,所有处于未提交状态的参与者都会陷入阻塞的情况
- 解决方案
- 引入协调者备份,同时协调者需记录操作日志
- 协调者和参与者都宕机
- 发生在第一阶段:因为所有参与者都没有真正执行Commit,所以只要重新在剩余的参与这种选举一个协调者,继续执行
- 发生在第二阶段,并且挂了的参与者在挂掉之前没有收到协调者的指令:新的协调者需要重新执行第一阶段和第二阶段
- 发生在第二阶段,并且部分参与者以及执行完commit操作:2PC无法解决。
- 协调者正常,参与者宕机
TCC补偿模式
- 场景:一个订单支付之后,我们需要做下面的步骤:
- 更改订单的状态为“已支付”
- 扣减商品库存
- 给会员增加积分
- 创建销售出库单通知仓库发货
- 即
- 订单服务-修改订单状态
- 库存服务-扣减库存
- 积分服务-增加积分
- 仓储服务-创建销售出库单。
- TCC实现阶段一:Try
- 订单服务把订单状态改为
UPDATING
,表示修改中的意思,而不是支付成功 - 库存服务也不是直接扣减库存,而是冻结库存,库存表添加一个冻结库存字段
- 积分服务不直接给用户增加积分,而是积分表里加一个与增加积分字段
- 仓储服务也添加一个中间状态
- 订单服务把订单状态改为
- TCC实现阶段二:Confirm
- 订单服务增加Confirm逻辑,把订单正式修改为“已支付”
- 库存服务吧冻结库存减为0
- 积分服务类似
- 仓储服务把出库单修改状态位“已创建”
- TCC实现阶段三:Cancel
- 如果以上操作出现异常:
- 订单服务需要提供Cancel逻辑可以把订单修改为“CANCELED”
- 库存服务提供逻辑把冻结库存归还
- 积分服务取消预增加积分
- 仓储服务吧出库修改为“CANCELED”
- TCC优点
- 解决了跨服务的业务操作原子性问题
- TCC的本质原理是把数据库的二阶段提交上升到微服务来实现,避免了数据库二阶段中锁冲突的长事务低性能风险
- TCC异步性能高
- TCC缺点
- 对微服务的侵入性强,微服务每个事务都必须实现try、confirm、cancel等3个方法,开发及维护改造成本高
- 为了达到事务的一致性要求,try、confirm、cancel接口必须实现等幂性操作(定时器+重试)
- 有序事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC事务时间拉长,建议采用redis的方式来记录事务日志
- TCC需要通过锁来确保数据一致性,会导致性能不高
举例
该方案核心是通过本地事务摆正数据业务操作和消息的一致性,最后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除
以注册积分为例
- 用户注册
- 用户服务在本地新增用户和增加“消息基本日志”。(用户表和消息表通过本地事务保持一致)
- 本地数据库操作与存储积分消息日志出于同一事务中,本地数据库操作与记录消息日志操作具备原子性
- 定时任务扫描日志
- 定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈成功后删除消息
- 消费消息
- 可以使用MQ的ack(确认消息)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ发送ack,说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则会不断重试向消费者来发送消息
- 用户注册
上述方案基本避免了分布式事务,实现了“最终一致性”
但是关系型数据的吞吐量和性能方面存在瓶颈,频繁的读写消息回给数据库造成压力。所以,在真正的高并发场景下,该方案也存在瓶颈和限制
最大努力通知
- 充值例子
- 交互流程
- 账户系统调用充值系统接口
- 充值系统完成支付处理向账户系统发起充值结果通知,若通知失败,则充值系统按策略进行重复通知
- 账户系统接收到充值结果通知修改充值状态。
- 账户系统未接收到通知会主动调用充值系统的接口查询充值结果。
- 目标
- 发起通知方通过一定的机制最大努力将业务处理结果通知到接收方
- 具体包括
- 有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
- 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
- 最大努力通知与可靠消息一致性有什么不同?
- 解决方案思想不同
- 可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发到接收通知方,消息的可靠性关键由发起通知方来保证。
- 最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是可能消息接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务处理结果,通知的可靠性关键在接收通知方。
- 两者的业务应用场景不同
- 可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易
- 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去
- 技术解决方向不同
- 可靠消息一致性要解决消息从发出到接收的一致性,即消息发出并且被接收到。
- 最大努力通知无法保证消息从发出到接收的一致性,只提供消息接收的可靠性机制。可靠机制是,最大努力的将消息通知给接收方,当消息无法被接收方接收时,由接收方主动查询消息(业务处理结果)
- 解决方案思想不同
- 交互流程
- 解决方案
- 采用MQ的ack机制就可以实现最大努力通知
- 方案1
- 利用MQ的ack机制由MQ向接收通知方发送通知
- 发起通知方将通知发给MQ。使用普通消息机制将通知发给MQ。
- 接收通知方监听 MQ。
- 接收通知方接收消息,业务处理完成回应ack。
- 接收通知方若没有回应ack则MQ会重复通知。(MQ会按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔 ,直到达到通知要求的时间窗口上限。)
- 接收通知方可通过消息校对接口来校对消息的一致性。
- 利用MQ的ack机制由MQ向接收通知方发送通知
- 方案2
- 利用MQ的ack机制,与方案1不同的是应用程序向接收通知方发送通知
- 发起通知方将通知发给MQ。
- 通知程序监听 MQ,接收MQ的消息。(方案1中接收通知方直接监听MQ,方案2中由通知程序监听MQ。通知程序若没有回应ack则MQ会重复通知。)
- 通知程序通过互联网接口协议(如http、webservice)调用接收通知方案接口,完成通知。通知程序调用接收通知方接口成功就表示通知成功,即消费MQ消息成功,MQ将不再向通知程序投递通知消息。
- 接收通知方可通过消息校对接口来校对消息的一致性。
- 利用MQ的ack机制,与方案1不同的是应用程序向接收通知方发送通知
- 方案1和方案2的不同点:
- 方案1中接收通知方与MQ接口,即接收通知方案监听 MQ,此方案主要应用与内部应用之间的通知。
- 方案2中由通知程序与MQ接口,通知程序监听MQ,收到MQ的消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知,例如支付宝、微信的支付结果通知。
基于可靠消息最终一致性方案
RocketMQ
事务消息设计则主要是为了解决Producer端的消息发送与本地事务执行的原子性问题,RocketMQ
的设计中broker与producer端的双向通信能力,使得broker天生可以作为一个事务协调者存在RocketMQ
本身提供的存储机制为事务消息提供了持久化能力;RocketMQ
的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性- 在
RocketMQ 4.3
后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决Producer端的消息发送与本地事务执行的原子性问题。 - 以注册送积分为例。Producer即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。
- Producer发送事务消息。Producer(MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预览状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
- MQ Server回应消息发送成功。MQ Server接收到Producer发送给的消息则回应发送成功表示MQ已接收到消息。
- Producer执行本地事务。Producer端执行业务代码逻辑,通过本地数据库事务控制。
- 消息投递
- 若Producer本地事务执行成功则自动向MQ Server发送commit消息,MQ Server接收到commit消息后将“增加积分消息”状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
- 若Producer 本地事务执行失败则自动向MQ Server发送rollback消息,MQ Server接收到rollback消息后将删除“增加积分消息”。
- MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
- 事务回查
- 如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
- 以上主干流程已由
RocketMQ
实现,对用户则来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
MQ(message queue)
什么是MQ
- 消息队列是一种“先进先出”的数据结构
应用场景
- 应用解耦
- 流量削峰
- 数据分发
MQ的优缺点
- 优点
- 解耦、削峰、数据分发
- 缺点
- 系统可用性降低:引入外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。
- 系统复杂度高:MQ的加入大大增加了系统的复杂度,以前是系统间的同步的远程调用,现在是通过MQ进行异步调用
- 一致性问题
幂等性
服务雪崩
定义
- 服务雪崩效应是一种因“服务提供者的不可用”导致“服务调用者不可用”,并将不可用逐渐放大的现象
超时机制
timeout
是为了保护服务,避免consumer
服务因为provider
响应慢而也变得响应很慢,这样consumer
可以尽量保持原有的性能
重试机制
- 如果
provide
只是偶尔都用,超时后直接放弃,会导致请求错误,所以需要超时后重试一下。重试可以考虑换一台机器进行调用
幂等
- 如果允许
consumer
重试,那么provider
就要能够做到幂等。同一个请求被consumer
多次调用,对provider
产生的影响是一致的。而且这个幂等应该是服务级别的,而不是某台机器层面的,重试调用任何一台机器,都应该做到幂等。 - 同样的数据,因为重试机制调用了多次,数据库中只能有一份数据
常见的幂等性解决方案
哪些情况需要考虑幂等性 - 同样请求发送两次:
- http请求的类型
- get:方法用于获取资源,不应当对系统资源进行改变,所以是幂等的
- post:新增操作天生就不是一个幂等操作
- put:修改操作有可能是幂等的也可能不幂等。如果修改的资源固定,调用几次都是幂等的。否则不幂等。
- delete:方法用于删除资源,虽然改变了系统资源,但是第一次和第N次删除操作对系统的作用是相同的,所以是幂等的
唯一索引,防止新增脏数据
- 比如新建用户时,吧手机号码设置为唯一索引,那么即使重试,也只会新建一个用户,不会因为重试创建多个
token机制,防止页面重复提交
- 服务端生成token记录到
redis
,并返回给前端页面 - 页面请求添加token字段,服务端删除
redis
的token,删除成功处理请求,否则不处理
悲观锁
- 加锁防止插入多条
乐观锁
- 防止插入多条,比悲观锁性能高
分布式锁
select
+ insert
- 并发不高的系统,先查询操作是否执行,然后判断操作。
对外提供接口的api
如何保证幂等
- 需要请求是额外附带
source来源
、seq序列号
等信息