spring boot:使用分布式事务seata(druid 1.1.23 / seata 1.3.0 / mybatis / spring boot 2.3.2)
一,什么是seata?
http://seata.io/zh-cn/
https://github.com/seata/seata
http://seata.io/zh-cn/docs/overview/what-is-seata.html
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上查看
六,测试效果
下一个自增值 70
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,没发生变化
下一个自增值 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
http://127.0.0.1:8080/home/addorderseatarest?isfail=1
可以观察到undo_log中自增值的变化和控制台的输出
与上一个例子基本一致,大家自己观察效果即可
七,查看spring boot 的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.2.RELEASE)