分布式面试题

什么是幂等?如何解决幂等问题

幂等性的核心思想,是保证这个接口的执行结果只影响一次,后续即便再次调用,也不能对数据产生影响,之所以要考虑到幂等性问题,是因为在网络通信中,存在俩种行为可能会导致接口被重复执行

  1. 用户的重复提交或者用户的恶意攻击,导致这个请求会被多次执行
  2. 在分布式架构中,为了避免网络通信导致的数据丢失,在服务之间通信的时候都会设计超时重试的机制,而这种机制有可能导致服务端接口被重复调用

所以在程序设计中,对于数据变更类操作的接口,需要保证接口的幂等性

  1. 使用redis里面提供的setNX指令,比如对于MQ的消费场景,为了避免MQ重复消费,导致数据多次被修改的问题,可以在收到MQ消息时,把这个消息通过setNX写到redis中,一旦消息被消费过就不会再次消费
  2. 去重表,将业务中的唯一标识字段保存到去重表,如果表中存在,则表示已经处理过了
  3. 版本控制,增加版本号,当版本号符合的时候,才更新数据
  4. 状态控制,录入的订单有状态已支付,未支付,支付失败,当处于未支付的时候,才允许被修改为支付中

举个栗子:比如添加请求的表单里,在打开添加表单页面的时候,就生成一个AddId标识,这个AddId跟着表单一起提交到后台接口。

后台接口根据这个AddId,服务端就可以进行缓存标记并进行过滤,缓存值可以是AddId作为缓存key,返回内容作为缓存Value,这样即使添加按钮被多次点下也可以识别出来。

这个AddId什么时候更新呢?只有在保存成功并且清空表单之后,才变更这个AddId标识,从而实现新数据的表单提交

分布式锁的理解和实现

分布式锁,是一种跨进程跨机器节点的互斥锁,它可以用来保证多台机器对于共享资源访问的排他性.分布式锁和线程锁的本质相似,线程锁的生命周期是单进程多线程,分布式锁的生命周期是多进程多机器节点

在本质上,他们都需要满足几个锁的特性

  • 排他性,也就是说,同一时刻,只能有一个节点去访问共享资源
  • 可重入性,允许一个已获得锁的进程,在没有释放锁之前再次重新获得锁
  • 锁的获取,释放的方法
  • 锁的失效机制,避免死锁的问题

实现方式

关系型数据库,可以使用唯一约束来实现锁的排他性,那抢锁逻辑就是:往表里插入一条数据,如果已经有其他的线程获得某个方法的锁,那这时候插入的数据会失败,从而保证了互斥性,解锁的时候就删除那条数据

redis,它里面提供了setnx命令可以实现锁的排他性,当key不存在时就返回1,存在返回0.还可以用expire命令设置锁的失效时间,从而避免死锁问题

当然有可能存在锁过期了,但是业务逻辑还没执行完的情况。 所以这种情况,可以写一个定时任务对指定的key进行续期。Redisson这个开源组件,就提供了分布式锁的封装实现,并且也内置了一个Watch Dog机制来对key做续期。Redis是一个AP模型,所以在集群架构下由于数据的一致性问题导致极端情况下出现多个线程抢占到锁的情况很难避免

zookeeper分布式锁.zk通过临时节点,解决了死锁的问题,一旦客户端获得之后突然挂掉,那么临时节点就会自动删除掉,其他客户端自动获得锁,临时顺序节点解决了惊群效应

redis看门狗机制

  1. 如果我们指定了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间,不会自动续期
  2. 如果我们未指定锁的超时时间,就使用LockWatchdogTimeout=30*1000 (看门狗默认时间)

只要占锁成功,就会启动一个定时任务(重新给锁设置过期时间,新的过期时间,就是看门狗的默认时间),每隔10s就会再自动续成30s

@GetMapping(value = "/hello")
@ResponseBody
public String hello() {
    //1、获取一把锁,只要锁的名字一样,就是同一把锁
    RLock lock = redisson.getLock("my-lock");
    //2、加锁 默认加锁时间30s
    lock.lock(); 
    try {
        System.out.println("加锁成功,执行业务..."  + Thread.currentThread().getId());
        TimeUnit.SECONDS.sleep(20);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        //3、解锁
        System.out.println("释放锁..." + Thread.currentThread().getId());
        lock.unlock();
    }
 
    return "hello";

}

