微服务架构应满足数据最终一致性,实现最终一致性的三种模式:可靠事件模式、补偿模式、TCC模式。
事务补偿机制
事务补偿即在事务链中的任何一个正向事务操作,都必须存在一个完全符合回滚规则的可逆事务。如果是一个完整的事务链,则必须事务链中的每一个业务服务或操作都有对应的可逆服务。
实现真正的预提交和正式提交的分离。
需要手工编写大量的代码来处理以保证事务的完整性。
考虑实现一个通用的事务管理器,实现事务链和事务上下文的管理。对于事务链上的任何一个服务正向和逆向操作均在事务管理和协同器上注册,由事务管理器接管所有的事务补偿和回滚操作。
BASE策略中的基于消息的最终一致性是比较好的解决方案。
补偿模式
第一部分:实现补偿模式的关键在于业务流水的记录;要实现补偿过程,我们需要做到两点
1、首先要确定失败的步骤和状态,从而确定需要补偿的范围。
2、其次要能提供补偿操作使用到的业务数据。
关联表,分为框架表和业务表,技术表中保存为实现补偿操作所需要的技术数据,业务表保存业务数据,通过在技术表中增加业务表名和业务表主键来建立和业务数据的关联。
第二部分:通过重试来保证补偿过程的完整,从而满足最终一致性。
补偿过程作为一个服务调用过程同样存在调用不成功的情况,这个时候需要通过重试的机制来保证补偿的成功率。当然这也就要求补偿操作本身具备幂等性(重试就要保证幂等)。
不同的重试策略
如果只是一味的失败就立即重试会给工作服务造成不必要的压力,我们要根据服务执行失败的原因来选择不同的重试策略。(重试也有不同的策略)
1)如果失败的原因不是暂时性的,由于业务因素导致(如业务要素检查失败)的业务错误,这类错误是不会重发就能自动恢复的,那么应该立即终止重试。(参数错误)
2)如果错误的原因是一些罕见的异常,比如因为网络传输过程出现数据丢失或者错误,应该立即再次重试,因为类似的错误一般很少会再次发生。(网络抖动,在发生网络抖动时可以进行自动地补偿调用)
3)如果错误的原因是系统繁忙(比如http协议返回的500或者另外约定的返回码)或者超时,这个时候需要等待一些时间再重试。(响应超时)
重试操作一般会指定重试次数上线,如果重试次数达到了上限就不再进行重试了。这个时候应该通过一种手段通知相关人员进行处理。
对于等待重试的策略如果重试时仍然错误,可逐渐增加等待的时间,直到达到一个上限后,以上限作为等待时间。
如果某个时刻聚集了大量需要重试的操作,补偿框架需要控制请求的流量,以防止对工作服务造成过大的压力。
补偿模式补充说明
1.微服务实现补偿操作不是简单的回退到业务发生时的状态,因为可能还有其他的并发的请求同时更改了状态。一般都使用逆操作的方式完成补偿。
2.补偿过程不需要严格按照与业务发生的相反顺序执行,可以依据工作服务的重用程度优先执行,甚至是可以并发的执行。
TCC模式是优化的补偿模式。
1、所有事务参与方都需要实现try,confirm,cancle接口。
2、事务发起方向事务协调器发起事务请求,事务协调器调用所有事务参与者的try方法完成资源的预留,这时候并没有真正执行业务,而是为后面具体要执行的业务预留资源,这里完成了一阶段。
3、如果事务协调器发现有参与者的try方法预留资源时候发现资源不够,则调用参与方的cancle方法回滚预留的资源,需要注意cancle方法需要实现业务幂等,因为有可能调用失败(比如网络原因参与者接受到了请求,但是由于网络原因事务协调器没有接受到回执)会重试。
4、如果事务协调器发现所有参与者的try方法返回都OK,则事务协调器调用所有参与者的confirm方法,不做资源检查,直接进行具体的业务操作。
5、如果协调器发现所有参与者的confirm方法都OK了,则分布式事务结束。
6、如果协调器发现有些参与者的confirm方法失败了,或者由于网络原因没有收到回执,则协调器会进行重试。这里如果重试一定次数后还是失败,会怎么样那?常见的是做事务补偿。
与补偿模式不同的是TCC服务框架不需要记录详细的业务流水,完成confirm和cancel操作的业务要素由业务服务提供。
发送消息之前就开始存储这些信息。
接口补偿还是kafka发送消息的补偿,这两者记录的字段不一样,对表设计有影响。
协调服务为一个通用的补偿框架。
补偿模式表设计
CREATE TABLE `data_send_status` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`flow_id` int(11) NOT NULL COMMENT '对应业务id',
`service_type` tinyint(4) NOT NULL COMMENT '1表示支付服务调用订单服务',
`send_topic` varchar(255) DEFAULT NULL COMMENT '发送对应topic',
`send_params` varchar(255) DEFAULT NULL COMMENT '参数',
`error_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
`is_prepare` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0表示准备发送,1表示发送结束',
`is_success` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0表示发送失败,1表示发送成功',
`delete_flag` int(1) NOT NULL COMMENT '删除标志',
`created_id` int(11) NOT NULL COMMENT '创建人id',
`created_time` datetime NOT NULL COMMENT '创建时间',
`modified_id` int(11) NOT NULL COMMENT '修改人id',
`modified_time` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据发送状态记录表';
CREATE TABLE `data_auto_compensation` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`data_send_id` int(11) NOT NULL COMMENT '数据发送失败的记录id',
`retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
`max_retry_count` int(11) NOT NULL DEFAULT '3' COMMENT '最大重试次数',
`status` int(11) NOT NULL COMMENT '状态 0未解决 1已解决',
`delete_flag` int(1) NOT NULL COMMENT '删除标志',
`created_id` int(11) NOT NULL COMMENT '创建人id',
`created_time` datetime NOT NULL COMMENT '创建时间',
`modified_id` int(11) NOT NULL COMMENT '修改人id',
`modified_time` datetime NOT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='数据补偿表';