自定义taglib引入失败_分布式事务探讨(自定义注解实现分布式事务最终一致性)...

  • 4.1、分布式事务中的CAP与BASE理论

    • CAP定理

  • 4.2、BASE理论

  • 5.1、2pc模式/3pc

  • 5.2、柔性事务-TCC事务补偿型方案

  • 5.3、柔性事务-最大努力通知型方案

  • 5.4、柔性事务-可靠消息+最终一致性方案(异步确保型)

  • 第一阶段

  • 第二阶段

  • 针对RabbitMQ 消息一致性的各种生产问题

前言

在开发这几年来,每次提到事务,都觉得很头疼。又很虚无的感觉。可是事务又是不可避免的一环。很难得的参与了公司的整个项目的重构,转型。对一些一线的解决方案,多了些自己的看法。故此记录一下。

1、本地事务

说起事务第一个就想到的四个特性呗。ACID

  • 原子性Atomicity
  • 一致性Consistency
  • 隔离性Isolation
  • 持久性Durabilily

在以往的单体应用中,使用spring提供的事务可以完美的解决各种事务问题。一旦有什么异常可以进行全局回滚。

2、事务的隔离级别

说到事务,第二个要说的就是事务的隔离级别了。(主要是针对Mysql哦)

  • READ UNCOMMITTED(读未提交)
    • 会读到其他未提交事务的数据,即为脏读
  • READ COMMITTED (读提交)
    • 一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle和SQL Server的默认隔离级别
  • REPEATABLE READ(可重复读)传说中的双R级别。
    • 该隔离级别是Mysql默认的隔离级别,在同一个事务里,select的结果是事务开始时时间点的状态,因此,同样的select操作读到的结果会是一致的,但是,会有幻读现象。MySQL的InnoDB引擎可以通过next-key locks 机制来避免幻读。(这里是一个面试的高频点,推荐阅读以下“行锁的算法”一文)
  • SERIALIZABLE(序列化)
    • 事务都是串行化执行的。不会出现任何问题,但是会极大程度的影响效率。
    • InnoDB会给读操作添加一把共享锁

本地事务在分布式系统中,只能控制自己的回滚,控制不了其他服务的回滚。(网络问题+分布式机器)

3、事务的传播行为

事务传播行为类型说明
PROPAGATION_REQUIRED如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的
PROPAGATION_SUPPORTS支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常
PROPAGATION_MANDATORY支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常
PROPAGATION_REQUIRES_NEW创建新事务,无论当前存不存在事务,都创建新事务
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务则执行与PROPAGATION_REQUIRED类似的操作

本地互相调用中存在一个坑。就是两个方法之间互相调用,会导致事务失效。原因是在方法调用的过程中绕过了代理对象。

4、分布式事务

在分布式系统中,存在各种各样的坑。

机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失…

分布式事务大致可分为以下几种:

  • XA 方案
  • TCC 方案
  • SAGA 方案
  • 本地消息表
  • 可靠消息最终一致性方案
  • 最大努力通知方案

4.1、分布式事务中的CAP与BASE理论

CAP定理

CAP是一个让我困惑很久的原则。原因就是只存在CPAP,之前我一度认为是存在CA的。

  • C : Consistency 一致性
    • 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的新数据副本)
  • A:Availability 可用性
    • 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
  • P:Partition tolerance 分区容错性
    • 大多数分布式系统都是在多个子网络中,每个子网络就叫做一个区(partition)。分区容错的意思就是,区间通信可能失败。比如,一台服务器放在中国,另一台在美国。这两个区可能无法通信。

为什么要说P是一定的。

因为在分布式系统下,网络是一定的。那么网络就一定会存在网络波动。所以P是一定无法避免的。

要保障强一致性的C,那么就只能是单机模式下。那么就做不到高可用

如果想要高可用,就一定存在数据的延迟,不一致性的问题。

所以一般对于大型的BAT公司,要确保服务达到5个9的可用性。即保证P和A,舍弃C。

4.2、BASE理论

是对CAP理论的延伸,思想就是及时无法做到强一致性(CAP的一致性就是强一致性),但可以采用释放的采取弱一致性,即最终一致性。

BASE:

  • 基本可用(Basically Available)
    • 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户想用的查询结果,但是由于出现故障(比如系统部分机房发生断电或者断网故障),查询结果的相应时间增加到了1~2秒
    • 功能上的损失:一般的电商系统,为了实现系统整个的稳定性。会采用服务的降级措施。
    • 是分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
  • 软状态(Soft State)
    • 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中以一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种软状态的体现
  • 最终一致性(Eventual Consistency)
    • 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况