CAP理论BASE理论,AP模式,CP模式

cap

  1. 一致性(Consistency):一个操作返回成功,那么只会的读请求都必须读到这个新数据;如果返回失败,那么所有读操作都不能读到这个数据。所有节点访问一份最新的数据
  2. 可用性(Availability):对数据更新具备高可用性,请求能够及时处理,不会一直等待,即使节点失效
  3. 分区容错性(Partition tolerance):系统中任意信息的丢失或失败不会影响系统的继续运作
  • 放弃p:放弃分区容错性的话,放弃了分布式系统的可扩展性
  • 放弃A:放弃可用性的话,遇到网络分区或其他故障时,受影响的服务需要等待一段时间,无法在对外提供服务
  • 放弃C:放弃一致性的话(这里指强一致性),则系统无法保证数据实时的一致性,在数据达到最终一致性时,有个时间窗口内,数据是不一致的

对于分布式系统来说 ,分区容错性是不能放弃的,因此通常是在可用性和一致性之间权衡

BASE

basically Available(基本可用)分布式系统在出现不可预知的故障的时候,允许损失部分的可用性

Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。

Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

AP

各个子事务分别执行提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致

CP

各个渍食物执行后相互等待,同时提交,同时回滚,达成强一致.但事务等待过程中,处于弱可用状态

分布式id的解决方案

UUID

复杂度最低,但会影响存储空间和性能

这种方式很简单,在每次需要新增数据的时候生成一个uuid

UUId是唯一标识码的缩写,但是在高并发下是有概重复的

优点

降低全局节点压力,使得主键生成速度更快

生成的主键"理论上"全局唯一,

跨服务器合并数据方便

缺点

UUID占用16个字符,空间占用较多,而且不是有序递增的数字,数据写入IO随机型很大,且索引效率下降

数据库主键自增

创建一个表来专门存放id

CREATE TABLE SEQID.SEQUENCE_ID ( 
    id bigint(20) unsigned NOT NULL auto_increment, 
    stub char(10) NOT NULL default '', 
    PRIMARY KEY (id), 
    UNIQUE KEY stub (stub) 
);

在每次新增的时候,先向该表新增一条数据,然后获取返回新增的主键作为要插入的主键id,我们可以使用下面的语句生成获取到一个自增id

begin; 
replace into SEQUENCE_ID (stub) VALUES ('anyword'); 
select last_insert_id(); 
commit;

stub字段没什么特殊意义,只是为了方便插入数据,只有插入数据才能产生自增id

而对于插入我们用的是replace,replace会先看是否存在stub指定值一样的数据,如果存在则先delete再insert,如果不存在则直接insert。

无论执行几次,数据库里都只有一条数据

优点

int和bigint类型占用空间较小

主键自动增长

IO写入的连续性好

数字类型查询速度优于字符串

缺点

并发性能不高,受限于数据库性能

分库分表需要改造,复杂

自增导致数据量泄漏

分布式的ID机制需要单独mysql实例,虽然可行,但是基于性能与可靠性来考虑的话,都不够,业务系统每次需要一个id时,都需要请求数据库获取,性能低并且如果此数据库实例下线了,那么将影响所有的业务系统

Redis自增

因为redis是单线程的,所以可以用来生成全部唯一id,通过incr,incrby实现

生产环境可能是redis集群, 假如有5个redis实例,每个rendis初始值是1,2,3,4,5然后增长都是5

A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25

这样的话,无论打到哪个redis上,都可以获得不同的id

使用redis的效率是非常高的,但是要考虑持久化的问题,redis支持RDB和AOF俩种持久化方式

RDB相当于一个定时快照,如果打完快照后,连续自增了几次,还没来得及做下一次快照持久化,这时候redis挂掉了,重启redis后会出现ID重复

AOF持久化相当于每条命令进行持久化,如果redis挂掉了,不会出现ID重复的现象,但是会由于incr命令过多,导致恢复时间变成

优点

使用内存并发性好

缺点

数据丢失,自增数据量泄漏

持续自增如果用AOF会导致大量的磁盘写入

