分布式服务需要满足CAP原则,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),但三者不可得兼;一般都会优先保证可用性和分区容错性,并且保证最终一致性。BASE理论是对CAP原则的补充,Basically Available,Soft state,Eventually Consistent,也就是当CAP三者不能兼得的时候,可以做到最终一致性。
大型应用一般需要做业务分拆以便于提升业务的可维护性和扩展性,但由于业务分布于多个系统中,数据自然也分布于多个DB中,数据库事务则不再能满足业务需求。相比较于数据库事务,分布式事务需要考虑更多的可变因素,比如某一台server没有响应或者响应超时,网络延迟等,因此一般会在分布式事务中引入一个中间协调者,用于协调多个事务参与者之间的状态。
一般分布式事务处理模式包括:2阶段提交、3阶段提交、TCC(Try-Confirm-Cancel)、可靠消息(消息队列、数据库表)、SAGAS长事务、补偿性事务。具体采用哪一种分布式事务处理模式,需要根据自己业务场景来选择合适的机制。
#1 异步消息(MQ):
异步方案用于保证最终事务一致性(Eventual Transaction Consistency),流程是MP在处理完业务之后发送一个消息到MQ里,MC从MQ中获取消息并进行处理,处理完毕之后向MP发送确认消息,为了保证消息机制的可靠性MP和MC需要保证在各自系统内部的动作都在一个事务里。优点是流程简单,MP能有很快的响应速度;缺点是没有回滚机制,因此MP和MC的状态有可能不一致;
#2 两阶段提交(2PC):
两阶段提交用于保证最终一致性,流程中需要有一个协调者,在pre-commit阶段协调者触发资源锁定动作,commit或者rollback阶段协调者进行实际的资源消费动作或者释放动作;由于时候多阶段提交,所以每一次交互都可能出现异常,因此需要引入重试机制或者MQ机制。优点是可以将极大降低状态不一致发生的可能;缺点是业务操作时同步阻塞,响应速度受影响,并且受单点问题影响。
2PC最大的问题在于当参与者比较多的时候,如果第一阶段任意有一个参与者没有及时响应的话所有参与者的资源都会被锁定,所以在此基础上第一阶段分成两个小的阶段,先发送一个询问canCommit的消息,当所有参与方都响应了才进入preCommit进行资源锁定,等所有的参与方都preCommit确定之后,进入doCommit阶段,同时所有的操作都会加入等待超时设置以保证数据一致性,这样的设计就是3阶段提交(3PC)。
#3 异步事务消息(RMQ):
异步事务消息用于保证最终一致性,流程更像是结合了异步消息和2阶段提交的混合体,可以解决MP的发送给MQ的消息如果没有得到处理或者处理失败的场景,RMQ加强了对MQ的消息状态管理和重试机制的实现;MP先发送一个pre-commit的消息给MQ,然后执行本地的事务,执行成功后给MQ一个确认消息,MQ收到消息后将消息状态设置成ready,也就是MC可以消费。
#4 补偿事务(CT)或TCC(Try-Confirm-Cancel):
针对每一个操作,都注册一个与之对应的补偿操作,从而组成一个操作链和补偿操作链;一旦操作链上某一步出错后,就会按照补偿操作链相反的顺序进行执行;优点在于不用提前锁定资源,流程比较简单;缺点是补偿操作的界定范围不明确容易造成状态不一致。
分布式系统中接口的幂等性(idempotent)
分布式系统中存在这样的情况,由于网络延迟,请求具有超时重试功能,MQ发送消息和RPC调用等,某个动作可能被执行多次,但业务需求上不能接受接口方法被执行一次与执行多次的结果状态不一致,比如接口boolean withdraw(UUID account_id, Long amount)用于对指定账户扣款,如果执行多次就会多次扣款,这样的结果是不能接受的,因此需要保证接口的幂等性。幂等性指的是对于一个接口而言如果参数相同,在特定业务场景下调用一次跟调用多次的结果状态都是相同的,因此上述接口如果遵从幂等性设计的话,则不管调用几次,用户账户的钱都只会被扣除一次。有多种幂等性的接口设计策略,如one-time-use token,全局唯一ID,多版本控制,状态机控制等,实际需要根据具体的业务进行设计。
幂等性设计的基本原则是给业务请求分配一个唯一的ID,业务单元在处理业务请求之前需要判断该业务请求是否被处理过,比如业务请求到达之后先判断缓存系统中是否有ID的记录,如果没有则表示第一次请求,则正常处理业务消息并将ID存入缓存系统;如果有记录则表示该业务请求已经被处理过,则忽略当前的业务请求;业务请求ID可以有多种形式,对于本地事务而言DB的primary key可以保证唯一性,对于分布式事务而言需要根据业务进行设计。
接口boolean withdraw(UUID orderId, UUID account_id, Long amount, OrderStatus status, String token)相比之前的接口多了三个参数,token是事先从一个共享service中获取,标识一次扣款操作,共享service会将这个token存储于自身的DB中并保存状态,orderId表示业务唯一性的ID,status表示针对orderId的状态流转,因此只要对于orderId操作过一次扣款操作之后,token标记的状态就会更新(或者status就会更新成STATUS_PAID),这样不管之后的有多少次操作都不会再次执行扣款的动作。
原文地址:https://www.cnblogs.com/leo-chen-2014/p/9532287.html