更新时间:2020/10/10 02:41,更新到了TC、TM、RM
本文主要对Spring Cloud Alibaba中的Seata进行学习与记录,偏向于实战,简单讲一下下面用到技术及版本,docker部署Seata1.3.0,Mysql采用8.0及以上,nacos采用1.3.1,重点讲一下Seata部署的方式,因为1.30版本跟0.9版本差距很大,代码不会过于详细,本文会持续更新,不断地扩充
注意:本文仅为记录学习轨迹,如有侵权,联系删除
这里给出官网地址:http://seata.io/zh-cn/docs/overview/what-is-seata.html Seata是什么 引用官网的话 ,Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
解决的问题 我们知道在普通的单体应用中,服务层里面的事务控制可以用@Transactional注解来实现事务的控制,但是如果是分布式的情况下,这个注解就不管用了,因为它们根本不在同一个服务中,这个时候就可以用Seata这个框架,就像官网所说的,致力于提供高性能和简单易用的分布式事务服务。
术语表 核心开发 关于其他的详细信息可以去官网直接看,这里重点讲一下开发,Seata的开发主要包括服务端和客户端的开发。下面直接进入实战,简单说一下技术选择和版本号
技术点版本docker/Seata1.3.0Mysql8.0及以上nacos1.3.2先去官网下载Seata1.3.0版,在进行docker部署之前,先了解一下Seata1.3.0版本的相关知识。
下载地址:http://seata.io/zh-cn/blog/download.html 如果下不了可以评论区留言,下载压缩包后解压即可,在conf里面有两个十分重要的文件,file.cof和registry.cof,怎么用后面会细讲
先是数据库的准备,这里用的mysql,,点开上图的README-zh.md,里面有链接,可以下载SQL 浏览器进入链接后,进入db,复制里面mysql的语句
里面的sql语句这里给大家复制出来了
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(96), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;创建名为seata的数据库,再执行上面的sql语句 数据库创建成功
这里说一下,这是一个不可少的步骤,需要将Seata的相关配置注册到Nacos服务中心,Nacos的创建和部署这里就不再重复,感兴趣的可以看一下本人之前的博客,地址:菜鸟的Spring Cloud Alibaba学习总结(一):Nacos
首先需要有配置文件,这样才能注册到Nacos中,还是上面的README-zh.md文件,现在就知道了为什么要先下载Seata的目的了吧,很多的配置文件都可以从里面的README.md获取到。 将里面的config.txt文件拷贝下来,放在seata安装目录的同级目录下 修改config.txt里面的内容,根据需要进行修改,这里给出我的配置
service.vgroupMapping.order_tx_group=default service.default.grouplist=39.96.22.34:8091 service.enableDegrade=false service.disableGlobalTransaction=false store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.cj.jdbc.Driver store.db.url=jdbc:mysql://39.96.22.34:3306/seata?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8 store.db.user=root store.db.password=123456 store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000下面对这些参数进行简单的讲解,具体可以看官网的配置说明:http://seata.io/zh-cn/docs/user/configurations.html 配置完之后需要从上面拷贝一个脚本下来,用该脚本将配置注册到nacos中 将nacos-config.sh放到seata安装目录,bin同级的目录下 先启动nacos之后,在该目录下右键,点击Git Bush Here,这是git工具的功能,通过git进行配置的注册 之后执行命令:sh nacos-config.sh -h nacos部署的ip,默认采用nacos端口号为8848
因为我部署的nacos端口号是8080的一个集群,通过8080端口号进行转发,所以修改了脚本的端口号8080,如果需要修改端口号可以进该脚本nacos-config.sh,修改里面的端口号为自己对应端口号即可 注册成功后,可以登录nacos查看配置文件,会生成配置文件conf.txt对应的配置 ok,目前已经成功了一半,下面开始用docker部署Seata
镜像的拉取:docker pull seataio/seata-server:1.3.0
挂载文件的创建,位置可以自己定义,这里给出我的文件创建,创建seata文件夹,里面创建seata-config文件夹,里面创建file.conf和registry.conf两个文件 编辑file.conf
## transaction log store, only used in seata-server service { #transaction service group mapping #指定测试的事务组名称 vgroup_mapping.order_tx_group = "default" #only support when registry.type=file, please don't set multiple addresses #指定默认组 地址和端口,可以设置多个地址 default.grouplist = "39.96.22.34:8091" #disable seata #禁用全局事务=false 即开启服务 disableGlobalTransaction = false } store { ## store mode: file、db、redis mode = "db" ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://39.96.22.34:3306/seata?userSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8" user = "root" password = "123456" minConn = 5 maxConn = 30 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } }注意,里面的配置可以参考我们下载的seata里面的conf目录里面的file.conf,里面还有file,redis模式的配置,由于这里mode=db,选择了db数据库模式,所以file模式和redis模式就不会生效了,所以这里就删掉了,需要的时候可以再配,里面的所有配置都跟前面的conf.txt文件里面的配置一致。 编辑registry.conf
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { # 服务名,自定义,建议默认seata-server application = "seata-server" # nacos的部署地址 serverAddr = "39.96.22.34:8080" # nacos的部署地址的分组 group = "SEATA_GROUP" # nacos的部署地址的命名空间 namespace = "" # nacos的部署地址节点默认 cluster = "default" # nacos的用户密码 username = "nacos" password = "nacos" } file { name = "/root/seata-config/file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "nacos" nacos { # nacos的部署地址 serverAddr = "39.96.22.34:8080" # nacos的部署地址的命名空间 namespace = "" # nacos的部署地址的分组 group = "SEATA_GROUP" # nacos的用户和密码 username = "nacos" password = "nacos" } file { name = "/root/seata-config/file.conf" } }配置可以参考我们下载的seata里面的conf目录里面的registry.conf,里面还有zk,consul等模式的配置,由于这里registry的type=nacos,选择了nacos的配置,config的type=nacos,选择了nacos的配置,所以其他的consul等配置就不会生效了,所以这里就删掉了,需要的时候可以再配。
启动容器
docker run --name seata01 -d -p 8091:8091 -e SEATA_CONFIG_NAME=file:/root/seata/config/registry -e SEATA_IP=39.96.22.34 -v /home/cainiao/seata/seata-config/:/root/seata/config --net=bridge --restart=always docker.io/seataio/seata-server:1.3.0 注意:-v后面的挂载文件的路径要根据自己的挂载文件的路径来登录nacos,根据挂载的配置文件所配置的,生成了对应的服务 到此为止,Seata的服务端部署结束
这里简单说一下下面要做的事情,开发3个微服务,订单服务、库存服务和账户服务,调用的逻辑为下单后,库存减少、账户扣款、下单结束,这个过程需要调用这3个服务,并且需要事务的控制,下面的客户端开发只讲一些重点的配置
每一个服务都需要一个独立的数据库,分别为seata_account、seata_order和seata_storage 之后在对应数据库表创建对应表,完整的sql如下
-- 创建3个数据库 create database seata_order; create database seata_storage; create database seata_account; -- 订单数据库表创建 use seata_order; create table t_order ( `id` bigint(20) not null auto_increment primary key, `user_id` bigint(20) default null comment '用户ID', `product_id` bigint(20) default null comment '产品ID', `count` int(11) default null comment '数量', `money` decimal(18,2) default null comment '金额', `status` int(1) default null comment '订单状态:0-创建中,1-已完结' ) engine=INNODB auto_increment = 1 default charset = 'utf8'; select * from t_order; -- 库存数据库表创建 use seata_storage; create table t_storage ( `id` bigint(20) not null auto_increment primary key, `product_id` bigint(20) default null comment '产品ID', `total` int(11) default null comment '总库存', `used` int(11) default null comment '使用库存', `residue` int(11) default null comment '剩余库存' ) engine=INNODB auto_increment = 1 default charset = 'utf8'; insert into t_storage(`id`, `product_id`, `total`, `used`, `residue`) values('1', '1', '100' , '0', '100'); select * from t_storage; -- 账户数据库表创建 use seata_account; create table t_account ( `id` bigint(20) not null auto_increment primary key, `user_id` bigint(20) default null comment '用户ID', `total` decimal(18,2) default null comment '总额度', `used` decimal(18,2) default null comment '使用额度', `residue` decimal(18,2) default '0' null comment '剩余额度' ) engine=INNODB auto_increment = 1 default charset = 'utf8'; insert into t_account(`id`, `user_id`, `total`, `used`, `residue`) values('1', '1', '10000' , '0', '10000'); select * from t_account;事务回滚表(暂且这样叫吧)undo_log的添加,上面的3个数据库表都要创建这样的一张表来支持seata的事务
-- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `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(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';这张表也可以在README-zh.md里面的链接找到 创建完的数据库
这里一共要创建3个服务,订单、库存和账户,这里以订单模块seata-order-service2001为例,接着之前的==菜鸟的Spring Cloud Alibaba学习总结(二)==的项目继续写
pom 引入坐标依赖pom
<dependencies> <!-- nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- seata--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <!-- 因为兼容版本问题,所以需要剔除它自己的seata的包 --> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <!--引入我们使用的自己的seata对应的版本的依赖,而不是使用starter默认的版本--> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.3.0</version> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--web启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!--alibaba druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!--监控--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--单元测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--hutool 测试雪花算法--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-captcha</artifactId> <version>5.2.0</version> </dependency> </dependencies>配置文件 创建配置文件,这里要创建file.conf、registry.conf和application.yml3个配置文件,首先是file.conf
transport { # tcp, unix-domain-socket type = "TCP" #NIO, NATIVE server = "NIO" #enable heartbeat heartbeat = true # the client batch send request enable enableClientBatchSendRequest = true #thread factory for netty threadFactory { bossThreadPrefix = "NettyBoss" workerThreadPrefix = "NettyServerNIOWorker" serverExecutorThread-prefix = "NettyServerBizHandler" shareBossWorker = false clientSelectorThreadPrefix = "NettyClientSelector" clientSelectorThreadSize = 1 clientWorkerThreadPrefix = "NettyClientWorkerThread" # netty boss thread size bossThreadSize = 1 #auto default pin or 8 workerThreadSize = "default" } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #transaction service group mapping vgroupMapping.order_tx_group = "default" #only support when registry.type=file, please don't set multiple addresses default.grouplist = "39.96.22.34:8091" #degrade, current not support enableDegrade = false #disable seata disableGlobalTransaction = false } client { rm { asyncCommitBufferLimit = 10000 lock { retryInterval = 10 retryTimes = 30 retryPolicyBranchRollbackOnConflict = true } reportRetryCount = 5 tableMetaCheckEnable = false reportSuccessEnable = false sagaBranchRegisterEnable = false } tm { commitRetryCount = 5 rollbackRetryCount = 5 defaultGlobalTransactionTimeout = 60000 degradeCheck = false degradeCheckPeriod = 2000 degradeCheckAllowTimes = 10 } undo { dataValidation = true onlyCareUpdateColumns = true logSerialization = "jackson" logTable = "undo_log" } log { exceptionRate = 100 } }配置跟前面服务端讲的file.conf要保持一致,像vgroupMapping.order_tx_group、nacos部署的地址,分组等信息一定要一致,然后是registry.conf
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" loadBalance = "RandomLoadBalance" loadBalanceVirtualNodes = 10 nacos { application = "seata-server" serverAddr = "39.96.22.34:8080" group = "SEATA_GROUP" namespace = "" username = "nacos" password = "nacos" } eureka { serviceUrl = "http://localhost:8761/eureka" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" password = "" timeout = "0" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } etcd3 { serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig type = "nacos" nacos { serverAddr = "39.96.22.34:8080" namespace = "" group = "SEATA_GROUP" username = "nacos" password = "nacos" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" apolloAccesskeySecret = "" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }这里的配置也是跟前面的registry.conf保持一致,只是这里一些没用的模式,像zk、consul模式等这里没有删掉,前面的删掉了,这里删不删都可以,最后是application.yml
server: port: 2001 spring: application: name: seata-order-service cloud: alibaba: seata: # 自定义事务组名称需要与seata-server中的对应,我们之前在seata的配置文件中配置的名字 tx-service-group: order_tx_group nacos: discovery: server-addr: 39.96.22.34:8080 # namespace: ffee2bbe-5ba5-4e6e-a822-6a00abe11769 datasource: # 当前数据源操作类型 type: com.alibaba.druid.pool.DruidDataSource # mysql驱动类 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://39.96.22.34:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: 123456 logging: level: io: seata: info mybatis: mapperLocations: classpath*:mapper/*.xml主启动类
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //取消数据源的自动创建 @EnableDiscoveryClient @EnableFeignClients public class SeataAccountMain2003 { public static void main(String[] args) { SpringApplication.run(SeataAccountMain2003.class,args); } }config包 数据源配置类DataSourceProxyConfig
@Configuration public class DataSourceProxyConfig { @Value("${mybatis.mapperLocations}") private String mapperLocations; @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } @Bean public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSourceProxy); ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); bean.setMapperLocations(resolver.getResources(mapperLocations)); return bean.getObject(); } }mybatis配置类MyBatisConfig
@Configuration @MapperScan({"com.zsc.mapper"}) public class MyBatisConfig { }entity包
@Data @AllArgsConstructor @NoArgsConstructor public class Order { /** * 主键 */ private Long id; /** * 用户id */ private Long userId; /** * 产品id */ private Long productId; /** * 数量 */ private Integer count; /** * 金额 */ private BigDecimal money; /** * 订单状态:0:创建中;1:已创建 */ private Integer status; }mapper包
@Mapper @Repository public interface OrderMapper { /** * 1 新建订单 * @param order * @return */ int create(Order order); /** * 2 修改订单状态,从0改为1 * @param userId * @param status * @return */ int update(@Param("userId") Long userId, @Param("status") Integer status); }对应的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.zsc.mapper.OrderMapper"> <resultMap id="BaseResultMap" type="com.zsc.entity.Order"> <id column="id" property="id" jdbcType="BIGINT"></id> <result column="user_id" property="userId" jdbcType="BIGINT"></result> <result column="product_id" property="productId" jdbcType="BIGINT"></result> <result column="count" property="count" jdbcType="INTEGER"></result> <result column="money" property="money" jdbcType="DECIMAL"></result> <result column="status" property="status" jdbcType="INTEGER"></result> </resultMap> <insert id="create" parameterType="com.zsc.entity.Order" useGeneratedKeys="true" keyProperty="id"> insert into t_order(user_id,product_id,count,money,status) values (#{userId},#{productId},#{count},#{money},0); </insert> <update id="update"> update t_order set status =1 where user_id =#{userId} and status=#{status}; </update> </mapper>server包 接口
public interface OrderServer { /** * 创建订单 * @param order */ String create(Order order); }利用openfeign调用其他服务的接口
@FeignClient(value = "seata-account-service") public interface AccountServer { @RequestMapping("/Account/{id}") public String update(@PathVariable("id") Long userId, @RequestParam("used") BigDecimal used); } @FeignClient(value = "seata-storage-service") public interface StorageServer { @RequestMapping("/Storage/{id}") public String update(@PathVariable("id") Long productId, @RequestParam("count") Integer count); }OrderServer的实现类
@Service @Slf4j public class OrderServerImpl implements OrderServer { @Autowired private OrderMapper orderMapper; @Autowired private AccountServer accountServer; @Autowired private StorageServer storageServer; @Override @GlobalTransactional(name = "order_tx_group",rollbackFor = Exception.class) public String create(Order order) { log.info("用户开始下订单......"); orderMapper.create(order); log.info("开始更新库存......"); String result01 = storageServer.update(order.getProductId(), order.getCount()); log.info("库存更新成功......"); int a = 10/0; log.info("开始更新用户账户......"); String result02 = accountServer.update(order.getUserId(), order.getMoney()); log.info("账户更新结束......"); int result03 = orderMapper.update(order.getUserId(), 0); log.info("订单操作结束......"); return "用户下单成功"; } }注意:上面加了@GlobalTransactional注解,order_tx_group自定义全局事务组,表示里面的所有数据库操作要么都成功,要么都失败,而且里面可以捕捉Exception,发生异常后事务回滚
controller包
@RestController public class OrderController { @Autowired private OrderServer orderServer; @GetMapping("/order") public String create(Order order){ String result = orderServer.create(order); return result; } }还有其他两个模块这里就不细说了,配置文件file.confregistry.conf一样的,application.yml基本一致
测试,加了@GlobalTransactional,并且在里面故意加了10/0的异常,发现事务回滚了,没加的事务没有回滚
用上面的3个微服务来讲就是,RM表示里面涉及到的一个个数据库操作,TM表示@GlobalTransactional所加的那个方法,该方法里面就有一个个数据库操作,TC表示协调这些包括回滚事务的协调者