号段模式

我们可以使用号段的方式来获取自增ID,号段可以理解成批量获取,比如DistributIdService从数据库获取ID时,如果能批量获取多个ID并缓存在本地的话,那样将大大提供业务应用获取ID的效率。

比如DistributIdService每次从数据库获取ID时,就获取一个号段,比如(1,1000],这个范围表示了1000个ID,业务应用在请求DistributIdService提供ID时,DistributIdService只需要在本地从1开始自增并返回即可,而不需要每次都请求数据库,一直到本地自增到1000时,也就是当前号段已经被用完时,才去数据库重新获取下一号段
这个数据库表用来记录自增步长以及当前自增ID的最大值(也就是当前已经被申请的号段的最后一个值),因为自增逻辑被移到DistributIdService中去了,所以数据库不需要这部分逻辑了。

这种方案不再强依赖数据库,就算数据库不可用,那么DistributIdService也能继续支撑一段时间。但是如果DistributIdService重启,会丢失一段ID,导致ID空洞。

为了提高DistributIdService的高可用,需要做一个集群,业务在请求DistributIdService集群获取ID时,会随机的选择某一个DistributIdService节点进行获取,对每一个DistributIdService节点来说,数据库连接的是同一个数据库,那么可能会产生多个
DistributIdService节点同时请求数据库获取号段,那么这个时候需要利用乐观锁来进行控制,比如在数据库表中增加一个version字段

因为newMaxId是DistributIdService中根据oldMaxId+步长算出来的,只要上面的update更新成功了就表示号段获取成功了。

为了提供数据库层的高可用,需要对数据库使用多主模式进行部署,对于每个数据库来说要保证生成的号段不重复,这就需要利用最开始的思路,再在刚刚的数据库表中增加起始值和步长,比如如果现在是两台Mysql,那么 mysql1将生成号段(1,1001],自增的时候序列为1,3,5,7… mysql2将生成号段(2,1002],自增的时候序列为2,4,6,8,10…

在TinyId中还增加了一步来提高效率,在上面的实现中,ID自增的逻辑是在DistributIdService中实现的,而实际上可以把自增的逻辑转移到业务应用本地,这样对于业务应用来说只需要获取号段,每次自增时不再需要请求调用DistributIdService了。

雪花算法snowflake

上面三种方法总的来说是基于自增思想的

我们可以换个角度对分布式ID进行思考,只要能让负责生成分布式ID的每台机器在毫秒内生成不一样的ID就行了

snowflake是twitter开源的分布式ID生成算法,是一种算法,所以和上面的三种生成分布式ID机制不太一样,它不依赖数据库

核心思想是分布式ID是一个long型的数字,一个long型占8个字节,也就是64个人bit,原始snowflake算法中对于bit的分配如下图

  • 符号为0,0表示证书,ID为整数,所以固定为0
  • 时间戳不用多说,用来存放时间戳,单位是ms,站41bit,这个是毫秒级的时间,一般实现实不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小的值开始
  • 工作机器id位用来存放机器的id,通常分为5个区域位+5个 服务器标识位,这里比较灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点
  • 序号位是自增

雪花算法能存放多少数据

  • 时间范围:2^41 / (1000L 60 60 24 365) = 69年
  • 工作进程范围:2^10 = 1024
  • 序列号范围:2^12 = 4096,表示1ms可以生成4096个ID。

在实际的使用场景里,很少有直接使用snowflake,而是进行改造,因为snowflake算法中最难实践的就是工作机器id,原始的snowflake算法需要人工去为每台机器去指定一个机器id,并配置在某个地方从而让snowflake从此处获取机器id。

尤其是机器是很多的时候,人力成本太大且容易出错,所以目前很多大厂对snowflake进行了改造。
百度

uid-generator使用的就是snowflake,只是在生产机器id,也叫做workId时有所不同。

uid-generator中的workId是由uid-generator自动生成的,并且考虑到了应用部署在docker上的情况,在uid-generator中用户可以自己去定义workId的生成策略,默认提供的策略是:应用启动时由数据库分配。

说的简单一点就是:应用在启动时会往数据库表(uid-generator需要新增一个WORKER_NODE表)中去插入一条数据,数据插入成功后返回的该数据对应的自增唯一id就是该机器的workId,而数据由host,port组成。

