maven依赖配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>dubbo-service-tcc</artifactId> <groupId>org.idea</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>dubbo-service-pay</artifactId> <properties> <mysql.version>5.1.26</mysql.version> <druid.version>1.1.10</druid.version> <seata.version>1.1.0</seata.version> <nacos.client.version>1.1.3</nacos.client.version> <nacos.springboot.starter.version>0.2.1</nacos.springboot.starter.version> <dubbo.service.interface.version>1.0-SNAPSHOT</dubbo.service.interface.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>${seata.version}</version> </dependency> <dependency> <groupId>org.idea</groupId> <artifactId>dubbo-service-interface</artifactId> <version>${dubbo.service.interface.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <!-- Dubbo Registry Nacos --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-nacos</artifactId> <version>${dubbo.version}</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>${nacos.client.version}</version> </dependency> <!-- 1. nacos-如果希望注入一些功能则需要使用starter --> <dependency> <groupId>com.alibaba.boot</groupId> <artifactId>nacos-config-spring-boot-starter</artifactId> <version>${nacos.springboot.starter.version}</version> </dependency> <!-- 2. nacos-服务发现功能依赖 --> <dependency> <groupId>com.alibaba.boot</groupId> <artifactId>nacos-discovery-spring-boot-starter</artifactId> <version>${nacos.springboot.starter.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> </dependencies> </project> 复制代码
配置文件
配置数据源application.propertie文件
server.port=8081 spring.application.name=seata-demo # goods spring.datasource.goods.jdbc-url=jdbc:mysql://prod.min.mall.com:3306/mall-goods?useUnicode=true&characterEncoding=utf8 spring.datasource.goods.username=root spring.datasource.goods.password=root spring.datasource.goods.driver-class-name=com.mysql.jdbc.Driver # order spring.datasource.order.jdbc-url=jdbc:mysql://prod.min.mall.com:3306/mall-order?useUnicode=true&characterEncoding=utf8 spring.datasource.order.username=root spring.datasource.order.password=root spring.datasource.order.driver-class-name=com.mysql.jdbc.Driver 复制代码
关于seata的配置主要写在了application.yml中:
seata: enabled: true application-id: tcc-seata-service tx-service-group: SEATA_GROUP # 事务群组(可以每个应用独立取名,也可以使用相同的名字) client: rm-report-success-enable: true rm-table-meta-check-enable: false # 自动刷新缓存中的表结构(默认false) rm-report-retry-count: 5 # 一阶段结果上报TC重试次数(默认5) rm-async-commit-buffer-limit: 10000 # 异步提交缓存队列长度(默认10000) rm: lock: lock-retry-internal: 10 # 校验或占用全局锁重试间隔(默认10ms) lock-retry-times: 30 # 校验或占用全局锁重试次数(默认30) lock-retry-policy-branch-rollback-on-conflict: true # 分支事务与其它全局回滚事务冲突时锁策略(优先释放本地锁让回滚成功) tm-commit-retry-count: 3 # 一阶段全局提交结果上报TC重试次数(默认1次,建议大于1) tm-rollback-retry-count: 3 # 一阶段全局回滚结果上报TC重试次数(默认1次,建议大于1) undo: undo-data-validation: true # 二阶段回滚镜像校验(默认true开启) undo-log-serialization: jackson # undo序列化方式(默认jackson) undo-log-table: undo_log # 自定义undo表名(默认undo_log) log: exceptionRate: 100 # 日志异常输出概率(默认100) support: spring: datasource-autoproxy: true service: vgroup-mapping: my_tcc: default # TC 集群(必须与seata-server保持一致) enable-degrade: false # 降级开关 disable-global-transaction: false # 禁用全局事务(默认false) grouplist: default: 127.0.0.1:8091 transport: shutdown: wait: 3 thread-factory: boss-thread-prefix: NettyBoss worker-thread-prefix: NettyServerNIOWorker server-executor-thread-prefix: NettyServerBizHandler share-boss-worker: false client-selector-thread-prefix: NettyClientSelector client-selector-thread-size: 1 client-worker-thread-prefix: NettyClientWorkerThread type: TCP server: NIO heartbeat: true serialization: seata compressor: none enable-client-batch-send-request: true # 客户端事务消息请求是否批量合并发送(默认true) registry: file: name: file.conf type: nacos nacos: server-addr: localhost:8848 namespace: cluster: default config: file: name: file.conf type: nacos nacos: namespace: server-addr: localhost:8848 这里面有几个坑的地方,建议下边的配置模块要和seata的server端配置相同,否则会出现错误。 service: vgroup-mapping: my_tcc: default # TC 集群(必须与seata-server保持一致) grouplist: default: 127.0.0.1:8091 seata: tx-service-group: SEATA_GROUP #这个属性后边我会讲解 复制代码
dubbo.properties配置文件
dubbo.application.id=dubbo-service dubbo.application.name=dubbo-service dubbo.registry.address=nacos://localhost:8848 dubbo.provider.threads=10 dubbo.provider.threadpool=fixed dubbo.provider.loadbalance=roundrobin dubbo.server=true dubbo.protocol.name=dubbo dubbo.protocol.port=9091 dubbo.protocol.threadpool=fixed #dubbo.protocol.dispatcher=execution dubbo.protocol.threads=100 dubbo.protocol.accepts=100 dubbo.protocol.queues=100 复制代码
整体项目结构:
代码模块
相关的数据源配置:
package org.idea.service.pay.config; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; /** * @author idea * @date created in 6:12 下午 2020/11/14 */ @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.order") public DataSource orderDataSource(){ return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.goods") public DataSource goodsDataSource(){ return DataSourceBuilder.create().build(); } @Bean(name = "orderJdbcTemplate") public JdbcTemplate orderJdbcTemplate(@Qualifier("orderDataSource")DataSource orderDataSource){ return new JdbcTemplate(orderDataSource); } @Bean(name = "goodsJdbcTemplate") public JdbcTemplate goodsJdbcTemplate(@Qualifier("goodsDataSource")DataSource goodsDataSource){ return new JdbcTemplate(goodsDataSource); } } 复制代码
dao层:
package org.idea.service.pay.dao; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import javax.annotation.Resource; /** * @author idea * @date created in 6:17 下午 2020/11/14 */ @Repository public class GoodsDao { @Resource private JdbcTemplate goodsJdbcTemplate; public boolean updateStock(int id,int stock){ String sql = "update t_goods set stock=stock-? where id=?"; int result = goodsJdbcTemplate.update(sql,new Object[]{stock,id}); return result>0; } } 复制代码
package org.idea.service.pay.dao; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import javax.annotation.Resource; import java.util.UUID; /** * @author idea * @date created in 6:17 下午 2020/11/14 */ @Repository public class OrderDao { @Resource private JdbcTemplate orderJdbcTemplate; public boolean insertOrder(){ int i=0/0; String orderNo = UUID.randomUUID().toString(); String sql = "INSERT INTO `t_order`( `order_no`, `user_id`, `goods_id`, `stock`, `unit`, `status`, `valid_status`, `create_time`, `update_time`) VALUES ( '" + orderNo+"', 1, 1, 1, '件', 1, 1, NOW(), NOW())"; orderJdbcTemplate.execute(sql); return true; } } 复制代码
在insertOrder方法中我特意写了一段异常,为后续测试seata使用。
service层:
package org.idea.service.pay.service; import io.seata.spring.annotation.GlobalTransactional; import org.apache.dubbo.config.annotation.Service; import org.idea.interfaces.pay.ITccService; import org.idea.service.pay.dao.GoodsDao; import org.idea.service.pay.dao.OrderDao; import javax.annotation.Resource; /** * @author idea * @date created in 7:41 下午 2020/11/14 */ @Service(interfaceName = "iTccService") public class ITccServiceImpl implements ITccService { @Resource private GoodsDao goodsDao; @Resource private OrderDao orderDao; @GlobalTransactional(timeoutMills = 300000,name = "tcc-seata-service-group") @Override public void doTcc(){ System.out.println("====== 开始执行事务 ====== "); goodsDao.updateStock(1,1); orderDao.insertOrder(); System.out.println("====== 执行事务结束 ====== "); } } 复制代码
注意这里代码中写的 @GlobalTransactional注解是seata用于捕获分布式事务的关键点,
可以看到我这里写入的name是:tcc-seata-service-group,这块可以自定义不影响。
启动类
package org.idea.service.pay; import org.apache.dubbo.config.annotation.Reference; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.idea.interfaces.pay.ITccService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; /** * @author idea * @date created in 6:00 下午 2020/11/14 */ @SpringBootApplication @EnableDubbo @RestController public class Application { @Reference private ITccService iTccService; @GetMapping(value = "tcc") public String doTcc(){ iTccService.doTcc(); return "success"; } public static void main(String[] args) { SpringApplication.run(Application.class); System.out.println("Application tcc demo"); } } 复制代码
启动程序,我们可以看到日志中会有相关的seata信息日志打印:
seata的分布式事务验证
异常验证
请求接口http://localhost:8081/tcc
在创建订单的环节中,出现异常,seata进行了回滚处理,此时数据库库存保持和一开始一致。订单库也没有新增数据。
正常流程验证
将异常代码去除,重新请求:
此时日志打印正常,seata的环境基本搭建成功!
可能出现的异常
io.seata.common.exception.FrameworkException: No available service 复制代码
不知道各位读者在最后的接口验证环境是不是会和我一样遇到类似的异常,这段异常如下所示:
网上搜索了相关资料,比较少看到讲解这块的原因,我只能硬着头皮去源码debug分析。
结合自己以前对于nacos源码的理解经验,快速地定位到了问题点:
io.seata.config.nacos.NacosConfiguration#getConfig 复制代码
此处的查询dataId为service.vgroupMapping.SEATA_GROUP,group值为SEATA_GROUP,然后到nacos中一查询,发现此配置不存在,于是手动添加:
其实这里到dataId尾部的SEATA_GREOUP名称就是我们文章上边提及的application.yml中配置的一项参数。
知道对应的dataid,group,猜测相关的value应该要和yml一致,于是我便配置了default。
结果控制台立马发生响应变化:
此时再重试接口,就恢复正常了。
小结
之前在工作中用得比较多的还是借助消息队列来实现分布式环境下的事务最终一致性,对于seata这款技术框架的原理还有很多细节不是很熟悉。
整理梳理下来,感觉seata的入门要比nacos,dubbo那些中间件高一些,配置复杂,细节繁琐。后边看了官网的介绍,这里还只是其中的at模型,对于更加多更加复杂的其他事务模型还有待继续学习。