springcloud集成seata实现分布式事务

news2024/11/28 4:27:47

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

官网:Apache Seata

文章目录

  • 一、部署
    • 1.下载
    • 2.修改配置,nacos作注册中心,db存储
  • 二、集成到springcloud项目
    • 1.引入依赖
    • 2.修改配置
    • 3.新建数据表
    • 4.编写代码
    • 5.测试结果

一、部署

由于网络问题一直拉取docker镜像失败,所以这里采用了下载zip包直接部署的方式

版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub (需要和springcloud的版本对应)

1.下载

直接部署 | Apache Seata

上传服务器并解压

在这里插入图片描述

2.修改配置,nacos作注册中心,db存储

修改conf/application.yml

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 192.168.100.52:4560
    kafka-appender:
      bootstrap-servers: 192.168.100.52:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 192.168.100.53:8848
      namespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466c
      group: spmp-system
      username: nacos
      password: nacos
      data-id: seataServer.properties

  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 192.168.100.53:8848
      group: spmp-system
      namespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466c
      # tc集群名称
      cluster: default
      username: nacos
      password: nacos
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login

此时启动seata服务端,已经可以在nacos服务列表看到seata-server服务

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

这里注意先建数据库seata,然后执行建表sql,脚本在script/server/db/下的mysql.sql

在这里插入图片描述

然后重启seata服务端

可以从seata启动日志 logs/start.out 看到读取配置的相关信息

在这里插入图片描述

二、集成到springcloud项目

这里我们拿项目里其中两个微服务来测试,如图所示,服务1被调用方服务2调用方

在这里插入图片描述

1.引入依赖

两个微服务的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.修改配置

修改两个微服务的配置文件,这里对应上前面seata服务端的配置

seata:
  registry:
      type: nacos
      nacos:
          application: seata-server
          server-addr: 192.168.100.53:8848
          group: spmp-system
          namespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466c
          username: nacos
          password: nacos
  config:
      type: nacos
      nacos:
          server-addr: 192.168.100.53:8848
          group: spmp-system
          namespace: 17a4ea5e-f549-4e4a-97a4-52ee2a9f466c
          dataId: seataServer.properties
          username: nacos
          password: nacos
  tx-service-group: spmp-system

3.新建数据表

