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是一个让我困惑很久的原则。原因就是只存在CP
和AP
,之前我一度认为是存在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)此操作,并反映是否可以提交
第二阶段:事务协调器要求每个数据库提交数据。
其中,如果有任何一个数据库否决了此次提交,那么所有数据库都会被要求回滚他们再次事务中的那部分信息
- XA协议比较简单,但是一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。
- XA性能不理想。无法满足高并发场景
- XA目前仅仅是在商业数据库中支持的比较理想,在mysl数据库中支持的不太理想。
- mysql的XA实现,没有记录prepare阶段的日志,主备切换会导致主库与备库数据不一致
- 许多nosql也没有支持XA。(所以应用场景变得非常狭隘)
- 其中3PC就2PC引入了超时机制(无论协调者还是参与者,在想对方发送请求后,若长时间未收到回应则作出相应处理)
- 这里有对3pc的解释
clipboard.png
5.2、柔性事务-TCC事务补偿型方案
刚性事务:遵循ACID原则,强一致性。
柔性事务:遵循BASE理论,最终一致性。
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
一阶段 prepare 行为:调动自定义的prepare逻辑。
二阶段 commit 行为:调用 自定义的commit 逻辑。
二阶段 rollback行为:调用自定义的rollback逻辑
所谓TCC模式,是指支持吧自定义的分支事务纳入到全局事务的管理中。
但是这种柔性事务,相对于代码来说,有点委屈求全了,对代码的侵入性太大。
5.3、柔性事务-最大努力通知型方案
按规律进行通知,不保证数据一定能通知成功,单会提供可查询操作接口进行核对。这中方案主要用在与第三方系统通讯时,比如:调用微信或者支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现的,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后,即不再通知。
ex:银行通知、商户通知等。支付宝的支付成功的异步回调
5.4、柔性事务-可靠消息+最终一致性方案(异步确保型)
最终一致性
基本实现:业务处理服务在物业事务提交之前,想实时详细服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,想实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
在异步确保型中,貌似最佳的载体就是RabbitMq了,因为RabbitMQ基于AMQP规范
,用消息确认机制来保证:只要消息发送,就能确保被消费者消费来做到了消息最终一致性。而且开源,文档丰富。
6、基于RabbitMQ的可靠消息实现最终一致性Coding
这里 模仿一个用户下单–>扣减用户积分的场景
在网上看到的一个较为简单的场景,也算是比较经典的。
我在此基础上,做了中间MQ处理消息状态的持久化。用于系统自动重试,实现最终一致性
整体业务做了整个封装
- 消息的重试
- 失败进入死信队列
- 对于中间状态的持久化支持多种DB
- 实现注解一键式分布式事务实现
- 采用的 RabbitMq 的可靠信息机制实现的 最终一致性
- 使用spring aop 实现动态增强
整体的架构都是基于spring家。大量使用ioc 注解。实现各个组件之间的操作
整个业务分为两个阶段。
第一阶段
上游应用执行业务并发送MQ消息
- 上游应用发送待确认消息到可靠消息系统
- 克劳消息系统保存待确认消息并返回
- 上游应用执行本地业务
- 上游应用通知可靠消息系统确认业务已执行并发送消息
中间会保存消息的状态并落地到磁盘上,保证消息的可靠性
/**
* 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消息并执行业务,并且将消息的消费结果通知给可靠消息服务
- 下游应用监听MQ消息组件并获取消息
- 下游应用根据MQ消息体信息处理本地业务
- 下游应用向MQ 确认消息已被消费
- 下游应用通知可靠消息系统被成功消费,可靠消息将该消息状态更改为已完成
整个实现,采用了多种应对策略。
针对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