对于uid-generator中的workId,占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,同一个应用每重启一次就会消费一个workId。

美团

美团的Leaf也是一个分布式ID生成框架。它非常全面,即支持号段模式,也支持snowflake模式。名字取自德国哲学家、数学家莱布尼茨的一句话:“There are no two identical leaves in the world.”Leaf具备高可靠、低延迟、全局唯一等特点。目前已经广泛应用于美团金融、美团外卖、美团酒旅等多个部门。
Leaf中的snowflake模式和原始snowflake算法的不同点,也主要在workId的生成,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,在启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。
Leaf特性如下:

全局唯一,绝对不会出现重复的ID,且ID整体趋势递增。
高可用,服务完全基于分布式架构,即使MySQL宕机,也能容忍一段时间的数据库不可用。
高并发低延时,在CentOS 4C8G的虚拟机上,远程调用QPS可达5W+,TP99在1ms内。
接入简单,直接通过公司RPC服务或者HTTP调用即可接入。

什么是数据一致性

数据更新成功返回客户端后,所有节点的数据保持一致,没有中间状态

强一致性:时刻保证数据一致性

最终一致性:一段时间后保证数据一致性

弱一致性:允许存在部分数据不一致

柔性事务和刚性事务

刚性事务

通常无业务改造,强一致性,原生支持回滚隔离性,低并发,适合短事务

比如XA,3PC,但由于同步阻塞,处理效率低,不适合大型网站分布式场景

刚性事务指的是,要使分布式事务,达到像本地式事务一样,具备数据强一致性,从CAP来看,就是说,要达到CP状态。

柔性事务

不要求强一致性,而是要求最终一致性,允许有中间状态也就是base理论(基本可用+软状态+最终一致性)

TCC/FMT、Saga(状态机模式、Aop模式)、本地事务消息、消息事务(半消息)

柔性事务分为

  • 补偿型
  • 异步确保型
  • 最大努力通知行

介绍分布式事务

事务是由一组操作构成的可靠的独立工作单元,事务具备ACID特性,即原子性,一致性,隔离性,持久性

对于分布式系统而言需要报在系统的数据一致性,保证在子系统中始终保持一致,避免业务出现问题.要么一起成功要么一起失败

分布式锁解决了分布式资源抢占的问题,分布式事务和本地事务是解决流程化提交的问题

常见的事务模型

TCC (Try-Confirm-Cacel 补偿事务)

TCC事务模型包括三部分

  1. 主业务服务:主业务服务为整个业务活动发起的地方,服务的编排者,负责发起并完成整个业务活动
  2. 从业务服务:从业务服务是整个业务服务的参与方,负责提供TCC的业务操作,实现初始(Try)确认(Confirm),取消(Cancel)三个接口,供主业务服务调用
  3. 业务活动管理器:业务活动管理器控制整个业务的活动,包括维护TCC全局事务状态和每个业务服务的子事务状态,并在业务活动提交时,调用所有业务服务的Confirm操作

简单理解为,发起方,参与方,管理方

TCC具体含义

  • Try:预留业务资源
  • Confirm:确认执行业务操作
  • Cancel:取消执行业务操作

2PC(标准XA模型)

2PC即Two-phase Commit 二阶段提交(AT模式为2PC的增强形)

广泛应用在数据库领域,为了使得基于分布式架构的所有节点可以在进行实物处理时能够保证原子性和一致性.绝大部分关系型数据库,都是基于2PC完成分布式事务的处理

2PC分为俩个阶段,处理

阶段1,提交事务请求,阶段2:执行事务提交

如果阶段1超时或出现异常,阶段2中断事务

阶段一提交事务请求

  1. 事务询问.协调者向所有参与者发送事务内容,询问是否可以进行提交操作,并开始等待各个参与者进行响应
  2. 执行事务.各参与者阶段,执行事务操作,并将redo和Redo操作计入本机事务日志
  3. 各参与者想协调者反馈事务询问的响应.成功返回执行yes,否则为no

阶段二执行事务提交

协调者在阶段二决定是否最终执行事务提交操作

意外

