spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)

一,dynamic-datasource-spring-boot-starter的优势?

1,dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器

它由苞米豆团队出品,集成多数据源时非常方便

2,官方站及文档:

官方站

https://mybatis.plus/

官方代码站:

https://gitee.com/baomidou/dynamic-datasource-spring-boot-starter

官方文档站:

https://mybatis.plus/guide/dynamic-datasource.html

3,seata的用途:

Seata:Simpe Extensible Autonomous Transcaction Architecture,
是阿里中间件,开源的分布式事务解决方案
官方站:
http://seata.io/zh-cn/

说明:刘宏缔的架构森林是一个专注架构的博客,地址: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/dynamicseata

2,项目功能说明:

        用dynamic-datasource-spring-boot-starter整合两个数据源+mybatis+druid+seata实现分布式事务

3,项目结构:如图:

 spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)

4,用到的数据库:

spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)

spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.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.1.0</version>
        </dependency>
        <!--dynamic datasource begin-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--dynamic datasource   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
logging.level.org.springframework.web=trace
#name
spring.application.name = my_test_tx
# orderdb设置为主数据源
spring.datasource.dynamic.primary = orderdb
spring.datasource.dynamic.seata = true
# orderdb数据源配置
spring.datasource.dynamic.datasource.orderdb.url = jdbc:mysql://127.0.0.1:3306/orderdb?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.dynamic.datasource.orderdb.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.orderdb.username = root
spring.datasource.dynamic.datasource.orderdb.password = lhddemo
spring.datasource.dynamic.datasource.orderdb.type= com.alibaba.druid.pool.DruidDataSource
spring.datasource.dynamic.datasource.orderdb.druid.initial-size=5
spring.datasource.dynamic.datasource.orderdb.druid.max-active=20
spring.datasource.dynamic.datasource.orderdb.druid.min-idle=5
spring.datasource.dynamic.datasource.orderdb.druid.max-wait=60000
spring.datasource.dynamic.datasource.orderdb.druid.min-evictable-idle-time-millis=300000
spring.datasource.dynamic.datasource.orderdb.druid.max-evictable-idle-time-millis=300000
spring.datasource.dynamic.datasource.orderdb.druid.time-between-eviction-runs-millis=60000
spring.datasource.dynamic.datasource.orderdb.druid.validation-query=select 1
spring.datasource.dynamic.datasource.orderdb.druid.validation-query-timeout=-1
spring.datasource.dynamic.datasource.orderdb.druid.test-on-borrow=false
spring.datasource.dynamic.datasource.orderdb.druid.test-on-return=false
spring.datasource.dynamic.datasource.orderdb.druid.test-while-idle=true
spring.datasource.dynamic.datasource.orderdb.druid.pool-prepared-statements=true
spring.datasource.dynamic.datasource.orderdb.druid.filters=stat,wall,log4j2
spring.datasource.dynamic.datasource.orderdb.druid.share-prepared-statements=true
# goodsdb数据源配置
spring.datasource.dynamic.datasource.goodsdb.url = jdbc:mysql://127.0.0.1:3306/store?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.dynamic.datasource.goodsdb.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.dynamic.datasource.goodsdb.username = root
spring.datasource.dynamic.datasource.goodsdb.password = lhddemo
spring.datasource.dynamic.datasource.goodsdb.type= com.alibaba.druid.pool.DruidDataSource
spring.datasource.dynamic.datasource.goodsdb.druid.initial-size=5
spring.datasource.dynamic.datasource.goodsdb.druid.max-active=20
spring.datasource.dynamic.datasource.goodsdb.druid.min-idle=5
spring.datasource.dynamic.datasource.goodsdb.druid.max-wait=60000
spring.datasource.dynamic.datasource.goodsdb.druid.min-evictable-idle-time-millis=300000
spring.datasource.dynamic.datasource.goodsdb.druid.max-evictable-idle-time-millis=300000
spring.datasource.dynamic.datasource.goodsdb.druid.time-between-eviction-runs-millis=60000
spring.datasource.dynamic.datasource.goodsdb.druid.validation-query=select 1
spring.datasource.dynamic.datasource.goodsdb.druid.validation-query-timeout=-1
spring.datasource.dynamic.datasource.goodsdb.druid.test-on-borrow=false
spring.datasource.dynamic.datasource.goodsdb.druid.test-on-return=false
spring.datasource.dynamic.datasource.goodsdb.druid.test-while-idle=true
spring.datasource.dynamic.datasource.goodsdb.druid.pool-prepared-statements=true
spring.datasource.dynamic.datasource.goodsdb.druid.filters=stat,wall,log4j2
spring.datasource.dynamic.datasource.goodsdb.druid.share-prepared-statements=true
#   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
#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
# IP白名单 (没有配置或者为空,则允许所有访问)
spring.datasource.druid.stat-view-servlet.allow = 127.0.0.1,192.168.163.1
# IP黑名单 (存在共同时,deny优先于allow)
spring.datasource.druid.stat-view-servlet.deny = 192.168.10.1
#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.application-id=my_test_tx
seata.tx-service-group=my_test_tx_group

seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091

说明:因为是用dynamic-datasource来整合seata,需要配置:

         spring.datasource.dynamic.seata = true

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

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

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

3,数据库的相关业务表:

goods表

spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)
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='商品表'
spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)

goods表中的数据:

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

order表:

spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)
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='订单表'
spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)

 每个库中seata要使用的事务恢复日志表:

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,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() {
    }
}

通过rest方式访问url时,分布式事务需要传递事务的xid

2,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
    @DS("goodsdb")
    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;
         }
    }

    //商品详情 参数:商品id
    @GetMapping("/goodsinfo")
    @ResponseBody
    @DS("goodsdb")
    public Goods goodsInfo(@RequestParam(value="goodsid",required = true,defaultValue = "0") Long goodsId) {
        Goods goods = goodsMapper.selectOneGoods(goodsId);
        return goods;
    }
}

3,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);
        if (orderId>0) {
            return SUCCESS;
        } else {
            return FAIL;
        }
    }
    
    //订单详情,参数:订单id
    @GetMapping("/orderinfo")
    @ResponseBody
    public Order orderInfo(@RequestParam(value="orderid",required = true,defaultValue = "0") Long orderId) {
        Order order = orderMapper.selectOneOrder(orderId);
        return order;
    }
}

4,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 GoodsService goodsService;

    //添加一个订单,直接访问数据库
    @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);
        //修改数据库中商品的库存
        int goodsUPNum = -1;
        String res = goodsService.goodsStock(Long.parseLong(goodsId),goodsUPNum);
        //是否要引发异常
        if (isFail == 1) {
            int divide = 0;
            int resul = 100 / divide;
        }
        return SUCCESS;
    }

    //添加一个订单,rest访问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";
        RestTemplate restTemplate = new RestTemplate();
        
        //得到事务的xid
        String xid = RootContext.getXID();
        System.out.println("xid before send:"+xid);
        if (StringUtils.isEmpty(xid)) {
            System.out.println("xid is null,return");
            return FAIL;
        }

        //增加一条订单的记录
        HttpHeaders headers = new HttpHeaders();
        headers.add(RootContext.KEY_XID, xid);
        System.out.println("xid not null");
        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();
        }

        //修改数据库中商品的库存
        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;
    }
}

说明:@GlobalTransactional 注解用来生成分布式事务

         @DS注解用来指定goodsdb这个库,

         因为orderdb被设置为了primary,所以无需指定

5,DemoApplication.java

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

说明:因为我们使用了druid-spring-boot-starter依赖包,

         druid会自动检查数据库的url配置,而我们使用了多个数据源,

         所以要exclude掉DruidDataSourceAutoConfigure这个class

6,GoodsService.java

@Service
public class GoodsService {

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

    @Resource
    private GoodsMapper goodsMapper;
    
//更新数据库 @DS(
"goodsdb") public String goodsStock(Long goodsId, int count) { int res = goodsMapper.updateGoodsStock(goodsId,count); System.out.println("res:"+res); if (res>0) { return SUCCESS; } else { return FAIL; } } }

说明:要用DS注解指定goodsdb这个库

7, OrderMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dynamicseata.demo.mapper.orderdb.OrderMapper">
    <select id="selectOneOrder" parameterType="long" resultType="com.dynamicseata.demo.pojo.Order">
        select * from orderinfo where orderId=#{orderId}
    </select>
    <insert id="insertOneOrder" parameterType="com.dynamicseata.demo.pojo.Order" useGeneratedKeys="true" keyProperty="orderId" >
        insert into orderinfo(orderSn,orderTime,orderStatus,userId,price)
        values(
            #{orderSn},now(),#{orderStatus},#{userId},#{price}
        )
     </insert>
</mapper>

8,GoodsMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dynamicseata.demo.mapper.goodsdb.GoodsMapper">
    <select id="selectOneGoods" parameterType="long" resultType="com.dynamicseata.demo.pojo.Goods">
        select * from goods where goodsId=#{goodsId}
    </select>
    <update id="updateGoodsStock">
        UPDATE goods SET
        stock = stock+#{changeAmount,jdbcType=INTEGER}
        WHERE goodsId = #{goodsId,jdbcType=BIGINT}
    </update>
</mapper>

9,Goods.java,Order.java,GoodsMapper.java,OrderMapper.java
  等代码,可以访问github.com

六,测试效果

1,测试seata访问两个库的事务效果:

先查看goodsdb数据库中id为3商品的库存:100

访问成功效果的url:

