在大型分布式系统中要同时能够满足,分布式一致性(Consistency)、可用性(Availability)和分区容忍性(Partitiontolerance),是不存在的。在大多数情况下只能满足其中的2项,而实现系统的最终一致性(Base理论)。
( 1 ) CAP特点:
a.一致性(Consistency): ( 同样数据在分布式系统的各个节点上都是一致的)
b.可用性(Availability): ( 所有在分布式系统活跃的节点都能够处理操作且能响应查询,也就是所有请求都能得到响应,不管是成功的还是失败的)
c.分区容忍性(Partition Tolerance) :(如果出现了网络故障、一部分节点无法通信,但是系统仍能够工作)
( 2 ) ACID特点:
a.原子性(Atomicity)
一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
b.一致性(Consistency)
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
c.隔离性(Isolation)
指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
d.持久性(Durability)
指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
分布式事务服务(Distributed Transaction Service,DTS)是一种分布式事务框架,用来确保在大规模分布式/微服务环境下端到端业务操作的最终一致性。
分布式事务可以简单的分为两种:
由CAP定理可知,任何大型的分布式系统/微服务在一致性C、可用性A和分区容忍P这三点上只能保证其中的两点。或者更准确的意思是: 在满足分区容忍性P的前提下,无法同时满足高可用性A和强一致性C
比如上图,服务A依赖于服务B, 分区容错性P也就是其中一个节点(假如节点B)挂掉了,但是整个系统还能用, 那么在P的前提下,有两种情况:
情况一, 满足强一致性C: 这种要求需要节点AB的数据完全一致以后再返回, 在服务A操作了某个数据,需要同步服务B,A需要一直等网络波动正常了,或者服务B起来了,再将请求返回, 这样无疑访问的效率很差, 而且A、B、C三个节点中任何一个宕机,都会导致数据不可用, 所以可用性差
情况二, 满足高可用A: 这样的话要确保查询的请求能立即返回结果, 不管是成功还是失败, 但是同样的, 如果在查询A节点的时候, B节点挂了或者网络波动, A急匆匆返回结果, 自然数据就不一致了.
基于以上的原因, 绝大部分系统都将强一致性C需求转化成最终一致性C的需求,并通过幂等机制保证了数据的最终一致性。
CAP定律是不是普通的三选二呢?
是误解,一般来说 P 是前提。所以基本是CA里选,不是任意3选2.为什么呢?P 意指分区容忍性。 这个分区容忍性什么意思,很多人容易望文生义,不要见风就是雨,理解成别的什么意思。所谓分区指的是网络分区的意思,这个一样还是容易望文生义。详细一点解释,比如你有A B两台服务器,它们之间是有通信的,突然,不知道为什么,它们之间的网络链接断掉了。好了,那么现在本来AB在同一个网络现在发生了网络分区,变成了A所在的A网络和B所在的B网络。所谓的分区容忍性,就是说一个数据服务的多台服务器在发生了上述情况的时候,依然能继续提供服务。所以显而易见的,P是大前提,如果P发生了,咱们的数据服务直接不服务了,还谈个毛的可用性和一致性呢。因此CAP要解释成,当P发生的时候,A和C只能而选一。举个简单的例子,A服务器B服务器同步数据,现在A B之间网络断掉了,那么现在发来A一个写入请求,但是B却没有相关的请求,显然,如果A不写,保持一致性,那么我们就失去了A的服务,但是如果A写了,跟B的数据就不一致了,我们自然就丧失了一致性。
2pc提交,也叫二阶段提交协议,简单的说就是有一个协调者,来协调多个参与事务的参与者,第一个阶段会向所有的参与者发送指令,让它们各自执行,然后参与者给协调者返回一个成功或失败的结果. 协调者再根据所有参与者是否执行成功,来发送最终的提交事务或回滚事务的指令.
XA是X/Open组织提出的分布式事务的架构,指的就是2pc提交的模式.它定义了(全局)事务管理器(Transaction Manager)和(局部)资源管理器(Resource Manager)之间的接口.
JTA是基于XA上的实现, 跟JDBC,JMS等j2ee规范一样,是一套规范,是定义好的接口, 用于分布式事务中控制多个数据库的事务.
我们使用atomiko + spring实现JTA分布式事务
具体代码Demo参考文章:
该方案的核心思想在于分布式系统在处理任务时 , 通过消息日志的方式来异步执行。消息日志可以存储至本地文本、数据库或消息队列,然后再通过业务规则定时任务或人工自动重试。
以在线支付系统的跨行转账为例:
第一步,伪代码如下,对用户id为A的账户扣款1000元,通过本地事务将事务消息(包括本地事务id、支付账户、收款账户、金额、状态等)插入至消息表:
Begin transaction
update user_account set amount = amount - 1000 where userId = 'A'
//更新状态到本地消息表
insert into trans_message(xid,payAccount,recAccount,amount,status)
values(uuid(),'A','B',1000,1);
end transactioncommit;
第二步,通知对方用户id为B,增加1000元,通常通过消息MQ的方式发送异步消息,对方订阅并监听消息后自动触发转账的操作;这里为了保证幂等性,防止触发重复的转账操作,需要在执行转账操作方新增一个trans_recv_log表用来做幂等,在第二阶段收到消息后,通过判断trans_recv_log表来检测相关记录是否被执行,如果未被执行则会对B账户余额执行加1000元的操作,并会将该记录增加至trans_recv_log,事件结束后通过回调更新trans_message的状态值。
Begin transaction
/**读取消息, B账户加1000
.....
*/
update trans_message set status = 0 where xid = ?
end transactioncommit;
使用消息中间件,可能会由于消费者挂掉或网络波动,短时间无法消费消息,但是生产者只关心消息是否发出去,而不关心是否被消费,所以这就是上面CAP理论中的,最终数据一致性,也是弱一致性, 优先确保A高可用
这里仍然以上面跨行转账为例,我们很难保证在扣款完成之后对MQ投递消息的操作就一定能成功。这样一致性似乎很难保证。以下伪代码说明了消息投递的异常:
try{
boolean result = dao.update(model);//更新数据库失败抛出异常
if(result){
mq.send(model);//如果MQ超时或者接收方处理失败,抛出异常
}
}catch(Exception ex){
rollback();//如果异常回滚
}
这样的话,
如果系统A在执行数据库的时候失败, 会被try catch回滚, 也就不会发送消息给系统B
如果A执行数据库成功,但是发送消息失败,同样try catch回滚
但是这种方式, B系统执行失败或者报异常的话,无法处理系统A回滚
Apache开源的RocketMQ中间件能够支持一种事务消息机制,确保本地操作和发送消息的异步处理达到本地事务的结果一致。
第一阶段,RocketMQ在执行本地事务之前,会先发送一个Prepared消息,这个消息保存在broker中,不会被消费者看到,并且会持有这个消息的接口回查地址。
第二阶段,执行本地事物操作。
第三阶段,确认消息发送,通过第一阶段拿到的接口地址URL执行回查,并修改状态,如果本地事务成功,则修改状态为已提交,否则修改状态为已回滚。
几个要解决的问题:
1 . 如果第三阶段发送失败:
如果第三阶段的确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事物消息,如果发现了prepare状态的消息(既不是提交也不是回滚的中间状态),它会向消息发送者确认本地事务是否已执行成功, 然后再根据我们配置文件配置的处理策略来决定是继续发送还是回滚
2 . 保证消费者不重复消费消息
RocketMQ、Kafka都不保证消息不重复,如果你的业务需要保证严格的不重复消息,那么就需要在我们的业务端保存消费状态,进行去重。
- 消费端处理消息的业务逻辑保持幂等性
- 保存消费者消费的状态即保证每条消息都有唯一编号,在消费者那边保证消息处理成功后,将状态写入到去重表中,每次消费消息都查询去重表中是否已经存在这个id的消费记录
3 . 解决消费失败:报警系统+人工处理
如果再消费者那边出现了逻辑业务上的异常Exception, 在普通情况下可以考虑回滚来解决, 但是在消息中间件这个系统下,系统复杂度将大大提升,且很容易出现Bug,估计出现Bug的概率会比消费失败的概率大很多。
所以针对消费失败这种情况,最好的办法就是通过报警系统及时发现失败情况然后再人工处理。其实为了交易系统更可靠,我们一般会在类似交易这种高级别的服务代码中,加入详细日志记录的,一旦系统内部引发类似致命异常要及时通过短信(钉钉、邮件)通知给业务操作人员