spring boot:使用分布式事务seata(druid 1.1.23 / seata 1.3.0 / mybatis / spring boot 2.3.2)

一,什么是seata?

Seata:Simpe Extensible Autonomous Transcaction Architecture,
是阿里中间件开源的分布式事务解决方案。
前身是阿里的Fescar
 
官方站:
http://seata.io/zh-cn/ 
官方代码地址:
https://github.com/seata/seata 
官方文档站:
http://seata.io/zh-cn/docs/overview/what-is-seata.html
各版本的release下载地址:
https://github.com/seata/seata/releases

 

说明:刘宏缔的架构森林是一个专注架构的博客,

网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/24/springboot-shi-yong-fen-bu-shi-shi-wu-seatadruid1123seata130mybatisspringboot232/

         对应的源码可以访问这里获取: https://github.com/liuhongdi/

说明:作者:刘宏缔 邮箱: 371125307@qq.com

 

二,seata-server的安装:

参见:

三,演示项目的相关信息

1,项目地址:

https://github.com/liuhongdi/seata

 

2,功能说明:

    分别实现了:同一个项目中不同数据库间的分布式事务

                      用resttemplate访问不同url的分布式事务

    

3,项目结构:

 

四,配置文件说明

1,pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
       <!--seata   begin-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
        <!--seata   end-->
        <!--druid begin-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
            <version>3.4.2</version>
        </dependency>
        <!--druid   end-->
        <!--mybatis begin-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <!--mybatis end-->
        <!--mysql begin-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--mysql end-->

 

2,application.properties

#error
server.error.include-stacktrace=always
#error log
logging.level.org.springframework.web=trace
#app name
spring.application.name = txtest

#   数据源goodsdb基本配置
spring.datasource.druid.goodsdb.username = root
spring.datasource.druid.goodsdb.password = lhddemo
spring.datasource.druid.goodsdb.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.druid.goodsdb.url = jdbc:mysql://127.0.0.1:3306/store?useAffectedRows=true&serverTimezone=UTC
spring.datasource.druid.goodsdb.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.goodsdb.initialSize = 5
spring.datasource.druid.goodsdb.minIdle = 5
spring.datasource.druid.goodsdb.maxActive = 20
spring.datasource.druid.goodsdb.maxWait = 60000
spring.datasource.druid.goodsdb.timeBetweenEvictionRunsMillis = 60000
spring.datasource.druid.goodsdb.minEvictableIdleTimeMillis = 300000
spring.datasource.druid.goodsdb.validationQuery = SELECT 1 FROM DUAL
spring.datasource.druid.goodsdb.testWhileIdle = true
spring.datasource.druid.goodsdb.testOnBorrow = false
spring.datasource.druid.goodsdb.testOnReturn = false
spring.datasource.druid.goodsdb.poolPreparedStatements = true
#   数据源orderdb基本配置
spring.datasource.druid.orderdb.username = root
spring.datasource.druid.orderdb.password = lhddemo
spring.datasource.druid.orderdb.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.druid.orderdb.url = jdbc:mysql://127.0.0.1:3306/orderdb?useAffectedRows=true&serverTimezone=UTC
spring.datasource.druid.orderdb.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.orderdb.initialSize = 5
spring.datasource.druid.orderdb.minIdle = 5
spring.datasource.druid.orderdb.maxActive = 20
spring.datasource.druid.orderdb.maxWait = 60000
spring.datasource.druid.orderdb.timeBetweenEvictionRunsMillis = 60000
spring.datasource.druid.orderdb.minEvictableIdleTimeMillis = 300000
spring.datasource.druid.orderdb.validationQuery = SELECT 1 FROM DUAL
spring.datasource.druid.orderdb.testWhileIdle = true
spring.datasource.druid.orderdb.testOnBorrow = false
spring.datasource.druid.orderdb.testOnReturn = false
spring.datasource.druid.orderdb.poolPreparedStatements = true

#配置监控统计拦截的filters
#stat:监控统计sql
#'wall':sql防火墙
spring.datasource.druid.filters = stat,wall,log4j2
spring.datasource.druid.maxPoolPreparedStatementPerConnectionSize = 20
spring.datasource.druid.useGlobalDataSourceStat = true
spring.datasource.druid.connectionProperties = druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

