一,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,项目结构:如图:
4,用到的数据库:
四,配置文件说明
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表
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='订单表'
每个库中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的版本:
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.3.RELEASE)