对强一致性和弱一致性和最终一致性的理解

用mysql来简单介绍一下。

如果早mysql中,一条数据的更新操作,要求后续的访问都能被看到,这就是强一致性

如果要求不一定要全部看到,这就是弱一致性。

如果是要求在一定的时间后,所有的访问都可以看到,这就是最终一致性

5、分布式事务几种方案

5.1、2pc模式/3pc

是数据库支持的一种模式(2 phase commit),又叫做XA Transactions

mysql 从5.5 版本开始支持。SQL Server 2005开始支持,Oracle 7 开始支持。

XA分为两个阶段提交协议

第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交

第二阶段:事务协调器要求每个数据库提交数据。

其中,如果有任何一个数据库否决了此次提交,那么所有数据库都会被要求回滚他们再次事务中的那部分信息

4608307baae94e5de325adaec181f149.png
image-20201210223535896
  • XA协议比较简单,但是一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。
  • XA性能不理想。无法满足高并发场景
  • XA目前仅仅是在商业数据库中支持的比较理想,在mysl数据库中支持的不太理想。
    • mysql的XA实现,没有记录prepare阶段的日志,主备切换会导致主库与备库数据不一致
  • 许多nosql也没有支持XA。(所以应用场景变得非常狭隘)
  • 其中3PC就2PC引入了超时机制(无论协调者还是参与者,在想对方发送请求后,若长时间未收到回应则作出相应处理)
  • 这里有对3pc的解释
  • 6761a7b50b85f0a9a3ae3105078b5323.png
    clipboard.png

5.2、柔性事务-TCC事务补偿型方案

刚性事务:遵循ACID原则,强一致性。

柔性事务:遵循BASE理论,最终一致性。

与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

932a94322f942393a01d171930b80f92.png
image-20201210224315560

一阶段 prepare 行为:调动自定义的prepare逻辑。

二阶段 commit 行为:调用 自定义的commit 逻辑。

二阶段 rollback行为:调用自定义的rollback逻辑

所谓TCC模式,是指支持吧自定义的分支事务纳入到全局事务的管理中。

但是这种柔性事务,相对于代码来说,有点委屈求全了,对代码的侵入性太大。

5.3、柔性事务-最大努力通知型方案

按规律进行通知,不保证数据一定能通知成功,单会提供可查询操作接口进行核对。这中方案主要用在与第三方系统通讯时,比如:调用微信或者支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现的,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后,即不再通知。

ex:银行通知、商户通知等。支付宝的支付成功的异步回调

10a909aa807611ce209804fcd8cfc2d8.png
image-20201210231245064

5.4、柔性事务-可靠消息+最终一致性方案(异步确保型)

最终一致性

基本实现:业务处理服务在物业事务提交之前,想实时详细服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,想实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

在异步确保型中,貌似最佳的载体就是RabbitMq了,因为RabbitMQ基于AMQP规范,用消息确认机制来保证:只要消息发送,就能确保被消费者消费来做到了消息最终一致性。而且开源,文档丰富。

6、基于RabbitMQ的可靠消息实现最终一致性Coding

这里 模仿一个用户下单–>扣减用户积分的场景

e63e2d4d221c7bf71b6c5d680666fc4a.png
image-20201211140908597

在网上看到的一个较为简单的场景,也算是比较经典的。

我在此基础上,做了中间MQ处理消息状态的持久化。用于系统自动重试,实现最终一致性

整体业务做了整个封装

  • 消息的重试
  • 失败进入死信队列
  • 对于中间状态的持久化支持多种DB
  • 实现注解一键式分布式事务实现
    • 采用的 RabbitMq 的可靠信息机制实现的 最终一致性
  • 使用spring aop 实现动态增强

整体的架构都是基于spring家。大量使用ioc 注解。实现各个组件之间的操作

整个业务分为两个阶段。

第一阶段

上游应用执行业务并发送MQ消息

058ede66a4fb2af9682665e499cd409c.png
preview
  1. 上游应用发送待确认消息到可靠消息系统
  2. 克劳消息系统保存待确认消息并返回
  3. 上游应用执行本地业务
  4. 上游应用通知可靠消息系统确认业务已执行并发送消息

中间会保存消息的状态并落地到磁盘上,保证消息的可靠性