#druid sql firewall monitor
spring.datasource.druid.filter.wall.enabled=true

#druid sql monitor
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=10000
spring.datasource.druid.filter.stat.merge-sql=true

#druid uri monitor
spring.datasource.druid.web-stat-filter.enabled=true
spring.datasource.druid.web-stat-filter.url-pattern=/*
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*

#druid session monitor
spring.datasource.druid.web-stat-filter.session-stat-enable=true
spring.datasource.druid.web-stat-filter.profile-enable=true

#druid spring monitor
spring.datasource.druid.aop-patterns=com.druid.*

#monintor,druid login user config
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.login-username=root
spring.datasource.druid.stat-view-servlet.login-password=root

#mybatis
mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
mybatis.type-aliases-package=com.example.demo.mapper
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#log
logging.config = classpath:log4j2.xml

#seata
seata.enabled=true
seata.application-id=txtest
seata.tx-service-group=txtest-group
seata.service.vgroup-mapping.txtest-group=default
seata.service.grouplist.default=127.0.0.1:8091
seata.client.undo.log-serialization=jackson
seata.client.undo.log-table=undo_log

说明:因为涉及到两个数据源,spring.datasource.druid用来供生成数据源使用

         seata的配置要注意:

          seata.application-id=txtest: 通常与应用程序的名字(spring.application.name)一致:

          seata.service.grouplist.default的值要和seata server服务的ip:端口一致

         seata.tx-service-group 用来指定所属事务的分组,一台seata server 可管理多个事务组,

        seata.service.vgroup-mapping.txtest-group=default:把服务组命名为default

        seata.service.grouplist.default=127.0.0.1:8091:指定服务组的server地址和端口

 

3, log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
<appenders>
    <Console name="Console" target="SYSTEM_OUT">
        <!--只接受程序中DEBUG级别的日志进行处理-->
        <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
        <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n"/>
    </Console>
    <!--处理INFO级别的日志,并把该日志放到logs/info.log文件中-->
    <RollingFile name="RollingFileInfo" fileName="./logs/info.log"
                 filePattern="logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log.gz">
        <Filters>
            <ThresholdFilter level="INFO"/>
            <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
        </Filters>
        <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n"/>
        <Policies>
            <SizeBasedTriggeringPolicy size="500 MB"/>
            <TimeBasedTriggeringPolicy/>
        </Policies>
    </RollingFile>
    <!--处理WARN级别的日志,并把该日志放到logs/warn.log文件中-->
    <RollingFile name="RollingFileWarn" fileName="./logs/warn.log"
                 filePattern="logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz">
        <Filters>
            <ThresholdFilter level="WARN"/>
            <ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
        </Filters>
        <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n"/>
        <Policies>
            <SizeBasedTriggeringPolicy size="500 MB"/>
            <TimeBasedTriggeringPolicy/>
        </Policies>
    </RollingFile>
    <!--处理error级别的日志,并把该日志放到logs/error.log文件中-->
    <RollingFile name="RollingFileError" fileName="./logs/error.log"
                 filePattern="logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log.gz">
        <ThresholdFilter level="ERROR"/>
        <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n"/>
        <Policies>
            <SizeBasedTriggeringPolicy size="500 MB"/>
            <TimeBasedTriggeringPolicy/>
        </Policies>
    </RollingFile>
    <!--druid的日志记录追加器-->
    <RollingFile name="druidSqlRollingFile" fileName="./logs/druid-sql.log"
                 filePattern="logs/$${date:yyyy-MM}/api-%d{yyyy-MM-dd}-%i.log.gz">
        <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%file:%line] %-5level %logger{35} - %msg %n"/>
        <Policies>
            <SizeBasedTriggeringPolicy size="500 MB"/>
            <TimeBasedTriggeringPolicy/>
        </Policies>
    </RollingFile>
</appenders>
<loggers>
    <AsyncRoot level="info">
        <appender-ref ref="Console"/>
        <appender-ref ref="RollingFileInfo"/>
        <appender-ref ref="RollingFileWarn"/>
        <appender-ref ref="RollingFileError"/>
    </AsyncRoot>
    <!--记录druid-sql的记录-->
    <AsyncLogger name="druid.sql.Statement" level="debug" additivity="false">
        <appender-ref ref="druidSqlRollingFile"/>
    </AsyncLogger>
</loggers>
</configuration>

 

4,    两个数据库中的数据表:

goods表

复制代码
CREATE TABLE `goods` (
 `goodsId` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
 `goodsName` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'name',
 `subject` varchar(200) NOT NULL DEFAULT '' COMMENT '标题',
 `price` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
 `stock` int(11) NOT NULL DEFAULT '0' COMMENT 'stock',
 PRIMARY KEY (`goodsId`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品表'
复制代码

goods表中的数据:

INSERT INTO `goods` (`goodsId`, `goodsName`, `subject`, `price`, `stock`) VALUES
(3, '100分电动牙刷', '好用到让你爱上刷牙', '59.00', 100);

order表:

复制代码
CREATE TABLE `orderinfo` (
 `orderId` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
 `orderSn` varchar(100) NOT NULL DEFAULT '' COMMENT '编号',
 `orderTime` timestamp NOT NULL DEFAULT '1971-01-01 00:00:01' COMMENT '下单时间',
 `orderStatus` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0,未支付,1,已支付,2,已发货,3,已退货,4,已过期',
 `userId` int(12) NOT NULL DEFAULT '0' COMMENT '用户id',
 `price` decimal(10,0) NOT NULL DEFAULT '0' COMMENT '价格',
 `addressId` int(12) NOT NULL DEFAULT '0' COMMENT '地址',
 PRIMARY KEY (`orderId`),
 UNIQUE KEY `orderSn` (`orderSn`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表'
复制代码

 

5,在每个库中创建seata回滚数据时要用到的undo_log数据表

CREATE TABLE `undo_log` (
 `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
 `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
 `xid` varchar(100) NOT NULL COMMENT 'global transaction id',
 `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
 `rollback_info` longblob NOT NULL COMMENT 'rollback info',
 `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
 `log_created` datetime NOT NULL COMMENT 'create datetime',
 `log_modified` datetime NOT NULL COMMENT 'modify datetime',
 PRIMARY KEY (`id`),
 UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table'

 

五,java代码说明

1,GoodsdbSourceConfig.java

@Configuration
@MapperScan(basePackages = "com.seata.demo.mapper.goodsdb", sqlSessionTemplateRef = "goodsdbSqlSessionTemplate")
public class GoodsdbSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.druid.goodsdb")
    public DataSource goodsdbDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public SqlSessionFactory goodsdbSqlSessionFactory(@Qualifier("goodsdbDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //bean.setDataSource();
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/goodsdb/*.xml"));
        return bean.getObject();
    }

    @Bean
    @Primary
    public DataSourceTransactionManager goodsdbTransactionManager(@Qualifier("goodsdbDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    @Primary
    public SqlSessionTemplate goodsdbSqlSessionTemplate(@Qualifier("goodsdbSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

 

2,OrderdbSourceConfig.java

@Configuration
@MapperScan(basePackages = "com.seata.demo.mapper.orderdb", sqlSessionTemplateRef = "orderdbSqlSessionTemplate")
public class OrderdbSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid.orderdb")
    public DataSource orderdbDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    public SqlSessionFactory orderdbSqlSessionFactory(@Qualifier("orderdbDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/orderdb/*.xml"));
        return bean.getObject();
    }

    @Bean
    public DataSourceTransactionManager orderdbTransactionManager(@Qualifier("orderdbDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public SqlSessionTemplate orderdbSqlSessionTemplate(@Qualifier("orderdbSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

说明:以上分别为goodsdb/orderdb创建两个数据源

 

3,GoodsController.java

@RestController
@RequestMapping("/goods")
public class GoodsController {

    private static final String SUCCESS = "SUCCESS";
    private static final String FAIL = "FAIL";

    @Resource
    private GoodsMapper goodsMapper;

    //更新商品库存 参数:商品id,数量
    @RequestMapping("/goodsstock/{goodsId}/{count}")
    @ResponseBody
    public String goodsStock(@PathVariable Long goodsId,
                            @PathVariable int count) {
         int res = goodsMapper.updateGoodsStock(goodsId,count);
         System.out.println("res:"+res);

         if (res>0) {
             return SUCCESS;
         } else {
             return FAIL;
         }
    }
}

rest方式调用访问goodsdb库时使用

 

4,OrderController.java

@RestController
@RequestMapping("/order")
public class OrderController {

    private static final String SUCCESS = "SUCCESS";
    private static final String FAIL = "FAIL";

    @Resource
    private OrderMapper orderMapper;

    //添加订单:参数:商品id,数量
    @RequestMapping("/orderadd/{goodsId}/{count}")
    @ResponseBody
    public String orderAdd(@PathVariable Long goodsId,
                             @PathVariable int count) {
        Order order = new Order();
        //得到sn
        String orderSn = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        order.setOrderSn(orderSn);
        order.setOrderStatus(0);
        order.setPrice(new BigDecimal(100.00));
        order.setUserId(8);

        int orderId = orderMapper.insertOneOrder(order);
        System.out.println("orderId:"+order.getOrderId());

        if (orderId>0) {
            return SUCCESS;
        } else {
            return FAIL;
        }
    }
}

rest方式访问orderdb库时使用

 

5,HomeController.java

@RestController
@RequestMapping("/home")
public class HomeController {

    private static final String SUCCESS = "SUCCESS";
    private static final String FAIL = "FAIL";

    @Resource
    private OrderMapper orderMapper;

    @Resource
    private GoodsMapper goodsMapper;

    //添加一个订单,访问两个数据库
    @GlobalTransactional(timeoutMills = 300000,rollbackFor = Exception.class)
    @GetMapping("/addorderseata")
    public String addOrderSeata(@RequestParam(value="isfail",required = true,defaultValue = "0") int isFail) {

        String goodsId = "3";
        String goodsNum = "1";

        Order order = new Order();
        //生成订单
        String orderSn = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
        order.setOrderSn(orderSn);
        order.setOrderStatus(0);
        order.setPrice(new BigDecimal(100.00));
        order.setUserId(8);
        int orderId = orderMapper.insertOneOrder(order);
        System.out.println("orderId:"+order.getOrderId());
        //更新商品库存
        int count = -1;
        int res = goodsMapper.updateGoodsStock(Long.parseLong(goodsId),count);
        System.out.println("res:"+res);
        //测试失败的情况
        if (isFail == 1) {
            int divide = 0;
            int resul = 100 / divide;
        }

        if (res>0) {
            return SUCCESS;
        } else {
            return FAIL;
        }
    }


    //添加一个订单,访问两个url(分别访问不同的数据库)
    @GlobalTransactional(timeoutMills = 300000,rollbackFor = Exception.class)
    @GetMapping("/addorderseatarest")
    public String addOrderSeataRest(@RequestParam(value="isfail",required = true,defaultValue = "0") int isFail) {
        String goodsId = "3";
        String goodsNum = "1";
        //得到事务的xid
        RestTemplate restTemplate = new RestTemplate();
        String xid = RootContext.getXID();
        System.out.println("xid before send:"+xid);
        if (StringUtils.isEmpty(xid)) {
            System.out.println("xid is null,return");
            return FAIL;
        }
        //把事务的xid添加到header
        HttpHeaders headers = new HttpHeaders();
        headers.add(RootContext.KEY_XID, xid);
        System.out.println("xid not null");
        //生成订单,传递xid
        String urlAddOrder = "http://127.0.0.1:8080/order/orderadd/"+goodsId+"/"+goodsNum+"/";
        String resultAdd = restTemplate.postForObject(urlAddOrder,new HttpEntity<String>(headers),String.class);
        if (!SUCCESS.equals(resultAdd)) {
            throw new RuntimeException();
        }
        //更新商品库存,传递xid
        String goodsUPNum = "-1";
        String urlUpStock = "http://127.0.0.1:8080/goods/goodsstock/"+goodsId+"/"+goodsUPNum+"/";
        String resultUp = restTemplate.postForObject(urlUpStock,new HttpEntity<String>(headers),String.class);
        if (!SUCCESS.equals(resultUp)) {
            throw new RuntimeException();
        }
        //测试失败的情况
        if (isFail == 1) {
            int divide = 0;
            int resul = 100 / divide;
        }
        return SUCCESS;
    }
}

需要注意的地方在于:用resttemplate访问其他url时,需要用xid传递全局事务

否则事务会不生效

 

6,SeataFilter.java

@Component
public class SeataFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String xid = req.getHeader(RootContext.KEY_XID.toLowerCase());
        System.out.println("xid:"+xid);
        boolean isBind = false;
        if (StringUtils.isNotBlank(xid)) {
            //如果xid不为空,则RootContext需要绑定xid,
            //供seata识别这是同一个分布式事务
            RootContext.bind(xid);
            isBind = true;
        }
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            if (isBind) {
                RootContext.unbind();
            }
        }
    }
    @Override
    public void destroy() {
    }
}

这个过滤器主要用来保存xid值

如果使用seata的spring-cloud starter包,则不需要做这个工作

 

7,GoodsMapper.java

@Repository
@Mapper
public interface GoodsMapper {
    //更新库存
    int updateGoodsStock(@Param("goodsId") Long goodsId, @Param("changeAmount") int changeAmount);
}

 

8,OrderMapper.java

@Repository
@Mapper
public interface OrderMapper {
    //插入一条订单
    int insertOneOrder(Order order);
}

 

9,Goods.java/Order.java/GoodsMapper.xml/OrderMapper.xml
   为节省篇幅,这些代码请从github上查看

 

六,测试效果

1,添加一个订单:
先测试事务成功的情况:
查看orderdb中undo_log表的下一个自增值:
下一个自增值 70
id为3的商品当前库存:100
 
访问:
http://127.0.0.1:8080/home/addorderseata

orderinfo表中增加了一条订单记录
查看id为3的商品库存:99

查看控制台:
2020-08-19 18:04:24.962 [http-nio-8080-exec-2] [:] INFO  io.seata.tm.TransactionManagerHolder - TransactionManager Singleton io.seata.tm.DefaultTransactionManager@6dc18e9e 
2020-08-19 18:04:25.016 [http-nio-8080-exec-2] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - Begin new global transaction [192.168.3.163:8091:39410791815843840] 
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5f4f6f65] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@5ef86b80] will not be managed by Spring
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5f4f6f65]
orderId:96
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@624dddb2] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@307310b5] will not be managed by Spring
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@624dddb2]
res:1
2020-08-19 18:04:25.991 [http-nio-8080-exec-2] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - [192.168.3.163:8091:39410791815843840] commit status: Committed 
2020-08-19 18:04:26.045 [http-nio-8080-exec-2] [AbstractMessageConverterMethodProcessor.java:265] DEBUG org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json] 
2020-08-19 18:04:26.074 [http-nio-8080-exec-2] [FrameworkServlet.java:1131] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK, headers={masked} 
2020-08-19 18:04:26.622 [rpcDispatch_RMROLE_1_1_4] [:] INFO  io.seata.core.rpc.processor.client.RmBranchCommitProcessor - rm client handle branch commit process:xid=192.168.3.163:8091:39410791815843840,branchId=39410794303066112,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/orderdb,applicationData=null 
2020-08-19 18:04:26.626 [rpcDispatch_RMROLE_1_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch committing: 192.168.3.163:8091:39410791815843840 39410794303066112 jdbc:mysql://127.0.0.1:3306/orderdb null 
2020-08-19 18:04:26.627 [rpcDispatch_RMROLE_1_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch commit result: PhaseTwo_Committed 
2020-08-19 18:04:26.642 [rpcDispatch_RMROLE_1_2_4] [:] INFO  io.seata.core.rpc.processor.client.RmBranchCommitProcessor - rm client handle branch commit process:xid=192.168.3.163:8091:39410791815843840,branchId=39410795796238336,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/store,applicationData=null 
2020-08-19 18:04:26.643 [rpcDispatch_RMROLE_1_2_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch committing: 192.168.3.163:8091:39410791815843840 39410795796238336 jdbc:mysql://127.0.0.1:3306/store null 
2020-08-19 18:04:26.643 [rpcDispatch_RMROLE_1_2_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch commit result: PhaseTwo_Committed 

可以看到GlobalTransaction的begin 和commit 记录

 
测试失败的测试:访问:
http://127.0.0.1:8080/home/addorderseata?isfail=1

在这里发生了一次除0错,

查看orderinfo表:记录未插入

查看id为3的商品库存:99,没发生变化

查看orderdb中undo_log表的下一个自增值:
下一个自增值    72
查看控制台:
2020-08-19 18:10:30.582 [http-nio-8080-exec-5] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - Begin new global transaction [192.168.3.163:8091:39412325219831808] 
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5de874f6] was not registered for synchronization because synchronization is not active
2020-08-19 18:10:30.595 [http-nio-8080-exec-5] [:] ERROR com.alibaba.druid.pool.DruidAbstractDataSource - discard long time none received connection. , jdbcUrl : jdbc:mysql://127.0.0.1:3306/orderdb?useAffectedRows=true&serverTimezone=UTC, jdbcUrl : jdbc:mysql://127.0.0.1:3306/orderdb?useAffectedRows=true&serverTimezone=UTC, lastPacketReceivedIdleMillis : 362967 
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@723d744f] will not be managed by Spring
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5de874f6]
orderId:97
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@460d292b] was not registered for synchronization because synchronization is not active
2020-08-19 18:10:30.717 [http-nio-8080-exec-5] [:] ERROR com.alibaba.druid.pool.DruidAbstractDataSource - discard long time none received connection. , jdbcUrl : jdbc:mysql://127.0.0.1:3306/store?useAffectedRows=true&serverTimezone=UTC, jdbcUrl : jdbc:mysql://127.0.0.1:3306/store?useAffectedRows=true&serverTimezone=UTC, lastPacketReceivedIdleMillis : 363066 
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@1f25e694] will not be managed by Spring
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@460d292b]
res:1
2020-08-19 18:10:30.812 [rpcDispatch_RMROLE_1_3_4] [:] INFO  io.seata.core.rpc.processor.client.RmBranchRollbackProcessor - rm handle branch rollback process:xid=192.168.3.163:8091:39412325219831808,branchId=39412325991583744,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/store,applicationData=null 
2020-08-19 18:10:30.813 [rpcDispatch_RMROLE_1_3_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacking: 192.168.3.163:8091:39412325219831808 39412325991583744 jdbc:mysql://127.0.0.1:3306/store 
2020-08-19 18:10:30.962 [rpcDispatch_RMROLE_1_3_4] [:] INFO  io.seata.rm.datasource.undo.AbstractUndoLogManager - xid 192.168.3.163:8091:39412325219831808 branch 39412325991583744, undo_log deleted with GlobalFinished 
2020-08-19 18:10:30.964 [rpcDispatch_RMROLE_1_3_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacked result: PhaseTwo_Rollbacked 
2020-08-19 18:10:30.976 [rpcDispatch_RMROLE_1_4_4] [:] INFO  io.seata.core.rpc.processor.client.RmBranchRollbackProcessor - rm handle branch rollback process:xid=192.168.3.163:8091:39412325219831808,branchId=39412325689593856,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/orderdb,applicationData=null 
2020-08-19 18:10:30.976 [rpcDispatch_RMROLE_1_4_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacking: 192.168.3.163:8091:39412325219831808 39412325689593856 jdbc:mysql://127.0.0.1:3306/orderdb 
2020-08-19 18:10:31.012 [rpcDispatch_RMROLE_1_4_4] [:] INFO  io.seata.rm.datasource.undo.AbstractUndoLogManager - xid 192.168.3.163:8091:39412325219831808 branch 39412325689593856, undo_log deleted with GlobalFinished 
2020-08-19 18:10:31.014 [rpcDispatch_RMROLE_1_4_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacked result: PhaseTwo_Rollbacked 
2020-08-19 18:10:31.035 [http-nio-8080-exec-5] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - [192.168.3.163:8091:39412325219831808] rollback status: Rollbacked 

可以看到事务的begin和rollback

 
 
2,用rest方式测试事务的执行:
  访问:
http://127.0.0.1:8080/home/addorderseatarest?isfail=1

可以观察到undo_log中自增值的变化和控制台的输出
与上一个例子基本一致,大家自己观察效果即可

 

七,查看spring boot 的版本:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.2.RELEASE)

 

posted @ 2020-08-19 19:01  刘宏缔的架构森林  阅读(1967)  评论(0编辑  收藏  举报