事情总会出现意外,当存在某一参与者相协调者发送No响应,或等待超时时,协调者只要无法收到所有参与者的yes响应,就会中断事务

  1. 发送回滚请求。协调者向所有参与者发送Rollback请求;
  2. 回滚。参与者收到请求后,利用本机Undo信息,执行Rollback操作。并在回滚结束后释放该事务所占用的系统资源;
  3. 反馈回滚结果。参与者在完成回滚操作后,向协调者发送Ack消息;
  4. 中断事务。协调者收到所有参与者的回滚Ack消息后,完成事务中断。

2pc方案比较适合单体里跨多个库的分布式事务,而且因为严重依赖数据库层面来搞定复杂事务,效率很低,不适合高并发场景

缺点

  • 2pc的提交在执行过程中,所有参与职务操作的逻辑都处于阻塞状态,也就是说,各个参与者都在等待其他的参与者响应,无法进行其他操作
  • 协调者是个单点,一旦出现问题,其他参与者无法释放事务资源,也无法完成事务操作
  • 数据不一致,当执行事务的提交过程中,如果协调者向所有参与者发送commit请求后,发生网络异常,或者协调者未发送完commit请求,出现崩溃,最终导致只有部分协调者收到,执行请求,那么整个系统将出现不一致的情形
  • 2pc没有提供容错机制,当参与者出现故障时,协调者无法快速得知这一失败,只能严格依赖超时设置,来决定是否进行进一步的提交或是中断

总结会出现,性能问题,单点故障问题,丢失消息导致数据不一致问题

3PC(Three-Phase)

针对2PC的缺点,研究者提出了3PC,即Three-Phase Commit

2PC将原有的俩阶段,重新划分为,CanCommit,PreCommit,和DoCommit三个阶段

阶段一canCommit

事务询问协调者向所有参与者发送包含事务内容的canCommit请求,询问是否可以提交事务,并等待应答

各参与者反馈事务询问,正常情况下,如果参与者认为可以顺利执行事务,返回yes,否则返回no

阶段二PreCommit

本阶段,协调者会根据上一阶段的反馈情况来决定是否可以执行事务的preCommit操作,有以下几种可能

执行事务预提交

  1. 发送预提交请求.协调者向所有节点发出preCommit请求,并进入prepared阶段
  2. 事务预提交.参与者收到preCommit请求后,会执行事务操作,并将undo和redo日志写入本机事务日志
  3. 各参与者成功执行事务操作,同时反馈以ACK响应形式发送给协调者,同时等待最终Commit或Abort指令

中断事务

假如任意一个其参与者像协调者发送No响应,或者等待超时,协调者在没有得到所有参与者响应时,即可以中断事务

  1. 发送中断请求.协调者向所有参与者发送abort请求
  2. 中断事务.无论是协调者的abort请求,还是等待协调者请求过程中出现超时,参与者都会中断事务

阶段三doCommit

在这个阶段会真正进行事务提交,同样存在俩种可能性

执行提交

  1. 发送提交请求.假如协调者收到了所欲参与者的ACK响应,那么将从预提交状态转换到提交状态,并向所有参与者,发送doCommit请求
  2. 事务提交.参与者收到docommit请求后,会正式执行事务提交操作,并在完成提交操作后释放占用资源
  3. 反馈事务提交结果后.参与者将完成事务提交后,向协调者发送ack消息
  4. 完成事务.协调者接收到所有参与者ACK后,完成事务

中断事务

在该阶段,假设正常状态的协调者接收到任一个参与者发送的No响应,或在超时时间内,仍旧没有收到反馈信息,就会中断事务

  1. 发送中断请求.协调者向所有的参与者发送abort请求
  2. 事务回滚.参与者收到abort请求后,会利用阶段二中的Undo消息执行事务回滚,并在完成回滚后释放占用资源
  3. 反馈事务回滚结果.参与者在完成回滚后像协调者发送ack消息
  4. 中断事务.协调者接收到所有参与者反馈ACK消息后事务中断

2pc和3pc区别

3pc有效降低了2pc带来的参与者阻塞范围,冰洁能够出现单点故障后继续达成一致