两个服务都需要新建undo_log表,在事务回滚时需要用到,建表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

    由于项目里的全局处理器通常都会将所有异常拦截,然后返回统一封装结果,而这会导致异常无法抛出

    /**
     * 全局异常处理器
     *
     * @author ruoyi
     */
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    
        /**
         * 先判断是否是seata全局事务异常,如果是,就直接抛给调用方,让调用方回滚事务
         * @param e
         * @throws Exception
         */
        private void checkSeataError(Exception e) throws Exception {
            log.info("seata全局事务ID: {}", RootContext.getXID());
            // 如果是在一次全局事务里出异常了,就不要包装返回值,将异常抛给调用方,让调用方回滚事务
            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());
        }
    
        /**
         * 业务异常
         */
        @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());
        }
    
        /**
         * 请求参数类型不匹配
         */
        @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("请求参数类型不匹配'{}',发生系统异常.", requestUri, e);
            return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
        }
    
        /**
         * 切面异常统一捕获
         */
        @ExceptionHandler(AspectException.class)
        public ResponseResult<?> handleAspectException(AspectException aspectException) {
            aspectException.printStackTrace();
            return ResponseResult.error(aspectException.getResultStatus(), null);
        }
    
        /**
         * 系统基类异常捕获
         */
        @ExceptionHandler(BasesException.class)
        public ResponseResult<?> handleBasesException(BasesException basesException) throws Exception {
            checkSeataError(basesException);
            basesException.printStackTrace();
            return ResponseResult.error(basesException.getResultStatus(), null);
        }
    
        /**
         * 拦截未知的运行时异常
         */
        @ExceptionHandler(RuntimeException.class)
        public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request) throws Exception {
            checkSeataError(e);
            String requestUri = request.getRequestURI();
            log.error("请求地址'{}',发生未知异常.", requestUri, e);
            return AjaxResult.error(e.getMessage());
        }
    
        /**
         * 系统异常
         */
        @ExceptionHandler(Exception.class)
        public AjaxResult handleException(Exception e, HttpServletRequest request) throws Exception {
            checkSeataError(e);
            String requestUri = request.getRequestURI();
            log.error("请求地址'{}',发生系统异常.", 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);
        }
    
        /**
         * 内部认证异常
         */
        @ExceptionHandler(InnerAuthException.class)
        public AjaxResult handleInnerAuthException(InnerAuthException e) throws Exception {
            checkSeataError(e);
            return AjaxResult.error(e.getMessage());
        }
        
        ......
    }
    
  • 修改Feign熔断降级方法

    由于项目对远程调用接口还做了熔断降级操作,导致调用方仍然识别不到异常,所以这里将熔断降级方法修改下,让其能正常抛异常

    @Component
    @Slf4j
    public class ConstructionProviderFallback implements IConstructionProvider {
        @Override
        public ResponseResult<String> testSeata(Boolean error) {
            if (error) {
                throw new RuntimeException("降级方法中---模拟被调用方异常");
            }
            return ResponseResult.success("----------------testSeata接口远程调用熔断-----------------");
        }
    }
    
  • 启动类增加AOP注解

    由于全局事务注解@GlobalTransactional底层是基于AOP实现,所以需要给两个服务的启动类都加上AOP注解

    @EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)

  • 调用方测试接口

    /**
     * 测试全局事务
     * @return
     */
    @ApiOperation("测试全局事务")
    @GetMapping("/testSeata")
    @ApiImplicitParam(name = "type", value = "1:模拟调用方异常 其他:模拟被调用方异常")
    public ResponseResult<Boolean> testSeata(@RequestParam Integer type) {
        SecurityTest securityTest = new SecurityTest();
        securityTest.setTestColumn("测试全局事务");
        securityTest.setOrganizeId(1L);
        return ResponseResult.success(testSeataService.testSeata(type,securityTest));
    }
    
    @GlobalTransactional
    @Override
    public Boolean testSeata(Integer type, SecurityTest securityTest) {
        log.info("seata全局事务ID: {}", RootContext.getXID());
        if (type!=null&&type==1) {
            //先远程调用construction服务保存远程服务数据
            constructionProvider.testSeata(false);
            //再保存自己服务数据
            securityTestService.save(securityTest);
            //模拟调用方异常
            throw new RuntimeException("模拟调用方异常");
        } else {
            //先保存自己服务数据
            securityTestService.save(securityTest);
            //再远程调用construction服务保存远程服务数据,且模拟被调用方异常
            constructionProvider.testSeata(true);
        }
        return true;
    }
    

    这里测试两种情况,调用方异常事务回滚,还有被调用方异常事务回滚

  • 被调用方提供的Feign接口

    @Service(value = "IConstructionProvider")
    @FeignClient(value = ConstructionProviderConstant.MATE_CLOUD_CONSTRUCTION, fallback = ConstructionProviderFallback.class)
    public interface IConstructionProvider {
    
        /**
         * 测试全局事务
         * @param error
         * @return
         */
        @GetMapping(ConstructionProviderConstant.TEST_SEATA)
        ResponseResult<String> testSeata(@RequestParam("error") Boolean error);
    
    }
    

    这里当时遇到了一个坑

    • 如果不写@RequestParam(“error”) ,会识别成POST请求,然后报错不支持POST请求

    • 如果写了@RequestParam,但是没设置value属性,即写@RequestParam Boolean error,也会报错

      参考:Feign 调用报 RequestParam.value() was empty on parameter 0-CSDN博客

    实现:

    在这里插入图片描述

    正常调用:

    /**
     * 测试Seata全局事务
     * @param error 是否模拟被调用方异常
     * @return
     */
    @Override
    @ApiOperation(value = "测试Seata全局事务", notes = "测试Seata全局事务", 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("模拟被调用方异常");
        }
        return ResponseResult.success("---------------testSeata接口正常------------------");
    }
    

    熔断降级:

    @Override
    public ResponseResult<String> testSeata(Boolean error) {
        if (error) {
            throw new RuntimeException("降级方法中---模拟被调用方异常");
        }
        return ResponseResult.success("----------------testSeata接口远程调用熔断-----------------");
    }
    

5.测试结果

分别测试了调用方异常、被调用方异常的情况,均能实现全局事务回滚(两边的数据库都回滚了),如下图所示

在这里插入图片描述

在这里插入图片描述

下面是seata控制台的信息(存于数据库里)

在这里插入图片描述

这里我测试的结果是 只有调用方和被调用方都有事务回滚 才会有信息,而且会定期清除

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2062609.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

