品牌展板設(shè)計制作seo免費軟件
Seata 是一款開源的分布式事務(wù)解決方案,致力于在微服務(wù)架構(gòu)下提供高性能和簡單易用的分布式事務(wù)服務(wù)。
官網(wǎng):Apache Seata
文章目錄
- 一、部署
- 1.下載
- 2.修改配置,nacos作注冊中心,db存儲
- 二、集成到springcloud項目
- 1.引入依賴
- 2.修改配置
- 3.新建數(shù)據(jù)表
- 4.編寫代碼
- 5.測試結(jié)果
一、部署
由于網(wǎng)絡(luò)問題一直拉取docker鏡像失敗,所以這里采用了下載zip包直接部署的方式
版本說明 · alibaba/spring-cloud-alibaba Wiki · GitHub (需要和springcloud的版本對應(yīng))
1.下載
直接部署 | Apache Seata
上傳服務(wù)器并解壓
2.修改配置,nacos作注冊中心,db存儲
修改conf/application.yml
server:port: 7091spring:application:name: seata-serverlogging:config: classpath:logback-spring.xmlfile:path: ${user.home}/logs/seataextend:logstash-appender:destination: 192.168.100.52:4560kafka-appender:bootstrap-servers: 192.168.100.52:9092topic: logback_to_logstashconsole:user:username: seatapassword: seataseata:config:# support: nacos, consul, apollo, zk, etcd3type: nacosnacos:server-addr: 192.168.100.53:8848namespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466cgroup: spmp-systemusername: nacospassword: nacosdata-id: seataServer.propertiesregistry:# support: nacos, eureka, redis, zk, consul, etcd3, sofatype: nacosnacos:application: seata-serverserver-addr: 192.168.100.53:8848group: spmp-systemnamespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466c# tc集群名稱cluster: defaultusername: nacospassword: nacos
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'security:secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017tokenValidityInMilliseconds: 1800000ignore:urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
此時啟動seata服務(wù)端,已經(jīng)可以在nacos服務(wù)列表看到seata-server
服務(wù)
cd bin
sh seata-seaver.sh
然后在nacos新建配置文件seataServer.properties
store.mode=db
store.db.dbType=mysql
store.db.datasource=druid
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://192.168.100.52:3306/seata?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai
store.db.user=seata
store.db.password=seata
這里注意先建數(shù)據(jù)庫seata
,然后執(zhí)行建表sql,腳本在script/server/db/
下的mysql.sql
然后重啟seata服務(wù)端
可以從seata啟動日志 logs/start.out
看到讀取配置的相關(guān)信息
二、集成到springcloud項目
這里我們拿項目里其中兩個微服務(wù)來測試,如圖所示,服務(wù)1是被調(diào)用方,服務(wù)2是調(diào)用方
1.引入依賴
兩個微服務(wù)的pom文件里都需要引入seata依賴
<dependency><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId><version>1.6.1</version>
</dependency>
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><version>2021.0.5.0</version><exclusions><exclusion><groupId>io.seata</groupId><artifactId>seata-spring-boot-starter</artifactId></exclusion></exclusions>
</dependency>
2.修改配置
修改兩個微服務(wù)的配置文件,這里對應(yīng)上前面seata服務(wù)端的配置
seata:registry:type: nacosnacos:application: seata-serverserver-addr: 192.168.100.53:8848group: spmp-systemnamespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466cusername: nacospassword: nacosconfig:type: nacosnacos:server-addr: 192.168.100.53:8848group: spmp-systemnamespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466cdataId: seataServer.propertiesusername: nacospassword: nacostx-service-group: spmp-system
3.新建數(shù)據(jù)表
兩個服務(wù)都需要新建undo_log表,在事務(wù)回滾時需要用到,建表sql:
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
4.編寫代碼
-
修改全局異常處理器GlobalExceptionHandler
由于項目里的全局處理器通常都會將所有異常攔截,然后返回統(tǒng)一封裝結(jié)果,而這會導(dǎo)致異常無法拋出
/*** 全局異常處理器** @author ruoyi*/ @RestControllerAdvice public class GlobalExceptionHandler {private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);/*** 先判斷是否是seata全局事務(wù)異常,如果是,就直接拋給調(diào)用方,讓調(diào)用方回滾事務(wù)* @param e* @throws Exception*/private void checkSeataError(Exception e) throws Exception {log.info("seata全局事務(wù)ID: {}", RootContext.getXID());// 如果是在一次全局事務(wù)里出異常了,就不要包裝返回值,將異常拋給調(diào)用方,讓調(diào)用方回滾事務(wù)if (StrUtil.isNotBlank(RootContext.getXID())) {throw e;}}/*** 請求方式不支持*/@ExceptionHandler(HttpRequestMethodNotSupportedException.class)public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e, HttpServletRequest request) throws Exception {checkSeataError(e);String requestUri = request.getRequestURI();log.error("請求地址'{}',不支持'{}'請求", requestUri, e.getMethod());return AjaxResult.error(e.getMessage());}/*** 業(yè)務(wù)異常*/@ExceptionHandler(ServiceException.class)public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request) throws Exception {checkSeataError(e);log.error(e.getMessage(), e);Integer code = e.getCode();return StringUtils.isNotNull(code) ? AjaxResult.error(code, StrUtil.isEmpty(e.getMessage()) ? e.getCause().getMessage() : e.getMessage()) : AjaxResult.error(StrUtil.isEmpty(e.getMessage()) ? e.getCause().getMessage() : e.getMessage());}/*** 請求參數(shù)類型不匹配*/@ExceptionHandler(MethodArgumentTypeMismatchException.class)public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) throws Exception {checkSeataError(e);String requestUri = request.getRequestURI();String value = Convert.toStr(e.getValue());if (StringUtils.isNotEmpty(value)) {value = EscapeUtil.clean(value);}log.error("請求參數(shù)類型不匹配'{}',發(fā)生系統(tǒng)異常.", requestUri, e);return AjaxResult.error(String.format("請求參數(shù)類型不匹配,參數(shù)[%s]要求類型為:'%s',但輸入值為:'%s'", e.getName(), e.getRequiredType().getName(), value));}/*** 切面異常統(tǒng)一捕獲*/@ExceptionHandler(AspectException.class)public ResponseResult<?> handleAspectException(AspectException aspectException) {aspectException.printStackTrace();return ResponseResult.error(aspectException.getResultStatus(), null);}/*** 系統(tǒng)基類異常捕獲*/@ExceptionHandler(BasesException.class)public ResponseResult<?> handleBasesException(BasesException basesException) throws Exception {checkSeataError(basesException);basesException.printStackTrace();return ResponseResult.error(basesException.getResultStatus(), null);}/*** 攔截未知的運(yùn)行時異常*/@ExceptionHandler(RuntimeException.class)public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) throws Exception {checkSeataError(e);String requestUri = request.getRequestURI();log.error("請求地址'{}',發(fā)生未知異常.", requestUri, e);return AjaxResult.error(e.getMessage());}/*** 系統(tǒng)異常*/@ExceptionHandler(Exception.class)public AjaxResult handleException(Exception e, HttpServletRequest request) throws Exception {checkSeataError(e);String requestUri = request.getRequestURI();log.error("請求地址'{}',發(fā)生系統(tǒng)異常.", requestUri, e);return AjaxResult.error(e.getMessage());}/*** 自定義驗證異常*/@ExceptionHandler(BindException.class)public AjaxResult handleBindException(BindException e) throws Exception {checkSeataError(e);log.error(e.getMessage(), e);String message = e.getAllErrors().get(0).getDefaultMessage();return AjaxResult.error(message);}/*** 自定義驗證異常*/@ExceptionHandler(MethodArgumentNotValidException.class)public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) throws Exception {checkSeataError(e);log.error(e.getMessage(), e);String message = e.getBindingResult().getFieldError().getDefaultMessage();return ResponseResult.error(message);}/*** 內(nèi)部認(rèn)證異常*/@ExceptionHandler(InnerAuthException.class)public AjaxResult handleInnerAuthException(InnerAuthException e) throws Exception {checkSeataError(e);return AjaxResult.error(e.getMessage());}...... }
-
修改Feign熔斷降級方法
由于項目對遠(yuǎn)程調(diào)用接口還做了熔斷降級操作,導(dǎo)致調(diào)用方仍然識別不到異常,所以這里將熔斷降級方法修改下,讓其能正常拋異常
@Component @Slf4j public class ConstructionProviderFallback implements IConstructionProvider {@Overridepublic ResponseResult<String> testSeata(Boolean error) {if (error) {throw new RuntimeException("降級方法中---模擬被調(diào)用方異常");}return ResponseResult.success("----------------testSeata接口遠(yuǎn)程調(diào)用熔斷-----------------");} }
-
啟動類增加AOP注解
由于全局事務(wù)注解@GlobalTransactional底層是基于AOP實現(xiàn),所以需要給兩個服務(wù)的啟動類都加上AOP注解
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
-
調(diào)用方測試接口
/*** 測試全局事務(wù)* @return*/ @ApiOperation("測試全局事務(wù)") @GetMapping("/testSeata") @ApiImplicitParam(name = "type", value = "1:模擬調(diào)用方異常 其他:模擬被調(diào)用方異常") public ResponseResult<Boolean> testSeata(@RequestParam Integer type) {SecurityTest securityTest = new SecurityTest();securityTest.setTestColumn("測試全局事務(wù)");securityTest.setOrganizeId(1L);return ResponseResult.success(testSeataService.testSeata(type,securityTest)); }
@GlobalTransactional @Override public Boolean testSeata(Integer type, SecurityTest securityTest) {log.info("seata全局事務(wù)ID: {}", RootContext.getXID());if (type!=null&&type==1) {//先遠(yuǎn)程調(diào)用construction服務(wù)保存遠(yuǎn)程服務(wù)數(shù)據(jù)constructionProvider.testSeata(false);//再保存自己服務(wù)數(shù)據(jù)securityTestService.save(securityTest);//模擬調(diào)用方異常throw new RuntimeException("模擬調(diào)用方異常");} else {//先保存自己服務(wù)數(shù)據(jù)securityTestService.save(securityTest);//再遠(yuǎn)程調(diào)用construction服務(wù)保存遠(yuǎn)程服務(wù)數(shù)據(jù),且模擬被調(diào)用方異常constructionProvider.testSeata(true);}return true; }
這里測試兩種情況,調(diào)用方異常事務(wù)回滾,還有被調(diào)用方異常事務(wù)回滾
-
被調(diào)用方提供的Feign接口
@Service(value = "IConstructionProvider") @FeignClient(value = ConstructionProviderConstant.MATE_CLOUD_CONSTRUCTION, fallback = ConstructionProviderFallback.class) public interface IConstructionProvider {/*** 測試全局事務(wù)* @param error* @return*/@GetMapping(ConstructionProviderConstant.TEST_SEATA)ResponseResult<String> testSeata(@RequestParam("error") Boolean error);}
這里當(dāng)時遇到了一個坑 :
-
如果不寫@RequestParam(“error”) ,會識別成POST請求,然后報錯不支持POST請求
-
如果寫了@RequestParam,但是沒設(shè)置value屬性,即寫@RequestParam Boolean error,也會報錯
參考:Feign 調(diào)用報 RequestParam.value() was empty on parameter 0-CSDN博客
實現(xiàn):
正常調(diào)用:
/*** 測試Seata全局事務(wù)* @param error 是否模擬被調(diào)用方異常* @return*/ @Override @ApiOperation(value = "測試Seata全局事務(wù)", notes = "測試Seata全局事務(wù)", httpMethod = "GET") @GetMapping(ConstructionProviderConstant.TEST_SEATA) @SentinelResource(value = ConstructionProviderConstant.TEST_SEATA, fallbackClass = ConstructionProviderFallback.class, fallback = "testFeign") public ResponseResult<String> testSeata(@RequestParam(value = "error") Boolean error) {SecurityTest1 test = new SecurityTest1();test.setTestColumn("seata");test.setOrganizeId(1L);securityTestService.save(test);if (error) {throw new RuntimeException("模擬被調(diào)用方異常");}return ResponseResult.success("---------------testSeata接口正常------------------"); }
熔斷降級:
@Override public ResponseResult<String> testSeata(Boolean error) {if (error) {throw new RuntimeException("降級方法中---模擬被調(diào)用方異常");}return ResponseResult.success("----------------testSeata接口遠(yuǎn)程調(diào)用熔斷-----------------"); }
-
5.測試結(jié)果
分別測試了調(diào)用方異常、被調(diào)用方異常的情況,均能實現(xiàn)全局事務(wù)回滾(兩邊的數(shù)據(jù)庫都回滾了),如下圖所示
下面是seata控制臺的信息(存于數(shù)據(jù)庫里)
這里我測試的結(jié)果是 只有調(diào)用方和被調(diào)用方都有事務(wù)回滾 才會有信息,而且會定期清除