1. 程式人生 > 實用技巧 >spring boot:使用分散式事務seata(druid 1.1.23 / seata 1.3.0 / mybatis / spring boot 2.3.2)

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://www.cnblogs.com/architectforest

對應的原始碼可以訪問這裡獲取:https://github.com/liuhongdi/

說明:作者:劉巨集締 郵箱: [email protected]

二,seata-server的安裝:

參見:
https://www.cnblogs.com/architectforest/p/13507695.html

三,演示專案的相關資訊

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)