go设计模式——单例模式

概念 单例是一种创建型设计模式&#xff0c;它确保一个类在整个程序运行期间只有一个实例&#xff0c;并提供一个全局访问点来使用该实例。虽然单例模式在某些情况下非常有用&#xff0c;例如管理全局配置、日志记录或资源共享&#xff0c;但它也带来了与全局变量相似的问题。…

【CSS】什么是1px问题,前端如何去解决它,如何画出0.5px边框?

1px 问题概述 在移动端开发中&#xff0c;1px 的边框在高 DPI 屏幕上可能会显得过粗&#xff0c;这是因为移动设备的像素密度&#xff08;DPI&#xff09;通常比传统的计算机屏幕高。在高 DPI 屏幕上&#xff0c;1px 实际上可能会被渲染为 2px 或更多&#xff0c;这使得边框看…

华为手机换ip地址怎么换?手机换ip地址有什么影响

在数字化时代&#xff0c;网络已成为我们生活中不可或缺的一部分。无论是日常沟通、工作学习还是娱乐休闲&#xff0c;我们都离不开互联网。然而&#xff0c;随着网络安全问题的日益突出&#xff0c;如何保护个人隐私和信息安全成为了用户关注的焦点。更换手机IP地址作为提升网…

Vue3+Vite 解决“找不到模块“@/components/xxx.vue”或其相应的类型声明 ts(2307)”

1. 安装插件 pnpm i types/node -D2. 修改vite.config.ts文件 import path from path;resolve: {alias: {"": path.resolve(__dirname,"./src"),},},3. 修改tsconfig.app.json文件 别人教的都是修改tsconfig.json文件&#xff0c;但是我发现可能是因为版…

NVF04M录音芯片在宠物喂食器的应用:录音播放功能,内置SPI闪存

在现代社会中&#xff0c;宠物已经成为人们生活中的一部分&#xff0c;而宠物喂食器作为宠物养护的重要工具&#xff0c;也越来越受到人们的关注。为了满足人们对宠物喂食器的多样化需求&#xff0c;九芯电子供应商研发了一款NVF04M录音芯片。它在宠物喂食器中的作用主要是提供…

巧用PDF转Markdown插件,在扣子(Coze)手搓一个有趣好玩的AI Bot

近期&#xff0c;TextIn团队开发的PDF转Markdown插件已经上架Coze平台。 短短的时间内&#xff0c;已经有不少朋友愉快地和我们的工具开始玩耍。今天我们抛砖引玉&#xff0c;介&#xff08;an&#xff09;绍&#xff08;li&#xff09;几种PDF转Markdown插件的有趣玩法&#…

通用情商智商性格测试ACCESS\EXCEL数据库

今天这个数据库记录数不太多&#xff0c;是个可以进行智商和情商测试的数据&#xff0c;也可以体验比较有趣的测试体验&#xff0c;整个测试主要是以回答不同方面的问题来分析的。智商测试和情商测试均采用国际标准试题&#xff0c;采用国际标准测试题目&#xff0c;通过回答不…

多模态模型评测框架lmms-eval发布!全面覆盖,低成本,零污染

随着大模型研究的深入&#xff0c;如何将其推广到更多的模态上已经成为了学术界和产业界的热点。最近发布的闭源大模型如 GPT-4o、Claude 3.5 等都已经具备了超强的图像理解能力&#xff0c;LLaVA-NeXT、MiniCPM、InternVL 等开源领域模型也展现出了越来越接近闭源的性能。 在…

NSSM 注册exe服务

参考链接&#xff1a;https://www.cnblogs.com/magicMaQaQ/p/18174409 下载NSSM&#xff1a;[NSSM - the Non-Sucking Service Manager](NSSM - the Non-Sucking Service Manager) 解压得到的压缩包 使用管理员权限运行 cmd&#xff0c;来到解压后的目录&#xff0c;执行nssm…

信息学奥赛知识点(十二)----栈和队列

一、栈 栈是只能在某一端插入和删除的特殊线性表。 用桶堆积物品&#xff0c;先堆进行的压在底下&#xff0c;随后一件一件往上堆。取走时&#xff0c;只能从上面一件一件取。堆和取都在顶部进行。底部一般是不动的。 栈就是一种类似桶堆积物品的数据结构&#xff0c;进行删…