但3pc带来了新的问题,在参与者收到preCommit消息后,如果网络出现分区,协调者和参与者无法进行后续的通信,这种情况下,参与者在等待超时后,依旧会执行事务提交,这样会导致数据的不一致。

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者abort请求时,会在等待超时之后,继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时, 由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大)

3pc主要解决的问题

相对于2pc,3pc主要解决单点故障问题,并减少阻塞,应为一旦参与者无法及时收到协调者的信息后,他会默认执行commit.而不会一直持有事务资源并处于阻塞状态

但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

通知形事务

异步确保

通知形事务的主流实现是通过MQ(消息队列)来通知其他事务参与者与自己事务的执行状态,引入MQ组件,有效的将事务参与者进行解耦,各参与者都可以异步执行,所以通知型事务也被称为异步事务

通知型事务主要适用于那些需要异步更新数据,并且对数据的实时性要求较低的场景,主要包含

  • 异步确保型事务:主要适用于内部系统的数据最终一致性保障,因此内部相对比较可控,如订单和购物车,收货与清算,支付与结算等场景
    将一系列同步的事务操作修改为基于消息队列执行的异步操作,来避免分布式事务中同步阻塞带来的数据操作性能的下降
  • 最大努力通知:主要用于外部系统,因为外部的网络环境更加复杂不可信,所以只能尽最大努力去通知实现最终一致性,比如重置平台与运营商,支付与对接等等跨网络级别的对接

MQ事务消息方案

基于MQ的事务消息方案主要依靠MQ的半消息机制来投递消息和参与者本地事务保证一致性.半消息机制实现原理借鉴的是2PC的思路,是二阶段提交的广义扩展

半消息:在原有队列消息执行后的逻辑,如果后面的本地逻辑出错,则不发送该消息,如果通过则告知MQ发送(半消息和普通消息的唯一区别是,在事务提交之前,对于消费者来说这个消息是不可见的)

  1. 事务发起方首先发送半消息到MQ
  2. MQ通知发送方消息发送成功
  3. 在发送半消息成功后执行本地事务
  4. 根据本地事务执行结果返回commit或者是rollback
  5. 如果本地消息是rollback,MQ将丢弃该消息不投递,如果是commit,MQ将会消息发送给消息订阅方
  6. 订阅方根据消息执行本地事务
  7. 订阅方执行本第事务成功后再从MQ中将该消息标记为已消费
  8. 如果本地事务执行过程中,执行端挂掉,或者超限,MQ服务器将不断的询问producer来获取事务状态
  9. Consumer端的消费成功机制有MQ保证

异步确保型事务

举个例子,假设存在业务规则,某比订单成功后为用户加一定的分

在这条规则里,订单数据源为服务事务发起方,管理几分数据源的服务为事务的跟随者

从这个过程可以看到,基于消息队列实现的事务存在以下操作:

  • 订单服务创建订单,提交本地事务
  • 订单服务发布一条消息
  • 积分服务受到消息后加积分

可以看到该事务形态过程简单,性能消耗小,发起方与跟随方之间的流量峰谷可以使用队列填平,同时业务开发工作量也基本与单机事务没有差别,都不需要编写反向的业务逻辑过程

因此基于消息队列实现的事务是我们除了单机事务外最优先考虑使用的形态。

本地消息方案

有时候我们目前的MQ组件不支持事务消息,或者我们想尽量减少侵入业务方.这时候我们需要另外一种放方案"基于DB本地消息表"

本地消息最初由eBay提出来解决分布式事务的问题.是目前业界使用较多的的方案之一,它的核心思想就是将分布式事务拆成本地事务进行处理

发送消息方

  • 需要有一个本地消息表,记录这消息状态相关信息
  • 业务数据和消息在同一个数据库,要保证它俩在同一个本地事务.直接利用本地事务,将业务消息写入数据库
  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到MQ消息队列.使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务的消息记录
  • 消息会发送到消息消费方,如果发送失败,即进行重试

消息消费方

  • 处理消息队列中的消息,完成自己的业务逻辑
  • 如果本地事务处理成功,则表明处理成功了
  • 如果本第事务处理失败,那么就会重试执行
  • 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚操作

生产方和消费方定时扫描本地消息表,把还没处理完成的消息在发送一遍.如果有靠谱的自动对账补齐逻辑,这种方式还是很实用的

