通過之前的幾篇文章我相信您已經搭建好了執行環境,本次的專案實戰是依照happylifeplat-tcc-demo專案來演練,也是非常經典的分散式事務場景:支付成功,進行訂單狀態的更新,扣除使用者賬戶,庫存扣減這幾個模組來進行tcc分散式事務。話不多說,讓我們一起進入體驗吧!
- 首先我們找到 PaymentServiceImpl.makePayment 方法,這是tcc分散式事務的發起者
//tcc分散式事務的發起者
@Tcc(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public void makePayment(Order order) {
order.setStatus(OrderStatusEnum.PAYING.getCode());
orderMapper.update(order);
//扣除使用者餘額 遠端的rpc呼叫
AccountDTO accountDTO = new AccountDTO();
accountDTO.setAmount(order.getTotalAmount());
accountDTO.setUserId(order.getUserId());
accountService.payment(accountDTO);
//進入扣減庫存操作 遠端的rpc呼叫
InventoryDTO inventoryDTO = new InventoryDTO();
inventoryDTO.setCount(order.getCount());
inventoryDTO.setProductId(order.getProductId());
inventoryService.decrease(inventoryDTO);
}
//更新訂單的confirm方法
public void confirmOrderStatus(Order order) {
order.setStatus(OrderStatusEnum.PAY_SUCCESS.getCode());
orderMapper.update(order);
LOGGER.info("=========進行訂單confirm操作完成================");
}
//更新訂單的cancel方法
public void cancelOrderStatus(Order order) {
order.setStatus(OrderStatusEnum.PAY_FAIL.getCode());
orderMapper.update(order);
LOGGER.info("=========進行訂單cancel操作完成================");
}複製程式碼
makePayment 方法是tcc分散式事務的發起者,它裡面有更新訂單狀態(本地),進行庫存扣減(rpc扣減),資金扣減(rpc呼叫)
confirmOrderStatus 為訂單狀態confirm方法,cancelOrderStatus 為訂單狀態cancel方法。
我們重點關注 @Tcc(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus") 這個註解,為什麼加了這個註解就有神奇的作用。
@Tcc註解切面
- 我們找到在happylifeplat-tcc-core包中找到
TccTransactionAspect,這裡定義@Tcc的切點。
@Aspect
public abstract class TccTransactionAspect {
private TccTransactionInterceptor tccTransactionInterceptor;
public void setTccTransactionInterceptor(TccTransactionInterceptor tccTransactionInterceptor) {
this.tccTransactionInterceptor = tccTransactionInterceptor;
}
@Pointcut("@annotation(com.happylifeplat.tcc.annotation.Tcc)")
public void txTransactionInterceptor() {
}
@Around("txTransactionInterceptor()")
public Object interceptCompensableMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
return tccTransactionInterceptor.interceptor(proceedingJoinPoint);
}
public abstract int getOrder();
}複製程式碼
我們可以知道Spring實現類的方法凡是加了@Tcc註解的,在呼叫的時候,都會進行 tccTransactionInterceptor.interceptor 呼叫。
其次我們發現該類是一個抽象類,肯定會有其他類繼承它,你猜的沒錯,對應dubbo使用者,他的繼承類為:
@Aspect
@Component
public class DubboTccTransactionAspect extends TccTransactionAspect implements Ordered {
@Autowired
public DubboTccTransactionAspect(DubboTccTransactionInterceptor dubboTccTransactionInterceptor) {
super.setTccTransactionInterceptor(dubboTccTransactionInterceptor);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}複製程式碼
- springcloud的使用者,它的繼承類為:
@Aspect
@Component
public class SpringCloudTxTransactionAspect extends TccTransactionAspect implements Ordered {
@Autowired
public SpringCloudTxTransactionAspect(SpringCloudTxTransactionInterceptor springCloudTxTransactionInterceptor) {
this.setTccTransactionInterceptor(springCloudTxTransactionInterceptor);
}
public void init() {
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}複製程式碼
- 我們注意到他們都實現了Spring的Ordered介面,並重寫了 getOrder 方法,都返回了 Ordered.HIGHEST_PRECEDENCE 那麼可以知道,他是優先順序最高的切面。
TccCoordinatorMethodAspect 切面
@Aspect
@Component
public class TccCoordinatorMethodAspect implements Ordered {
private final TccCoordinatorMethodInterceptor tccCoordinatorMethodInterceptor;
@Autowired
public TccCoordinatorMethodAspect(TccCoordinatorMethodInterceptor tccCoordinatorMethodInterceptor) {
this.tccCoordinatorMethodInterceptor = tccCoordinatorMethodInterceptor;
}
@Pointcut("@annotation(com.happylifeplat.tcc.annotation.Tcc)")
public void coordinatorMethod() {
}
@Around("coordinatorMethod()")
public Object interceptCompensableMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
return tccCoordinatorMethodInterceptor.interceptor(proceedingJoinPoint);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}複製程式碼
該切面是第二優先順序的切面,意思就是當我們對有@Tcc註解的方法發起呼叫的時候,首先會進入 TccTransactionAspect 切面,然後再進入 TccCoordinatorMethodAspect 切面。
該切面主要是用來獲取@Tcc註解上的後設資料,比如confrim方法名稱等等。到這裡如果大家基本能明白的話,差不多就瞭解了整個框架原理,現在讓我們來跟蹤呼叫吧。
現在我們回過頭,我們來呼叫 PaymentServiceImpl.makePayment 方法
- dubbo使用者,我們會進入如下攔截器:
@Component
public class DubboTccTransactionInterceptor implements TccTransactionInterceptor {
private final TccTransactionAspectService tccTransactionAspectService;
@Autowired
public DubboTccTransactionInterceptor(TccTransactionAspectService tccTransactionAspectService) {
this.tccTransactionAspectService = tccTransactionAspectService;
}
@Override
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
final String context = RpcContext.getContext().getAttachment(Constant.TCC_TRANSACTION_CONTEXT);
TccTransactionContext tccTransactionContext = null;
if (StringUtils.isNoneBlank(context)) {
tccTransactionContext =
GsonUtils.getInstance().fromJson(context, TccTransactionContext.class);
}
return tccTransactionAspectService.invoke(tccTransactionContext, pjp);
}
}複製程式碼
- 我們繼續跟蹤 tccTransactionAspectService.invoke(tccTransactionContext, pjp) ,發現通過判斷過後,我們會進入 StartTccTransactionHandler.handler 方法,我們已經進入了分散式事務處理的入口了!
/**
* 分散式事務處理介面
*
* @param point point 切點
* @param context 資訊
* @return Object
* @throws Throwable 異常
*/
@Override
public Object handler(ProceedingJoinPoint point, TccTransactionContext context) throws Throwable {
Object returnValue;
try {
//開啟分散式事務
tccTransactionManager.begin();
try {
//發起呼叫 執行try方法
returnValue = point.proceed();
} catch (Throwable throwable) {
//異常執行cancel
tccTransactionManager.cancel();
throw throwable;
}
//try成功執行confirm confirm 失敗的話,那就只能走本地補償
tccTransactionManager.confirm();
} finally {
tccTransactionManager.remove();
}
return returnValue;
}複製程式碼
- 我們進行跟進 tccTransactionManager.begin() 方法:
/**
* 該方法為發起方第一次呼叫
* 也是tcc事務的入口
*/
void begin() {
LogUtil.debug(LOGGER, () -> "開始執行tcc事務!start");
TccTransaction tccTransaction = CURRENT.get();
if (Objects.isNull(tccTransaction)) {
tccTransaction = new TccTransaction();
tccTransaction.setStatus(TccActionEnum.TRYING.getCode());
tccTransaction.setRole(TccRoleEnum.START.getCode());
}
//儲存當前事務資訊
coordinatorCommand.execute(new CoordinatorAction(CoordinatorActionEnum.SAVE, tccTransaction));
CURRENT.set(tccTransaction);
//設定tcc事務上下文,這個類會傳遞給遠端
TccTransactionContext context = new TccTransactionContext();
context.setAction(TccActionEnum.TRYING.getCode());//設定執行動作為try
context.setTransId(tccTransaction.getTransId());//設定事務id
TransactionContextLocal.getInstance().set(context);
}複製程式碼
這裡我們儲存了事務資訊,並且開啟了事務上下文,並把它儲存在了ThreadLoacl裡面,大家想想這裡為什麼一定要儲存在ThreadLocal裡面。
begin方法執行完後,我們回到切面,現在我們來執行 point.proceed(),當執行這一句程式碼的時候,會進入第二個切面,即進入了 TccCoordinatorMethodInterceptor
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
final TccTransaction currentTransaction = tccTransactionManager.getCurrentTransaction();
if (Objects.nonNull(currentTransaction)) {
final TccActionEnum action = TccActionEnum.acquireByCode(currentTransaction.getStatus());
switch (action) {
case TRYING:
registerParticipant(pjp, currentTransaction.getTransId());
break;
case CONFIRMING:
break;
case CANCELING:
break;
}
}
return pjp.proceed(pjp.getArgs());
}複製程式碼
- 這裡由於是在try階段,直接就進入了 registerParticipant 方法
private void registerParticipant(ProceedingJoinPoint point, String transId) throws NoSuchMethodException {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> clazz = point.getTarget().getClass();
Object[] args = point.getArgs();
final Tcc tcc = method.getAnnotation(Tcc.class);
//獲取協調方法
String confirmMethodName = tcc.confirmMethod();
/* if (StringUtils.isBlank(confirmMethodName)) {
confirmMethodName = method.getName();
}*/
String cancelMethodName = tcc.cancelMethod();
/* if (StringUtils.isBlank(cancelMethodName)) {
cancelMethodName = method.getName();
}
*/
//設定模式
final TccPatternEnum pattern = tcc.pattern();
tccTransactionManager.getCurrentTransaction().setPattern(pattern.getCode());
TccInvocation confirmInvocation = null;
if (StringUtils.isNoneBlank(confirmMethodName)) {
confirmInvocation = new TccInvocation(clazz,
confirmMethodName, method.getParameterTypes(), args);
}
TccInvocation cancelInvocation = null;
if (StringUtils.isNoneBlank(cancelMethodName)) {
cancelInvocation = new TccInvocation(clazz,
cancelMethodName,
method.getParameterTypes(), args);
}
//封裝呼叫點
final Participant participant = new Participant(
transId,
confirmInvocation,
cancelInvocation);
tccTransactionManager.enlistParticipant(participant);
}複製程式碼
- 這裡就獲取了 @Tcc(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
的資訊,並把他封裝成了 Participant,存起來,然後真正的呼叫 PaymentServiceImpl.makePayment 業務方法,在業務方法裡面,我們首先執行的是更新訂單狀態(相信現在你還記得0.0):
order.setStatus(OrderStatusEnum.PAYING.getCode());
orderMapper.update(order);複製程式碼
- 接下來,我們進行扣除使用者餘額,注意扣除餘額這裡是一個rpc方法:
AccountDTO accountDTO = new AccountDTO();
accountDTO.setAmount(order.getTotalAmount());
accountDTO.setUserId(order.getUserId());
accountService.payment(accountDTO)複製程式碼
- 現在我們來關注 accountService.payment(accountDTO) ,這個介面的定義。
dubbo介面:
public interface AccountService {
/**
* 扣款支付
*
* @param accountDTO 引數dto
* @return true
*/
@Tcc
boolean payment(AccountDTO accountDTO);
}複製程式碼
springcloud介面
@FeignClient(value = "account-service", configuration = MyConfiguration.class)
public interface AccountClient {
@PostMapping("/account-service/account/payment")
@Tcc
Boolean payment(@RequestBody AccountDTO accountDO);
}複製程式碼
很明顯這裡我們都在介面上加了@Tcc註解,我們知道springAop的特性,在介面上加註解,是無法進入切面的,所以我們在這裡,要採用rpc框架的某些特性來幫助我們獲取到 @Tcc註解資訊。 這一步很重要。當我們發起
accountService.payment(accountDTO) 呼叫的時候:
- dubbo使用者,會走dubbo的filter介面,TccTransactionFilter:
private void registerParticipant(Class clazz, String methodName, Object[] arguments, Class... args) throws TccRuntimeException {
try {
Method method = clazz.getDeclaredMethod(methodName, args);
Tcc tcc = method.getAnnotation(Tcc.class);
if (Objects.nonNull(tcc)) {
//獲取事務的上下文
final TccTransactionContext tccTransactionContext =
TransactionContextLocal.getInstance().get();
if (Objects.nonNull(tccTransactionContext)) {
//dubbo rpc傳引數
RpcContext.getContext()
.setAttachment(Constant.TCC_TRANSACTION_CONTEXT,
GsonUtils.getInstance().toJson(tccTransactionContext));
}
if (Objects.nonNull(tccTransactionContext)) {
if(TccActionEnum.TRYING.getCode()==tccTransactionContext.getAction()){
//獲取協調方法
String confirmMethodName = tcc.confirmMethod();
if (StringUtils.isBlank(confirmMethodName)) {
confirmMethodName = method.getName();
}
String cancelMethodName = tcc.cancelMethod();
if (StringUtils.isBlank(cancelMethodName)) {
cancelMethodName = method.getName();
}
//設定模式
final TccPatternEnum pattern = tcc.pattern();
tccTransactionManager.getCurrentTransaction().setPattern(pattern.getCode());
TccInvocation confirmInvocation = new TccInvocation(clazz,
confirmMethodName,
args, arguments);
TccInvocation cancelInvocation = new TccInvocation(clazz,
cancelMethodName,
args, arguments);
//封裝呼叫點
final Participant participant = new Participant(
tccTransactionContext.getTransId(),
confirmInvocation,
cancelInvocation);
tccTransactionManager.enlistParticipant(participant);
}
}
}
} catch (NoSuchMethodException e) {
throw new TccRuntimeException("not fount method " + e.getMessage());
}
}複製程式碼
- springcloud 使用者,則會進入 TccFeignHandler :
public class TccFeignHandler implements InvocationHandler {
/**
* logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(TccFeignHandler.class);
private Target<?> target;
private Map<Method, MethodHandler> handlers;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
final Tcc tcc = method.getAnnotation(Tcc.class);
if (Objects.isNull(tcc)) {
return this.handlers.get(method).invoke(args);
}
final TccTransactionContext tccTransactionContext =
TransactionContextLocal.getInstance().get();
if (Objects.nonNull(tccTransactionContext)) {
final TccTransactionManager tccTransactionManager =
SpringBeanUtils.getInstance().getBean(TccTransactionManager.class);
if (TccActionEnum.TRYING.getCode() == tccTransactionContext.getAction()) {
//獲取協調方法
String confirmMethodName = tcc.confirmMethod();
if (StringUtils.isBlank(confirmMethodName)) {
confirmMethodName = method.getName();
}
String cancelMethodName = tcc.cancelMethod();
if (StringUtils.isBlank(cancelMethodName)) {
cancelMethodName = method.getName();
}
//設定模式
final TccPatternEnum pattern = tcc.pattern();
tccTransactionManager.getCurrentTransaction().setPattern(pattern.getCode());
final Class<?> declaringClass = method.getDeclaringClass();
TccInvocation confirmInvocation = new TccInvocation(declaringClass,
confirmMethodName,
method.getParameterTypes(), args);
TccInvocation cancelInvocation = new TccInvocation(declaringClass,
cancelMethodName,
method.getParameterTypes(), args);
//封裝呼叫點
final Participant participant = new Participant(
tccTransactionContext.getTransId(),
confirmInvocation,
cancelInvocation);
tccTransactionManager.enlistParticipant(participant);
}
}
return this.handlers.get(method).invoke(args);
}
}
public void setTarget(Target<?> target) {
this.target = target;
}
public void setHandlers(Map<Method, MethodHandler> handlers) {
this.handlers = handlers;
}
}複製程式碼