http://127.0.0.1:8080/home/addorderseata

访问后:可以看到库存变成了99,

而且orderdb订单中新插入了订单记录

查看控制台中关于分布式事务的输出:

2020-08-21 19:22:09.447 [http-nio-8080-exec-1] [:] INFO  org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet' 
2020-08-21 19:22:09.448 [http-nio-8080-exec-1] [FrameworkServlet.java:525] INFO  org.springframework.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet' 
2020-08-21 19:22:09.480 [http-nio-8080-exec-1] [FrameworkServlet.java:542] DEBUG org.springframework.web.servlet.DispatcherServlet - enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data 
2020-08-21 19:22:09.480 [http-nio-8080-exec-1] [FrameworkServlet.java:547] INFO  org.springframework.web.servlet.DispatcherServlet - Completed initialization in 32 ms 
xid:null
2020-08-21 19:22:09.526 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load ContextCore[null] extension by class[io.seata.core.context.FastThreadLocalContextCore] 
2020-08-21 19:22:09.560 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load TransactionManager[null] extension by class[io.seata.tm.DefaultTransactionManager] 
2020-08-21 19:22:09.560 [http-nio-8080-exec-1] [:] INFO  io.seata.tm.TransactionManagerHolder - TransactionManager Singleton io.seata.tm.DefaultTransactionManager@1b211325 
2020-08-21 19:22:09.563 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load LoadBalance[null] extension by class[io.seata.discovery.loadbalance.RandomLoadBalance] 
2020-08-21 19:22:09.564 [http-nio-8080-exec-1] [:] INFO  io.seata.core.rpc.netty.NettyClientChannelManager - will connect to 127.0.0.1:8091 
2020-08-21 19:22:09.565 [http-nio-8080-exec-1] [:] INFO  io.seata.core.rpc.netty.NettyPoolableFactory - NettyPool create channel to transactionRole:TMROLE,address:127.0.0.1:8091,msg:< RegisterTMRequest{applicationId='my_test_tx', transactionServiceGroup='my_test_tx_group'} > 
2020-08-21 19:22:09.609 [http-nio-8080-exec-1] [:] INFO  io.seata.core.rpc.netty.NettyPoolableFactory - register success, cost 32 ms, version:1.3.0,role:TMROLE,channel:[id: 0xd46743ae, L:/127.0.0.1:45978 - R:/127.0.0.1:8091] 
2020-08-21 19:22:09.625 [http-nio-8080-exec-1] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - Begin new global transaction [192.168.3.173:8091:40155132419117056] 
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d9820f] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@7bd566ba] will not be managed by Spring
==>  Preparing: insert into orderinfo(orderSn,orderTime,orderStatus,userId,price) values( ?,now(),?,?,? )
==> Parameters: 20200821192209638(String), 0(Integer), 8(Integer), 100(BigDecimal)
2020-08-21 19:22:09.915 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load SQLRecognizerFactory[druid] extension by class[io.seata.sqlparser.druid.DruidDelegatingSQLRecognizerFactory] 
2020-08-21 19:22:09.994 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load SQLOperateRecognizerHolder[mysql] extension by class[io.seata.sqlparser.druid.mysql.MySQLOperateRecognizerHolder] 
2020-08-21 19:22:10.064 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load KeywordChecker[mysql] extension by class[io.seata.rm.datasource.undo.mysql.keyword.MySQLKeywordChecker] 
2020-08-21 19:22:10.065 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load TableMetaCache[mysql] extension by class[io.seata.rm.datasource.sql.struct.cache.MysqlTableMetaCache] 
2020-08-21 19:22:10.195 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load UndoLogManager[mysql] extension by class[io.seata.rm.datasource.undo.mysql.MySQLUndoLogManager] 
2020-08-21 19:22:10.202 [http-nio-8080-exec-1] [:] WARN  io.seata.common.loader.EnhancedServiceLoader - load [io.seata.rm.datasource.undo.parser.ProtostuffUndoLogParser] class fail. io/protostuff/runtime/RuntimeEnv 
2020-08-21 19:22:10.203 [http-nio-8080-exec-1] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load UndoLogParser[jackson] extension by class[io.seata.rm.datasource.undo.parser.JacksonUndoLogParser] 
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25d9820f]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a29a25f] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@36a96f9c] will not be managed by Spring
==>  Preparing: UPDATE goods SET stock = stock+? WHERE goodsId = ?
==> Parameters: -1(Integer), 3(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a29a25f]
res:1
2020-08-21 19:22:11.025 [http-nio-8080-exec-1] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - [192.168.3.173:8091:40155132419117056] commit status: Committed 
2020-08-21 19:22:11.041 [http-nio-8080-exec-1] [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-21 19:22:11.053 [http-nio-8080-exec-1] [FrameworkServlet.java:1131] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 200 OK, headers={masked} 
2020-08-21 19:22:11.064 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.core.rpc.netty.RmMessageListener - onMessage:xid=192.168.3.173:8091:40155132419117056,branchId=40155134826647552,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/orderdb,applicationData=null 
2020-08-21 19:22:11.065 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch committing: 192.168.3.173:8091:40155132419117056 40155134826647552 jdbc:mysql://127.0.0.1:3306/orderdb null 
2020-08-21 19:22:11.066 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch commit result: PhaseTwo_Committed 
2020-08-21 19:22:11.088 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.core.rpc.netty.RmMessageListener - onMessage:xid=192.168.3.173:8091:40155132419117056,branchId=40155138186285056,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/store,applicationData=null 
2020-08-21 19:22:11.088 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch committing: 192.168.3.173:8091:40155132419117056 40155138186285056 jdbc:mysql://127.0.0.1:3306/store null 
2020-08-21 19:22:11.088 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch commit result: PhaseTwo_Committed 

测试发生异常时事务的效果:

先查看undo_log表的自增值:

orderdb库中undo_log表:

下一个自增值    77

goodsdb库中undo_log表:

下一个自增值    48

访问:

http://127.0.0.1:8080/home/addorderseata?isfail=1

查看数据表,没有新增订单,商品也没有减库存,

注意查看两个库中undo_log表的自增值:

orderdb库中undo_log表:

下一个自增值    81

goodsdb库中undo_log表:

下一个自增值    50

发生了变化,说明曾经有undo_log写入

查看控制台的输出:

2020-08-21 19:30:31.695 [http-nio-8080-exec-1] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - Begin new global transaction [192.168.3.173:8091:40157238274297856] 
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@4d10bcbf] will not be managed by Spring
==>  Preparing: insert into orderinfo(orderSn,orderTime,orderStatus,userId,price) values( ?,now(),?,?,? )
==> Parameters: 20200821193031695(String), 0(Integer), 8(Integer), 100(BigDecimal)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@42fca4a3]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@17ef4bcf] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@5f4fe7f4] will not be managed by Spring
==>  Preparing: UPDATE goods SET stock = stock+? WHERE goodsId = ?
==> Parameters: -1(Integer), 3(Long)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@17ef4bcf]
res:1
2020-08-21 19:30:31.883 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.core.rpc.netty.RmMessageListener - onMessage:xid=192.168.3.173:8091:40157238274297856,branchId=40157238832140288,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/store,applicationData=null 
2020-08-21 19:30:31.883 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacking: 192.168.3.173:8091:40157238274297856 40157238832140288 jdbc:mysql://127.0.0.1:3306/store 
2020-08-21 19:30:32.038 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.common.loader.EnhancedServiceLoader - load UndoExecutorHolder[mysql] extension by class[io.seata.rm.datasource.undo.mysql.MySQLUndoExecutorHolder] 
2020-08-21 19:30:32.075 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.datasource.undo.AbstractUndoLogManager - xid 192.168.3.173:8091:40157238274297856 branch 40157238832140288, undo_log deleted with GlobalFinished 
2020-08-21 19:30:32.076 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacked result: PhaseTwo_Rollbacked 
2020-08-21 19:30:32.081 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.core.rpc.netty.RmMessageListener - onMessage:xid=192.168.3.173:8091:40157238274297856,branchId=40157238739865600,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/orderdb,applicationData=null 
2020-08-21 19:30:32.081 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacking: 192.168.3.173:8091:40157238274297856 40157238739865600 jdbc:mysql://127.0.0.1:3306/orderdb 
2020-08-21 19:30:32.091 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.datasource.undo.AbstractUndoLogManager - xid 192.168.3.173:8091:40157238274297856 branch 40157238739865600, undo_log added with GlobalFinished 
2020-08-21 19:30:32.092 [rpcDispatch_RMROLE_1_4] [:] INFO  io.seata.rm.AbstractRMHandler - Branch Rollbacked result: PhaseTwo_Rollbacked 2020-08-21 19:30:32.147 [http-nio-8080-exec-1] [:] INFO  io.seata.tm.api.DefaultGlobalTransaction - [192.168.3.173:8091:40157238274297856] rollback status: Rollbacked 

2,测试seata用过rest方式访问两个库的事务效果

分别访问:成功效果:

http://127.0.0.1:8080/home/addorderseatarest

和 发生异常失败效果:

http://127.0.0.1:8080/home/addorderseatarest?isfail=1

效果和直接访问数据库方式一致,大家自己查看即可

3,查看druid中建立的连接:访问:

http://127.0.0.1:8080/druid

可以看到dynamic-datasource所创建的两个到数据库的连接

spring boot:用dynamic-datasource-spring-boot-starter配置多数据源访问seata(seata 1.3.0 / spring boot 2.3.3)

七,查看spring boot的版本:

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