本地消息表优缺点:

优点:

  • 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
  • 无需提供回查方法,进一步减少的业务的侵入。
  • 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。

缺点:

  • 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
  • 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的

共同

  1. 事务的消息都依赖MQ进行事务通知,所以都是异步的
  2. 事务消息在投递方都是存在重复投递的可能,需要有配套的机制去降低重复投递的使用率,实现更好的消息投递去重
  3. 事务消息的消费方,因为投递重复的无法避免,因此需要进行消费去重设计或者服务幂等设计

区别

最大努力通知

最大通知方案的目标,就是发起通知放通过一定的机制,最大努力将业务处理结果通知到接收方

最终一致性:

本质是通过通知引入定期校验最终一致性,对业务的侵入性较低,适合对最终一致性敏感度比较低,业务链路较短的场景

努力最大通知事务主要用于外部系统,业务外部的网络环境更加复杂,所以只能尽最大努力去通知实现数据最终一致性,比如充值平台与运营商支付对接,上湖通知等等跨平台,跨企业系统间业务场景交互场景

而一部确保型事务主要适用于内部系统的数据最终一致性保障,业务内部相对比较可控

普通消息是无法解决本地事务执行和消息发送的一致性问题的.因为发送是一个网络通信的过程,发送消息的过程就有可能出现发送失败,或者超时的情况.有可能发送成功了,也可能发送失败了,消息的发送方是无法确定的,所以此时消息发送方无论是提交事务还是回滚事务都有可能不一致性出现.

所以通知型事务的难度在于:投递消息和参与者本地事务保证一致性保障

因为核心要点一致,都是为了保证消息的一致性,所以流程和异步确保型一样,俩个分支

  • 基于MQ自身的事务消息方案
  • 基于DB的本地事务消息表方案
MQ事务消息方案

要实现最大努力通知,可以采用 MQ 的 ACK 机制。

最大努力通知事务在投递之前,跟异步确保型流程都差不多,关键在于投递后的处理。

因为异步确保型在于内部的事务处理,所以MQ和系统是直连并且无需严格的权限、安全等方面的思路设计。最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:

  • 业务主动方在完成业务处理后,向业务被动房(第三方系统)发送消息,允许存在消息丢失
  • 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
  • 业务被动房提供幂等的服务接口,防止通知重复消费
  • 业务主动方需要有定期效验机制,对业务数据进行兜底;防止业务被动房履行责任时进行业务回滚,确保数据最终一致性

  1. 业务活动主动方,在完成业务处理后,想业务活动的被动方发送消息,允许消息丢失
  2. 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知
  3. 主动方提供校对接口给被动房按需校对查询,用于恢复丢失的业务信息
  4. 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务
  5. 如果被动放没有正常接收,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息。

特点

  1. 用到的服务模式:可查询操作、幂等操作;
  2. 被动方的处理结果不影响主动方的处理结果;
  3. 适用于对业务最终一致性的时间敏感度低的系统;
  4. 适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;
本地消息表方案

要实现努力最大通知,可以采用定期检查本地消息表的机制

在这里插入图片描述

发送消息方:

  • 需要有一个消息表,记录着消息状态相关信息。
  • 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数据和事务消息直接写入数据库。
  • 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录
  • 消息会发到消息消费方,如果发送失败,即进行重试。
  • 生产方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:

  • 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
  • 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
  • 业务被动方提供幂等的服务接口,防止通知重复消费。
  • 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。

最大努力通知事务在于第三方系统的对接,所以最大努力通知事务有几个特性:

  • 业务主动方在完成业务处理后,向业务被动方(第三方系统)发送通知消息,允许存在消息丢失。
  • 业务主动方提供递增多挡位时间间隔(5min、10min、30min、1h、24h),用于失败重试调用业务被动方的接口;在通知N次之后就不再通知,报警+记日志+人工介入。
  • 业务被动方提供幂等的服务接口,防止通知重复消费。
  • 业务主动方需要有定期校验机制,对业务数据进行兜底;防止业务被动方无法履行责任时进行业务回滚,确保数据最终一致性。
Last modification:December 30, 2022
如果觉得我的文章对你有用,请随意赞赏