介绍在一个互联网系统中,理想情况下,肯定希望系统能够同时满足“一致性”、“可用性”和“分区容错性”。但是根据大家熟悉的CAP定律或者BASE理论,我们知道在实际情况下是不可能实现的。在金融领域,一致性是最受关注的特性,无论如何都要满足一致性。关于CAP定律和BASE理论,本文不再介绍。有兴趣的同学可以百度一下。本文重点讲解一致性方案,包括强一致性和最终一致性。在互联网领域,很多时候会牺牲强一致性来实现高可用。系统往往只需要保证“最终一致性”,只要最终时间在用户可接受的范围内即可。数据库本地事务数据库事务一定是强一致性方案,而且是一致性最简单的方案,因为一致性由数据库事务保证,业务层不需要关心细节。一个典型的应用是返现场景。有返现的一笔交易的退款,需要同时退两张交易票,通过数据库本地交易完成。详情如下:用户A在商户B处消费100元购买商品,购买完成后用户A将获得2元现金奖励。这是两笔交易,原交易100元,返现交易2元。那么,当发生退款时,需要保证两笔交易同时退款。这是直接使用数据库中的本地交易实现的,即一次退款请求,同时退款两笔交易。总结:数据库事务的优点是简单,业务层关心的少。但是对于一个高可用的系统来说,所有的业务都是在数据库事务中执行的,这会使事务变得非常复杂,不利于系统的扩展和维护。两阶段提交除了保证数据库的本地一致性外,对于互联网系统来说,更像是一种分布式系统。说到分布式系统,就不得不提到分布式事务。在分布式事务中,不得不引入两阶段提交协议(2pc)。在核心系统中,两阶段提交方案主要用于分布式数据库NesioDB和事务账户分离的灵活事务。分布式数据库NesioDB由百度DBA和百度钱包联合开发。它是一个支持分布式事务的数据库。已应用于百度钱包核心交易业务,稳定运行两年。数据库的设计要求是让用户像使用单机数据库一样使用分布式数据库,所以实现的分布式事务满足单机事务的ACID原则。对于分布式事务的一致性,采用两阶段提交的方式实现,满足分布式事务模型。如下所示。第一阶段是准备阶段。DTM通知所有参与事务的RM,并向每个RM发送准备消息。RM收到消息后进入准备阶段,要么直接返回失败,要么创建并执行本地事务,写入本地事务日志(redo和undo日志),但不commit(这里只保留最后一步与第二阶段执行最耗时的提交操作)。第二阶段是提交/回滚阶段。DTM如果在RM准备阶段收到失败消息或者从RM获取返回消息超时,则直接向RM发送回滚(rollback)消息,否则发送commit(提交)消息。RM根据TM的指令进行commit或rollback,执行完成后释放事务处理中使用的所有锁(最后阶段释放锁)。数据库层面的两阶段提交可以用来保证分布式事务的一致性,让用户像使用单机事务一样方便地使用分布式事务。两阶段提交的另一种实现方式是TCC(Try-Confirm-Cancel),这是一种业务层面的灵活事务。交易与账户分离的一致性实现就是通过使用这种灵活的交易来实现的。首先说一下灵活事务,涉及到3个模块,主业务、从业务和活动管理器(协作者)。下图是关于灵活交易的经典图片。***阶段:主业务服务分别调用所有从业务服务的try操作,并在活动管理器中记录所有从业务服务。当所有从业务服务尝试成功或一个从业务服务尝试失败时,进入第二阶段。第二阶段:活动管理器根据第一阶段业务服务的尝试结果执行确认或取消操作。如果第一阶段所有从业务服务尝试成功,则协作者调用所有从业务服务的确认操作,否则,调用所有从业务服务的取消操作。第二阶段,confirm和cancel同样失败,所以需要对这两种情况进行异常处理,保证数据的一致性。确认失败:回滚所有确认操作,执行取消操作。取消失败:从业务服务需要提供自动取消机制来保证取消成功。如果对应一个交易和账户分离的项目,流程如下:阶段1:主业务服务调用交易和账户进行try操作,交易启动交易,进行业务判断和写入,但是做不提交交易。资源锁定在会计级别。第二阶段:账户资源锁定成功,交易提交成功,然后向账户发送确认。如果事务提交失败,发送cancel释放资源。如果confirm或者cancel出现异常,需要对异常进行处理,保证数据的一致性。总结:这种方式实现起来难度不大,比较适合传统的单体应用。同一个方法中存在跨库操作。回滚机制在分布式架构中,功能X需要协调后端A、B甚至更多的原子服务。那么问题来了,如果A和B的其中一个调用失败了怎么办?这时候可以使用回滚机制来保证一致性。该机制应用于钱包与信用的联合借贷项目。本项目一共有两个原子操作,如下图所示。两个原子操作是资金收集和资金转账到卡。所谓归集资金就是将商户A的钱和商户B的钱汇集到中间商户C,而资金到卡就是把中间商户C的钱通过银行转账到D用户的银行卡上。系统。这两个操作必须满足一致性,即收款成功,然后向用户卡支付成功。或者商户A、B的钱没有变,资金没到卡。总而言之,不允许资金在中间商家C停留,针对这种情况,提供了强大的回滚操作,通过回滚机制实现上述一致性。例如,如果收款成功,但向卡转账失败,则回滚收款操作,即资金从中间商户C退还给商户A、B分别。总结:这种方式有很多缺点,在复杂的场景下通常不推荐使用,除非是非常简单的场景,非常容易提供回滚,依赖的服务非常少。这种实现方式会导致代码量大,耦合度高。而且是非常有限的,因为有很多业务是不能轻易回滚的。如果串行服务很多,回滚的代价太大。实现本地消息表的想法其实起源于eBay,后来通过支付宝等公司的传道在业界得到广泛应用。它的基本设计思想是将远程分布式事务拆分成一系列本地事务。如果不考虑性能和优雅设计,可以借助关系数据库中的表来实现。本地消息的方式应用于钱包非核心业务异步改造项目。当时项目的改造方案是:核心业务实时写入交易表,非核心业务非实时异步写入交易表。根据用户维度的交易查询表。交易表在交易维度,为了满足用户的查询性能,需要按照用户维度备份复制同一张交易查询表。从业务属性来看,交易表是核心业务,交易查询表是非核心业务(用于查询)。在实现上,事务表是核心库,查询表是非核心库。但是,两者需要保持一致。关于这种一致性保证,如果有一个不丢失消息的消息队列,就很容易解决。如果没有这样的消息队列怎么办?其实使用本地消息表也可以解决。如图所示,是一个使用本地消息表来维护最终一致性的应用。具体如下:业务A将A的本地消息和业务数据以本地事务的形式写入DB1;业务A写完本地事务后,向MQ发送消息。MQ将消息推送给业务B,业务B执行消息写入DB2。由于MQ不能保证消息不会丢失,如果消息丢失,需要通过业务C读取DB1的消息,然后通过rpc发送给业务B重新执行。当然,如何判断DB1的消息已经被消费,可以通过DB2的事务执行结果来判断。总结:上诉方式是一个非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是关系型数据库在吞吐量和性能上存在瓶颈,频繁的消息读写会给数据库带来压力。所以在真正的高并发场景下,这个方案也会有瓶颈和局限。补偿机制补偿机制是分布式系统中使用最广泛的。钱包应用的场景有很多,比如核心收银、刷卡支付等。在核心收银机里,要求银行扣款,扣款成功后,自己的系统就挂了。这时候就会有一个后台程序,我们也叫订单补货程序,开始处理这类流程,让原本中途中断的流程继续进行。在一个普遍成熟的系统中,对于更高层的服务和接口,整体的可用性通常是很高的。如果有些业务是因为瞬间网络故障或者调用超时等问题,那么这个补偿机制其实是非常有效的。小结本文通过核心系统的几个具体实践项目,阐述了如何保证分布式系统的一致性。每个解决方案都有一定的特点和应用场景。事实上,分布式系统的事务一致性本身就是一个技术问题。目前还没有简单完美的解决方案可以应对所有场景。具体来说,用户还是需要根据不同的业务场景进行选择。