/**
* aop 拦截 注解,实现动态增强第一步
*
* @author by Mr. Li 2020/12/11 22:19
*/
@Component
@Aspect
@Slf4j
public class TransactionSender {

@Autowired
private ApplicationContext applicationContext;

@Autowired
private RabbitSender rabbitSender;

/**
* 雪花算法生成全局唯一id
*/
@Autowired
private IdWorker idWorker;

@Autowired
@Qualifier("delayQueue")
private Queue queue;

/**
* 切点
*/
@Pointcut("@annotation(com.lg.distributed.transaction.message.annotation.LgDistributedTransaction)")
public void pointCut() {
}

/**
* 环绕
* 这里是将生成的 bizName 作为唯一id
*
* @param joinPoint
* @param rd
*/
@Around("pointCut() && @annotation(rd)")
public void sendMsg(ProceedingJoinPoint joinPoint, LgDistributedTransaction rd) throws Throwable {
log.info("==> custom mq annotation,rd:{}", rd);
String exchange = rd.exchange();
String routingKey = rd.routingKey();
String bizName = rd.bizName() + MQConstants.DB_SPLIT + getCurrentDateTime();
String dbCoordinator = rd.dbCoordinator();
String msgId = idWorker.nextId() + MQConstants.DB_SPLIT + getCurrentDateTime();
if (queue.getArguments() == null) {
// 填充 死信队列 信息
/**
* 需要设置三个设置
* x-dead-letter-exchange:order-event-exchange
* x-dead-letter-routing-key:order.release.order
* x-message-ttl:60000
*
* 给当前队列绑定一个交换机,设置一个路由键,设置一个过期时间
*
*/
queue.addArgument("x-dead-letter-exchange", exchange);
queue.addArgument("x-dead-letter-exchange", routingKey);
queue.addArgument("x-message-ttl", 60000);
}

DBCoordinator coordinator = null;
// 将获取到的信息持久化到磁盘
try {
coordinator = (DBCoordinator) applicationContext.getBean(dbCoordinator);
} catch (Exception e) {
log.error("无消息存储类,事务执行终止");
return;
}
// 发送消息前,先持久化消息
coordinator.setMsgPrepare(msgId);
// 放行
Object proceed = null;
try {
proceed = joinPoint.proceed();
} catch (Exception e) {
log.error("业务执行失败,业务名称:{}", bizName);
throw e;
}

if (proceed == null) {
proceed = MQConstants.BLANK_STR;
}
// 发送信息
RabbitMetaMessage rabbitMetaMessage = new RabbitMetaMessage();
MessageEntity messageEntity = new MessageEntity();
messageEntity.setClassType(joinPoint.getSignature().getDeclaringTypeName());
messageEntity.setMessageId(msgId);
messageEntity.setContent(bizName);
messageEntity.setCreateTime(new Date());
messageEntity.setMessageStatus(0);
messageEntity.setRoutingKey(routingKey);
messageEntity.setToExchange(exchange);
rabbitMetaMessage.setMessageEntity(messageEntity);
rabbitMetaMessage.setPayload(proceed);

// 将消息设置为ready状态
coordinator.setMsgReady(msgId, rabbitMetaMessage);

//发送
try {
rabbitSender.setCorrelationData(dbCoordinator);
rabbitSender.send(rabbitMetaMessage);
} catch (Exception e) {
log.error("第一阶段消息发送异常" + bizName + e);
throw e;
}

}

private static String getCurrentDateTime() {
SimpleDateFormat df = new SimpleDateFormat(MQConstants.TIME_PATTERN);
return df.format(new Date());
}

}

较为重要的代码逻辑

第二阶段

下游应用监听MQ消息并执行业务,并且将消息的消费结果通知给可靠消息服务

a94d46a75e779af89c0eec697ca38fc4.png
clipboard.png
  1. 下游应用监听MQ消息组件并获取消息
  2. 下游应用根据MQ消息体信息处理本地业务
  3. 下游应用向MQ 确认消息已被消费
  4. 下游应用通知可靠消息系统被成功消费,可靠消息将该消息状态更改为已完成

整个实现,采用了多种应对策略。

1c035b998d1c584197a19aa4a4f740bc.png
image-20201212180842783

针对RabbitMQ 消息一致性的各种生产问题

https://www.cnblogs.com/sw008/p/11054331.html

生产者:confire模式。异步等待MQ回调通知是否接收到消息,判断是否重发。

MQ:持久化。设置Queue持久化 + Msg持久化deliveryMode=2

消费者:手动ACK。注意:超时、死循环、Qos、幂等

源码地址:

https://gitee.com/ligangyun/distributed/tree/master/transaction-message

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值