前面一章我們走完了訂單下單流程發起者部分的原始碼,這次我們進入參與者部分原始碼解析~
訂單下單流程原始碼解析(參與者)
前面order服務中已經發起了對account服務的呼叫,接下來進入account服務扣款介面的實現部分
//order服務呼叫端
@PostMapping("/account-service/account/payment")
@Myth(destination = "account", target = AccountService.class)
Boolean payment(@RequestBody AccountDTO accountDO);
//account服務介面實現 AccountServiceImpl.payment(AccountDTO accountDTO)
@Override
@Myth(destination = "account")
public boolean payment(AccountDTO accountDTO) {
LOGGER.info("============springcloud執行付款介面===============");
final AccountDO accountDO = accountMapper.findByUserId(accountDTO.getUserId());
if (accountDO.getBalance().compareTo(accountDTO.getAmount()) <= 0) {
throw new MythRuntimeException("spring cloud account-service 資金不足!");
}
accountDO.setBalance(accountDO.getBalance().subtract(accountDTO.getAmount()));
accountDO.setUpdateTime(new Date());
final int update = accountMapper.update(accountDO);
if (update != 1) {
throw new MythRuntimeException("spring cloud account-service 資金不足!");
}
return Boolean.TRUE;
}
複製程式碼
我們發現在實現類方法頭部也進行了@Myth 註解的標記,AccountServiceImpl 是一個實現類,因此這裡必然也會走aop切面,aop切面流程入口同order服務相同,區別在於order為發起方,而account,inventory為參與者,我們是否還記得角色判斷程式碼實現部分?MythTransactionFactoryServiceImpl.factoryOf 我們再來回顧下程式碼
public Class factoryOf(MythTransactionContext context) throws Throwable {
//如果事務還沒開啟或者 myth事務上下文是空, 那麼應該進入發起呼叫
if (!mythTransactionManager.isBegin() && Objects.isNull(context)) {
return StartMythTransactionHandler.class;
} else {
if (context.getRole() == MythRoleEnum.LOCAL.getCode()) {
return LocalMythTransactionHandler.class;
}
return ActorMythTransactionHandler.class;
}
}
複製程式碼
判斷條件要想進入參與者角色分支,這裡事務必須開啟狀態 或者 myth事務上下文必須有值 ,這兩個條件又是在哪裡進行了設值呢? 我們往回看看呼叫處,找到 SpringCloudMythTransactionInterceptor.interceptor(ProceedingJoinPoint pjp) 方法
@Override
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
MythTransactionContext mythTransactionContext = TransactionContextLocal.getInstance().get();
if (Objects.nonNull(mythTransactionContext) &&
mythTransactionContext.getRole() == MythRoleEnum.LOCAL.getCode()) {
mythTransactionContext = TransactionContextLocal.getInstance().get();
} else {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = requestAttributes == null ? null : ((ServletRequestAttributes) requestAttributes).getRequest();
String context = request == null ? null : request.getHeader(CommonConstant.MYTH_TRANSACTION_CONTEXT);
if (StringUtils.isNoneBlank(context)) {
mythTransactionContext =
GsonUtils.getInstance().fromJson(context, MythTransactionContext.class);
}
}
return mythTransactionAspectService.invoke(mythTransactionContext, pjp);
}
複製程式碼
因為第一次進來,顯然mythTransactionContext值為空,進入else分支,這裡我們發現是從request請求頭中獲取的事務上下文資訊的。 既然是從請求頭資訊中拿到資料, 那必然在呼叫端要先設定對不對, 我們找到myth-springcloud工程下MythRestTemplateInterceptor類
//springcloud
@Configuration
public class MythRestTemplateInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
final MythTransactionContext mythTransactionContext =
TransactionContextLocal.getInstance().get();
requestTemplate.header(CommonConstant.MYTH_TRANSACTION_CONTEXT,
GsonUtils.getInstance().toJson(mythTransactionContext));
}
}
// motan
@Component
public class MotanMythTransactionInterceptor implements MythTransactionInterceptor {
private final MythTransactionAspectService mythTransactionAspectService;
@Autowired
public MotanMythTransactionInterceptor(MythTransactionAspectService mythTransactionAspectService) {
this.mythTransactionAspectService = mythTransactionAspectService;
}
@Override
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
MythTransactionContext mythTransactionContext = null;
final Request request = RpcContext.getContext().getRequest();
if (Objects.nonNull(request)) {
final Map<String, String> attachments = request.getAttachments();
if (attachments != null && !attachments.isEmpty()) {
String context = attachments.get(CommonConstant.MYTH_TRANSACTION_CONTEXT);
mythTransactionContext =
GsonUtils.getInstance().fromJson(context, MythTransactionContext.class);
}
} else {
mythTransactionContext = TransactionContextLocal.getInstance().get();
}
return mythTransactionAspectService.invoke(mythTransactionContext, pjp);
}
}
//dubbo
@Component
public class DubboMythTransactionInterceptor implements MythTransactionInterceptor {
private final MythTransactionAspectService mythTransactionAspectService;
@Autowired
public DubboMythTransactionInterceptor(MythTransactionAspectService mythTransactionAspectService) {
this.mythTransactionAspectService = mythTransactionAspectService;
}
@Override
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
final String context = RpcContext.getContext().getAttachment(CommonConstant.MYTH_TRANSACTION_CONTEXT);
MythTransactionContext mythTransactionContext;
if (StringUtils.isNoneBlank(context)) {
mythTransactionContext =
GsonUtils.getInstance().fromJson(context, MythTransactionContext.class);
}else{
mythTransactionContext= TransactionContextLocal.getInstance().get();
}
return mythTransactionAspectService.invoke(mythTransactionContext, pjp);
}
}
複製程式碼
我們發現是通過實現feign的RequestInterceptor介面來實現mythTransactionContext設定到頭資訊中的,這裡dubbo,motan也類似,只是實現方式不同。這裡也是實現分散式事務的最關鍵一部分,通過同一個事務上下文來關聯多子系統之間事務關係,是分散式事務實現的核心所在。
接下來我們進入參與者角色ActorMythTransactionHandler.handler
public Object handler(ProceedingJoinPoint point, MythTransactionContext mythTransactionContext) throws Throwable {
try {
//處理併發問題
LOCK.lock();
//先儲存事務日誌
mythTransactionManager.actorTransaction(point, mythTransactionContext);
//發起呼叫 執行try方法
final Object proceed = point.proceed();
//執行成功 更新狀態為commit
mythTransactionManager.updateStatus(mythTransactionContext.getTransId(),
MythStatusEnum.COMMIT.getCode());
return proceed;
} catch (Throwable throwable) {
LogUtil.error(LOGGER, "執行分散式事務介面失敗,事務id:{}", mythTransactionContext::getTransId);
mythTransactionManager.updateStatus(mythTransactionContext.getTransId(),
MythStatusEnum.FAILURE.getCode());
throw throwable;
} finally {
LOCK.unlock();
TransactionContextLocal.getInstance().remove();
}
}
複製程式碼
參與者實現比較簡單, 執行業務方法前主要封裝MythTransaction訊息(狀態為:開始,角色為:參與者),然後進行持久化操作,再執行業務方法,如果成功更新MythTransaction狀態為:COMMIT,反之狀態為:FAILURE,到這裡我們參與者也是走完了 ~~ 那我們這個流程是不是完了呢? 其實還沒有,上一章最後我們留了一小塊,我們再來回顧下
/**
* Myth分散式事務處理介面
*
* @param point point 切點
* @param mythTransactionContext myth事務上下文
* @return Object
* @throws Throwable 異常
*/
@Override
public Object handler(ProceedingJoinPoint point, MythTransactionContext mythTransactionContext) throws Throwable {
try {
//主要防止併發問題,對事務日誌的寫造成壓力,加了鎖進行處理
try {
LOCK.lock();
mythTransactionManager.begin(point);
} finally {
LOCK.unlock();
}
return point.proceed();
} finally {
//傳送訊息
mythTransactionManager.sendMessage();
mythTransactionManager.cleanThreadLocal();
TransactionContextLocal.getInstance().remove();
}
}
複製程式碼
在走account流程時,其實發起者一直在 point.proceed(); 這裡等待返回結果呢,這裡需要等待orderService.orderPay業務方法全部執行完才會返回,然而我們上面才走account一個扣款介面,還有inventory扣減庫存介面,這裡inventory介面與account介面角色都是參與者,流程上是一樣的,只是業務不一樣而已,這裡也就不做過多介紹了,童鞋們自己過一遍即可。
到這裡有童鞋可能就要說了,myth打著是一個基於訊息佇列解決分散式事務框架,但是前面講了這麼多,貌似都未涉及到訊息佇列啊, 好了,我們這就帶你們飛進mq,我們來看 mythTransactionManager.sendMessage(); 直接進入關鍵程式碼部分 CoordinatorServiceImpl.sendMessage 方法
public Boolean sendMessage(MythTransaction mythTransaction) {
final List<MythParticipant> mythParticipants = mythTransaction.getMythParticipants();
/*
* 這裡的這個判斷很重要,不為空,表示本地的方法執行成功,需要執行遠端的rpc方法
* 為什麼呢,因為我會在切面的finally裡面傳送訊息,意思是切面無論如何都需要傳送mq訊息
* 那麼考慮問題,如果本地執行成功,呼叫rpc的時候才需要發
* 如果本地異常,則不需要傳送mq ,此時mythParticipants為空
*/
if (CollectionUtils.isNotEmpty(mythParticipants)) {
for (MythParticipant mythParticipant : mythParticipants) {
MessageEntity messageEntity =
new MessageEntity(mythParticipant.getTransId(),
mythParticipant.getMythInvocation());
try {
final byte[] message = serializer.serialize(messageEntity);
getMythMqSendService().sendMessage(mythParticipant.getDestination(),
mythParticipant.getPattern(),
message);
} catch (Exception e) {
e.printStackTrace();
return Boolean.FALSE;
}
}
//這裡為什麼要這麼做呢? 主要是為了防止在極端情況下,發起者執行過程中,突然自身down 機
//造成訊息未傳送,新增一個狀態標記,如果出現這種情況,通過定時任務傳送訊息
this.updateStatus(mythTransaction.getTransId(), MythStatusEnum.COMMIT.getCode());
}
return Boolean.TRUE;
}
private synchronized MythMqSendService getMythMqSendService() {
if (mythMqSendService == null) {
synchronized (CoordinatorServiceImpl.class) {
if (mythMqSendService == null) {
mythMqSendService = SpringBeanUtils.getInstance().getBean(MythMqSendService.class);
}
}
}
return mythMqSendService;
}
複製程式碼
根據程式碼我們知道,這裡主要是將分散式訊息封裝至MessageEntity中,然後進行序列化傳送至mq訊息佇列,這裡有兩點要注意:
- serializer.serialize(messageEntity); serializer物件為服務啟動時通過spi機制載入注入
- mythMqSendService 為applicationContext.xml 配置的rocketmq
既然產生了訊息,必然會有消費者去消費,我們回到 myth-demo-springcloud-account工程下的RocketmqConsumer類 , account服務對應topic=“account”, Inventory服務對應的topic=“inventory”, 我們進入關鍵程式碼部分: mythMqReceiveService.processMessage(message);
public Boolean processMessage(byte[] message) {
try {
MessageEntity entity;
try {
entity = serializer.deSerialize(message, MessageEntity.class);
} catch (MythException e) {
e.printStackTrace();
throw new MythRuntimeException(e.getMessage());
}
/*
* 1 檢查該事務有沒被處理過,已經處理過的 則不處理
* 2 發起發射呼叫,呼叫介面,進行處理
* 3 記錄本地日誌
*/
LOCK.lock();
final String transId = entity.getTransId();
final MythTransaction mythTransaction = findByTransId(transId);
//如果是空或者是失敗的
if (Objects.isNull(mythTransaction)
|| mythTransaction.getStatus() == MythStatusEnum.FAILURE.getCode()) {
try {
//設定事務上下文,這個類會傳遞給遠端
MythTransactionContext context = new MythTransactionContext();
//設定事務id
context.setTransId(transId);
//設定為發起者角色
context.setRole(MythRoleEnum.LOCAL.getCode());
TransactionContextLocal.getInstance().set(context);
executeLocalTransaction(entity.getMythInvocation());
//會進入LocalMythTransactionHandler 那裡有儲存
} catch (Exception e) {
e.printStackTrace();
throw new MythRuntimeException(e.getMessage());
} finally {
TransactionContextLocal.getInstance().remove();
}
}
} finally {
LOCK.unlock();
}
return Boolean.TRUE;
}
@SuppressWarnings("unchecked")
private void executeLocalTransaction(MythInvocation mythInvocation) throws Exception {
if (Objects.nonNull(mythInvocation)) {
final Class clazz = mythInvocation.getTargetClass();
final String method = mythInvocation.getMethodName();
final Object[] args = mythInvocation.getArgs();
final Class[] parameterTypes = mythInvocation.getParameterTypes();
final Object bean = SpringBeanUtils.getInstance().getBean(clazz);
MethodUtils.invokeMethod(bean, method, args, parameterTypes);
LogUtil.debug(LOGGER, "Myth執行本地協調事務:{}", () -> mythInvocation.getTargetClass()
+ ":" + mythInvocation.getMethodName());
}
}
複製程式碼
消費者在接收到訊息後,進行反序列化,拿到transId查詢分散式事務訊息MythTransaction,這裡能查到資料嗎? 答案是肯定的,因為前面我們走服務呼叫時就已經對事務訊息進行了持久化操作,我們發現這裡需要進行事務狀態判斷, mythTransaction 為空或者事務狀態為FAILURE才執行本地協調事務,因為正常介面呼叫會走一次,所以這裡需要避免重複執行,導致資料不一致。
好了,到此為止我們原始碼解析部分就全部講解完畢, myth實現是沒有回滾機制的,這裡有別於tcc,也不同於2pc, 只要發起者本地事務執行成功,那麼認為這個事務就必須一直執行下去,直到成功為止,即使在呼叫其他子系統介面出現超時或者本地當機這種異常情況,待服務恢復後便會通過排程執行緒藉助mq把事務訊息傳輸給參與者,來達到最終的一致性!
異常情況處理機制介紹
- order服務異常(此時還未涉及呼叫account和Inventory服務),order本地事務回滾,account及Inventory服務無需處理。
- order服務呼叫account或Inventory服務超時,account及Inventory服務未接受到請求,此時order會通過MQ將分散式事務訊息投遞給消費者即(account及Inventory服務),account及Inventory消費訊息後查詢本地事務訊息(此時事務狀態為開始),並執行本地協調事務,以保證資料一致性。
- order服務呼叫account或Inventory服務超時,account及Inventory服務已接受到請求並處理,此時order還是會通過MQ將分散式事務訊息投遞給消費者即(account及Inventory服務),account及Inventory消費訊息查詢本地事務訊息,判斷事務狀態,因前面服務以接收到請求並處理,所以此時事務狀態為提交,固不會再次執行本地協調事務,因此這裡是支援冪等的。
- 如account及Inventory服務已接受到請求處理出現異常,此種情況會修改事務訊息狀態為:FAILURE,此時使用者可登陸管理後臺檢視到異常事務資訊,這裡需要使用者自行決定後續處理邏輯,其目的是要保證資料一致性。
如果大家有任何問題或者建議歡迎溝通 ,歡迎加入QQ群:162614487 進行交流。