分布式事务(Saga模式)
概览
分布式事务包含两个组件TxServer和TxClient
TxServer是分布式事务协调器,主要负责接收每个参与者事务开启和结束的上报信息,并做持久化。协调补偿子事务,达到事务的最终一致。
TxClient是分布式事务客户端,主要负责上报事务开启和结束结果信息。
业务接入
业务服务需要引入TxClient依赖
上图是一个简单的通过api调用的分布式事务应用案例
由服务A开启分布式事务,生成全局事务Id以及服务A的本地事务Id,上报到协调器开启事务。
服务A开始执行业务,调用服务B
服务B开启本地事务,上报到协调器
服务B执行业务
服务B业务执行完成,向协调器上报本地事务执行结果,服务B本地事务结束
服务B返回数据给服务A
服务A继续执行后续业务
服务A执行完成,向协调器上报全局事务执行结果,全局事务结束
成功场景:
所有服务都执行成功,协调器记录所有事务为已完成状态。
服务B异常场景:
B服务发生异常,A服务也会发生异常,AB两个子事务都是异常状态,不需要补偿。
服务A在服务B执行成功后发生异常场景:
服务B本地事务为成功状态,服务A本地事务发生异常,全局事务异常,补偿服务B业务,使服务B本地事务和全局事务达到一致。
服务B执行超时场景:
服务A在调用服务B时发生超时,则服务A本地事务发生异常,全局事务异常。此时服务可能还没执行完,协调器会定期扫描。如果服务B最终执行失败,则不进行补偿,最终执行成功,则进行补偿。
接入代码示例:
在服务A接口上添加@SagaStart注解,开启分布式事务
@GetMapping("/test")
@SagaStart(serviceName = “mw-crm-service-biz2”)
public String test() throws Exception {
UserDTO userDTO = new UserDTO();
userDTO.setUserName(“ACE”);
userDTO.setAge(20);
crm1Api.service1(userDTO);
return “success”;
}
在服务B接口上添加@TxCompensation注解,成为事务链的子事务。
参数compensationServiceName为全局事务发生异常后,协调器调用的补偿服务,compensationServicePath则为补偿接口路径,补偿接口参数直接拿原接口的参数
@PostMapping("/service1")
@TxCompensation(serviceName = “mw-crm-service-biz1”, compensationServiceName = “mw-crm-service-biz1”, compensationServicePath = “/crm1/compen/service1”)
public String service1(@RequestBody UserDTO userDTO) {
return “service1”;
}
@GetMapping("/service3")
@TxCompensation(serviceName = “mw-crm-service-biz1#service3”, compensationServiceName = “mw-crm-service-biz1”, compensationServicePath = “/crm1/compen/service3”)
public String service3(@RequestParam(“userName”) String userName, @RequestParam(“age”) Integer age) {
return “service3”;
}
业务补偿接口规范:
如果业务接口请求参数为对象实体,补偿接口通过json对象接收参数
@PostMapping("/compen/service1")
public ApiResult compenService(@RequestBody UserDTO userDTO) {
log.info(“完成补偿1{}”, userDTO);
return ApiResult.build(Code.Code_Success.getName(), Code.Code_Success.getValue());
}
如果业务接口请求参数为String或者基础数据类型,补偿接口使用restful方式接收
@PostMapping("/compen/service3/{p1}/{p2}")
public ApiResult compenService2(@PathVariable String p1, @PathVariable Integer p2) {
log.info(“完成补偿p1{}, p2{}”, p1, p2);
return ApiResult.build(Code.Code_Success.getName(), Code.Code_Success.getValue());
}
mq消息接入分布式事务
流程示例:
上图是一个简单的通过mq消息传递的分布式事务流程图
服务A向服务B发送消息1,生成全局事务id和本地事务id
服务上向协调器上报消息1事务开启信息
服务B接收到到消息1,开始执行业务
服务B向服务C发送消息2
服务B向协调器上报消息2事务开启信息
服务B业务执行完成,向协调器上报消息1事务结束信息
服务C接收到消息2,开始执行业务
服务C业务执行完成,向协调器上报消息2事务结束信息
成功场景:
由于消息是异步执行的,服务B发送完消息2后可能有其它业务还需执行,所有消息1和消息2事务结束的时间先后不确定。只要两个消息事务最终上报是成功状态的,全局事务即成功。
异常场景:
消息1和消息2,不管哪个消息事务异常,全局事务异常,对整个事务链中所有的事务进行补偿。
消息消费超时场景:
业务可以根据需要设置消息消费的超时时间,协调器会定时扫描,如果消息超时还没上报结束信息,会认为该全局事务链异常,补偿上报成功的消息。
接入代码示例:
使用消息分布式事务,业务服务必须使用2.0版本的mqcenter-spring-boot-starter
参数compensationServiceName为补偿时协调器调用的服务,compensationServicePath为调用的补偿接口路径,加上@MqTxParam注解的参数为补偿时回传的参数。
注意:这个类尽量不要写其它业务,如果这个类被别的切面代理,会导致分布式事务切面解析不到补偿参数注解@MqTxSender
package com.mw.crm.biz1.service;
import com.mw.mqcenter.send.MqSendMessage;
import com.mw.tx.context.annotations.mq.MqTxParam;
import com.mw.tx.context.annotations.mq.MqTxSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
-
@Description
-
@Author caojiahai1
-
@Date 2020/12/18 10:17 上午
-
Version 1.0
*/
@Service
@Slf4j
public class MqTxMessageSender {@Autowired
private MqSendMessage mqSendMessage;@MqTxSender(serviceName = “crm1”, compensationServiceName = “mw-crm-service-biz1”, compensationServicePath = “/crm/txCompen”, timeout = 20)
public void txSendMsg(Message message, @MqTxParam String mqKey) {
mqSendMessage.sendMessage(message, mqKey);
}@MqTxSender(serviceName = “crm1”, compensationServiceName = “mw-crm-service-biz1”, compensationServicePath = “/crm/txCompen2”, timeout = 20)
public void txSendMsg2(Message message, String mqKey, @MqTxParam Map map) {
mqSendMessage.sendMessage(message, mqKey);
}
}
接收端:
在消息接收类上添加@MqTxReciever注解
@StreamListener(“demo-mq-test”)
@MqTxReciever
public void receiver(String json, @Header(AmqpHeaders.CHANNEL) Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) Long deliveryTag, @Headers Map<String, Object> heards) throws IOException {
Action action = Action.ACCEPT;
try {
log.info("收到消息:{}", json);
} catch (Exception e) {
action = handleExceptionAction(e);
} finally {
// 通过finally块来保证Ack/Nack会且只会执行一次
confirmAck(action, channel, deliveryTag, "demo-mq-test2");
}
}
业务补偿:
参数为字符串或者基础数据类型,补偿接口使用restful方式接收
@PostMapping("/txCompen/{mqKey}")
public ApiResult txCompen(@PathVariable String mqKey) {
System.out.println(“补偿key:” + mqKey);
return ApiResult.build(Code.Code_Success.getName(), Code.Code_Success.getValue());
}
在消息接收类上添加@MqTxReciever注解
@PostMapping("/txCompen2")
public ApiResult txCompen(@RequestBody Map map) {
System.out.println(“补偿:” + map);
return ApiResult.build(Code.Code_Success.getName(), Code.Code_Success.getValue());
}