グローバル例外処理 Seata トランザクション障害ソリューション
最近のプロジェクトでは、seata を使用してグローバル トランザクションを管理しました。テスト中に、サービス A がサービス B を呼び出すときに、ServiceA がエラーを報告した場合、ServiceB はロールバックできるが、ServiceB がエラーを報告した場合、ServiceA はロールバックできないことがわかりました。
調べてみると、システムがグローバル例外を統一的に処理するために、統一された例外情報をフロントエンドに返すことでグローバル例外のインターセプトを行っていることが原因だったことが分かりました。具体的なコードは以下の通りです。
/**
* Controller统一异常处理
*
* @author : 777666
* @date : 2022/01/19
*/
@ControllerAdvice
public class AllControllerAdvice {
private static Logger logger = LoggerFactory.getLogger(AllControllerAdvice.class);
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
}
/**
* 把值绑定到Model中,使全局@RequestMapping可以获取到该值
*/
@ModelAttribute
public void addAttributes(Model model) {
}
/**
* 捕捉BusinessException自定义抛出的异常
*
* @return
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public ResultData handleBusinessException(BusinessException e) {
String message = e.getMessage();
if (message.indexOf(":--:") > 0) {
String[] split = message.split(":--:");
return ResultData.error(split[0], split[1]);
}
return ResultData.error(CodeEnum.DATA_ERROR.getCode(), message, null);
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(value = HttpMessageNotReadableException.class)
@ResponseBody
public ResultData handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
logger.info("参数错误:" + e.getMessage());
return ResultData.error(CodeEnum.PARAM_ERROR.getCode(), e.getMessage(), null);
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public ResultData handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
logger.info("参数错误:" + e.getMessage());
return ResultData.error(CodeEnum.PARAM_ERROR.getCode(), e.getBindingResult().getAllErrors().get(0).getDefaultMessage(), null);
}
/**
* 全局异常捕捉处理
*/
@ResponseBody
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.OK)
public ResultData<String> errorHandler(Exception ex) {
logger.error("接口出现严重异常:{}", ex.getMessage());
return ResultData.error(CodeEnum.ERROR.getMsg());
}
}
これを行う利点の 1 つは、コード内でどのような例外が報告されても、AOP がそれをインターセプトし、英語の例外情報の文字列をフロントエンドに返すのではなく、合意された値をフロントエンドに返すことができることです。
では、シータを使用するときにこれを行うとどのような問題があるのでしょうか?
ServiceAを使ってServiceBを呼び出しますが、このときServiceBが例外をスローすると、ServiceBはロールバックするがServiceAはロールバックしないという現象が発生し、分散トランザクションが失敗したと言えます。
データを確認した結果、例外が内部で処理される (カスタム グローバル例外 AOP によってインターセプトされる) 場合、seata は例外情報を処理しなくなり、ServiceA のトランザクション ロールバックが失敗することがわかりました。
これを理解すると、問題は解決しやすくなります。
手動ロールバックを実行するか、すべての分散トランザクション要求インターフェイスがこの AOP によってインターセプトされないようにします。
AOP 処理については、seata が公式にソリューションを提供しています
ここではこの方法は使いません
まず、 @ControllerAdvice アノテーションを見てみましょう
このアノテーションには、basePackages 属性があります。つまり、この属性が設定されている場合、basePackages で指定されたパッケージで発生する例外のみをインターセプトします。
したがって、basePackages の値を指定するだけで、リモートで呼び出されるすべてのサービスを抽出できます。キーコードは次のとおりです。
グローバル例外インターセプターは、通常のコントローラーの下ですべてのリクエストを処理するだけで済みます。
このときのリモート呼び出しのパッケージ構造は以下の通りです
この時点で再試行し、ServiceB でゼロ除算例外をスローします。
ServiceA はこれを感知した後、ロールバックを実行します。
このアプローチの限界について話しましょう。
ServiceA と ServiceB の両方がグローバル例外を使用して構成されているという前提の下で、ServiceA は ServiceB を呼び出します。
ServiceA がリモート呼び出し用の偽リモート パッケージを除外しているが、ServiceB がそれを除外していない場合、ServiceB の分散例外は内部で処理され、seata はそれを処理しなくなるため、ServiceA はロールバックされません。
1 つは、ServiceA と ServiceB が偽のリモート パッケージを除外するかどうかに関係なく、呼び出し元である ServiceA で例外が発生する限り、呼び出し先である ServiceB のトランザクションはロールバックされるということです (seata の内部原理があまり明確ではないため、特定の状況では、さらに調査するまで理解できませんでした)。
概要: Seata を使用して分散トランザクションを管理する場合、プロジェクト内でグローバル例外処理が使用されている場合は、リモート呼び出しパッケージを除外する必要があります。除外しないと、分散トランザクションが失敗する可能性があります。