Go 1.23 新特性:Timer 和 Ticker 的重要优化

作者&#xff1a;陈明勇 个人网站&#xff1a;https://chenmingyong.cn 文章持续更新&#xff0c;如果本文能让您有所收获&#xff0c;欢迎点赞收藏加关注本号。 微信阅读可搜《程序员陈明勇》。 这篇文章已被收录于 Github&#xff0c;欢迎大 家Star 催更并持续关注。 前言 G…

js 实现对一个元素得拉伸

前言&#xff1a; 最近写一个项目遇到了需要拉伸调整一个元素得大小&#xff08;宽高&#xff09;。所以打算实现一下。 思路就是用 mousedown、mousemove、mouseup 来实现。 mousemove是动态获取坐标&#xff0c;然后 动态改变元素宽度 js自己实现&#xff1a; html里实现…

CentOS7发送邮件如何配置SMTP服务器发信?

CentOS7发送邮件安全设置&#xff1f;CentOS7发信性能优化方法&#xff1f; 对于使用CentOS7操作系统的用户而言&#xff0c;配置SMTP服务器以发送邮件是一个关键步骤。AokSend将详细介绍如何在CentOS7中配置SMTP服务器发信的方法和注意事项。 CentOS7发送邮件&#xff1a;准…

C#发邮件时如何确保邮件内容的安全和隐私?

C#发邮件性能优化的策略&#xff1f;如何设置C#发邮件的功能&#xff1f; 在使用C#发邮件时&#xff0c;如何确保邮件内容不被泄露、篡改或非法访问&#xff0c;已成为开发者需要面对的关键问题。AokSend将探讨在C#发邮件过程中&#xff0c;确保邮件内容安全和隐私的几种有效方…

你也想转行成为一名程序员吗?作为过来人的我希望你想清楚这几个问题再做决定

1 有个朋友突然找我&#xff1a;“现在的工作不想干了&#xff0c;我现在转行搞IT能不能行&#xff1f;学哪个编程语言比较有前景&#xff1f;现在去搞网络安全应该没问题吧&#xff1f;”我相信&#xff0c;很多人出于各种原因都在考虑要不要进行职业转换&#xff0c;迷茫又焦…

2024年最新Pycharm专业版激活码+Pycharm详细安装汉化教程

一、PyCharm激活 激活码&#xff1a; KQ8KMJ77TY-eyJsaWNlbnNlSWQiOiJLUThLTUo3N1RZIiwibGljZW5zZWVOYW1lIjoiVW5pdmVyc2l0YXMgTmVnZXJpIE1hbGFuZyIsImxpY2Vuc2VlVHlwZSI6IkNMQVNTUk9PTSIsImFzc2lnbmVlTmFtZSI6IkpldOWFqOWutuahtiDorqTlh4blupflkI0iLCJhc3NpZ25lZUVtYWlsIjoi…

动态规划part13

647. 回文子串 给定一个字符串&#xff0c;你的任务是计算这个字符串中有多少个回文子串。 具有不同开始位置或结束位置的子串&#xff0c;即使是由相同的字符组成&#xff0c;也会被视作不同的子串。 class Solution:def countSubstrings(self, s: str) -> int:dp [[Fa…

Chat App 项目之解析(九)

Chat App 项目介绍与解析&#xff08;一&#xff09;-CSDN博客文章浏览阅读468次&#xff0c;点赞12次&#xff0c;收藏3次。Chat App 是一个实时聊天应用程序&#xff0c;旨在为用户提供一个简单、直观的聊天平台。该应用程序不仅支持普通用户的注册和登录&#xff0c;还提供了…

ArcGIS小技巧:编辑一个面的边界时,如何让相邻面同步修改

欢迎关注同名微信公众号&#xff0c;更多文章推送&#xff1a; 在ArcGIS中手动编辑2个相邻面的共同边界时&#xff0c;通常需要2个面都跟着修改&#xff1a; 一般做法是用【整形要素工具】&#xff0c;但是【整形要素工具】不能选择多个要素执行&#xff0c;所以需要执行2次&am…

GoFly框架登录Token使用

token是验证请求接口用户身份的标识&#xff0c;框架建议开发者在识别用户身份时使用token尽量不要明文传用户标识数据。 一、生成用户登录token 通过routeuse.GenerateToken生成登录token&#xff0c;方法返回生成的token字符串和err错误提示&#xff0c;其中routeuse.UserC…