zoukankan      html  css  js  c++  java
  • 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/

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

     

    二,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)
  • 相关阅读:
    适用于Bash编程初学者小例子
    Linux下的压缩与解压命令速查
    Linux下拷贝一个带有soft link的dir,会把被link的内容也拷贝过来吗?
    适用于Bash编程初学者小例子
    从一个git仓库迁移代码到另一个git仓库(亲测有效版)(转)
    海量数据面试题(附题解+方法总结)(转)
    leetcode刷题(六)路径总和I、II、III(转)
    浅谈AVL树,红黑树,B树,B+树原理及应用(转)
    [LeetCode] 860. Lemonade Change 买柠檬找零(转)
    [leetcode] 134. Gas Station 解题报告(转)
  • 原文地址:https://www.cnblogs.com/architectforest/p/13531167.html
Copyright © 2011-2022 走看看