公司新产品供应链平台基于Saas的多租户模式设计,采用微服务架构。在前期技术架构选型、基础方案设计的过程中,我就一直在考虑如何保证在微服务架构下的数据一致性。
背景
数据一致性深受重视的原因主要是受老系统的影响。老系统采用单体架构设计,但作为Saas模式提供服务,一个服务集群为几十个仓库提供服务;经过多年持续迭代,内部业务逻辑耦合度极高,作为业务下游,不仅要为上游ERP提供方案兜底,还要为不同的客户提供定制化服务;在公司降本增效的前提下,服务器资源冗余度较低;当然还存在其他原因。在这种情况下,老系统经常出现数据状态不一致的问题。
新产品采用微服务架构设计,结合CAP和BASE理论,微服务架构下更无法避免数据一致性的问题。因此,需要更加完整的方案解决这方面的问题。
强一致性和最终一致性
数据一致性有两种解决方案,强一致性和最终一致性。在传统的支持事务的数据库中,同一数据源,可利用事务解决;多个数据源,可利用XA 两阶段提交达到强一致性,或重试的方式达到最终一致性。
常见解决方案
XA两阶段提交
XA协议
XA协议规范了DTP分布式事务的模型,该模型定义如下角色:
AP(Application Program):应用程序,可理解为调用分布式事务的应用程序。
TM(Resource Manager):事务管理器,负责协调和管理事务,事务管理器控制全局事务,管理事务生命周期。全局事务是指分布式事务处理环境中,需要操作多个数据库共同完成一个工作,这个工作即是一个全局事务。
RM(Transaction Manager):资源管理器,可以理解为事务的参与者,一般情况是指一个数据库实例,控制分支事务。
两阶段提交
两阶段提交是将整个事务(全局事务)分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase)。
准备阶段
事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地Undo/Redo日志,此时事务并未提交。
提交阶段
准备阶段全部成功,事务管理器给每个参与者发送提交(Commit)消息;如果有任一参与者执行失败,则发送(Rollback)消息。
注意:最后阶段才释放锁资源。
TCC模式
TCC(Try-Confirm-Cancel)是一种分布式事务解决方案,通过在业务逻辑中嵌入Try-Confirm-Cancel三个阶段,保证事务的一致性。
- Try阶段:业务条件判断逻辑检查,预占资源。
- Confirm阶段:业务逻辑确认执行结果,确认通过则提交操作,否则回滚操作。
- Cancel阶段:业务逻辑撤销之前执行的操作,释放预占资源。
SAGA模式
Saga模式的一些关键特性:
- 每个步骤都是原子性的本地事务,可以独立执行,也可以回滚。
- 每个步骤都记录其状态以便于回滚或提交。
- 每个步骤都需要明确的指定它的补偿操作,以便在出现故障时可以回滚之前的步骤。
- 如果某个步骤失败,这需要启动相应的补偿操作以回滚之前的步骤。
综合分析
XA两阶段提交
- 优点:使用简单,可以像使用本地事务一样使用基于XA的分布式事务,对业务侵入小。
- 缺点:基于强一致性的同步阻塞协议,锁资源持有时间长,对并发和吞吐量影响较大。且在事务管理器或资源管理器出现连接中断或执行超时的情况下,仍可能导致数据不一致。
TCC模式
对业务侵入大,需要将正常的业务逻辑拆分为三个阶段;一次正常执行分为两个阶段执行,即预占资源和提交两个阶段(Confirm阶段用于异常补偿);业务逻辑中预占资源,对业务状态管理要求高。对开发成本、管理成本及性能均有较大影响。
SAGA模式
每个步骤都需要明确指定其补偿操作,管理成本增加。 相比于TCC模式,SAGA模式性能较好。
总结
XA两阶段提交性能较差,无法接受;TCC模式个人来看真的是在用创造问题的方式解决问题;SAGA模式性能损失较小,可接受。但仍存在几个问题:一是每个步骤需明确指定其补偿操作,开发成本和管理成本增加;二是从补偿操作(业务逻辑回滚)来看,其过程仍存在分布式环境下的数据一致性问题,即如果有多个步骤需要补偿,需要保证多个补偿步骤均能成功执行。
最终解决方案
综上来看,XA两阶段提交、TCC模式、SAGA模式在开发、管理、性能等方面均存在一定的问题。结合我司业务特性,最终采用的解决方案:通过重试达到最终一致性。
优势
- 无性能损失。
- 将业务隔离为正向流程和逆向流程,正向流程为正常业务逻辑,逆向流程可视为补偿操作,例如取消流程。将正向流程和逆向流程隔离,业务逻辑清晰,降低开发和管理成本。
- 业务逻辑保证幂等,执行过程异常可通过人工或系统重试,或执行逆向流程。
- 在分布式环境中,接口执行超时重试是极其有效的解决方案,通过重试达到最终一致性与该设计目标是一致的。
注意:在分布式环境中应尽量保证接口幂等。
异常处理
通过重试达到最终一致性,需要解决几个问题:一是异常处理;二是接口幂等。
异常处理
业务逻辑执行异常,需要通过人工或系统重试。对于业务系统来说,需要显式标识业务逻辑异常状态并引导人工重试。 接口调用场景如下:
场景1:
- 开启本地事务
- 本地业务逻辑执行
- 执行RPC命令
- 提交本地事务
场景2:
- 开启本地事务
- 执行RPC命令
- 本地业务逻辑执行
- 提交本地事务
场景3:
- 开启本地事务
- 本地业务逻辑执行
- 执行RPC命令1
- 执行RPC命令2
- ...
- 提交本地事务
场景4:
- 开启本地事务
- 本地业务中间状态更新
- 提交本地事务
- 执行RPC命令1
- 执行RPC命令2
- ...
- 开启本地事务
- 本地业务完成状态更新
- 提交本地事务
场景1:本地业务逻辑执行一般涉及状态变更,步骤2和步骤3执行异常触发事务回滚,一般情况下不会产生副作用(需要注意步骤3执行超时而最终执行成功的问题)
场景2:如果步骤3执行异常,步骤2已执行的命令无法回滚。
步骤3:如果步骤4执行异常,同样步骤3已的命令无法回滚。
步骤4:加入中间状态,重试幂等保证最终一致性,不产生副作用。同时可结合逆向流程设计。
综合上述场景,针对简单、非核心的业务场景,考虑到开发和管理成本,可采用场景1的执行方式;针对重要的、复杂的业务场景,可采用场景4的执行方式。
接口幂等
默认情况下,接口应设计为支持幂等,例如dubbo默认在接口执行超时的情况下,会自动重试(可配置重试次数),如不支持幂等,可能会产生副作用。考虑到开发和管理成本,我们采用三种方式结合的方式保证幂等:一是唯一索引;二是状态;三是幂等表。
唯一索引:防止实体重复创建,特别是在重试场景下。
状态:一般情况下,业务性较强的场景,可采用状态+乐观锁(防止并发)的方式保证幂等。这种方式简单、直观,开发成本低。
幂等表:用于解决无法通过状态保证幂等的场景,例如库存调整。库存一般作为独立的服务提供接口,对外提供封装的库存调整接口,库存调整封装的命令是无状态的,此时需要在参数中封装业务唯一标识保证幂等。