携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
前言
本文给大家带来的是Seata最新版的尝鲜体验,会详细描述我遇到的坑、详配置过程、使用方法等等。也会聊聊我对分布式事务的理解,顺道给大家推荐一些我看过的比较好的文章。
分布式事务大家应该都不陌生,应该也用过不少好用的方法来实现,比如Seata、手写事务补偿性代码、本地消息表。我一开始做技术选型的时候,选的是Seata,当时的版本是1.4.X,主要是看重AT模式,使用够简单,侵入性不低但是能够接受。在做部署和本地化的时候感觉挺顺畅,就是官方文档当时写的有点模糊,很多关键的点都没有明确给出解决方案,比如详细配置、XID传递。
上周分布式事务同事使用的时候突然又出现问题了,我就想借着这个口子把Seata升级到1.5.2,十几天前刚刚更新的,正好尝尝鲜。新特性的话,没有特别关注细节,看到有个控制台,但是本质上还是Seata服务端的单表查询,感觉意义不大,期待官方能有更多新功能。
环境需求及部署情况
- seata1.5.2--集成方式spring-boot-starter
- nacos2.0以上--注册及配置中心
- 扩展OpenFeign植入XID,请求链路绑定XID
部署的方式很淳朴,单节点部署在服务器上,分了正式和测试,分别对接公司的正式和测试的nacos三节点集群,高可用肯定是没有了,不过一年过去没出啥问题倒是真的幸运。Seata服务端配置中心和注册中心都是用的nacos,项目集成用的spring-boot-starter,因为没用Alibaba-Cloud,所以自己得做XID的绑定和传递。使用的还是AT模式,性能比较好,而且集成简单。
Server端配置
文件解压
下载1.5.2安装包,上传至服务器解压
tar -xvf /wa/seata-server-1.5.2.tar.gz
修改配置文件
修改/conf/application.yml
cd /wa/seata/conf
修改/conf/application.yml文件,修改nacos相关配置
server:
port: 7091
spring:
application:
name: seata-new-uat
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
# 没有删掉下面扩展
# extend:
# logstash-appender:
# destination: 127.0.0.1:4560
# kafka-appender:
# bootstrap-servers: 127.0.0.1:9092
# topic: logback_to_logstash
# 控制台用户名密码
console:
user:
username: seata
password: wzy
seata:
config:
type: nacos
nacos:
server-addr: xxx
namespace: xxx
group: SEATA_GROUP
username: xxx
password: xxx
data-id: seataNew
registry:
type: nacos
nacos:
server-addr: xxx
namespace: xxx
group: SEATA_GROUP
username: xxx
password: xxx
application: seata-new-uat
cluster: default
# 后面server或者client配置都不用配,因为配置了nacos作为配置中心。security得配,不配报错
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
复制代码
关联Nacos配置中心
进入nacos,创建命名空间
进入配置列表,进入seata命名空间,新建配置
dataId为seataNew,group为SEATA_GROUP,类型为properties
配置详情参考seata.io/zh-cn/docs/…
源码里的全部配置,会比官网上的更多。链接github.com/seata/seata…
实际使用,修改如下,完成后发布(参数没有详细说明的都是默认配置)
#公共部分
transport.serialization=seata
transport.compressor=none
transport.heartbeat=true
registry.type=nacos
config.type=nacos
#server端
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
#存储模式我这里选用的db,不用file和redis,这里很多配置都没有默认值但是必须指定,按情况配置即可
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
#这里url跟上rewriteBatchedStatements=true,原因看官网-参数配置-附录7,简单来说就是增加批量插入效率
store.db.url=jdbc:mysql://xxx:3306/seata_new?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&rewriteBatchedStatements=true
store.db.user=xxx
store.db.password=xxx
#默认1和20,稍微调大点
store.db.minConn=5
store.db.maxConn=30
store.db.maxWait=5000
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.lockTable=lock_table
store.db.queryLimit=100
#监控,只支持prometheus,我这里没有现成的就没用
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
#client端
seata.enabled=true
seata.enableAutoDataSourceProxy=true
seata.useJdkProxy=false
transport.enableClientBatchSendRequest=true
client.log.exceptionRate=100
service.disableGlobalTransaction=false
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.rm.reportSuccessEnable=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
#一阶段全局提交和回滚结果上报TC重试次数,默认1,这里改成3
client.tm.commitRetryCount=3
client.tm.rollbackRetryCount=3
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.logTable=undo_log
client.undo.onlyCareUpdateColumns=true
client.rm.sqlParserType=druid
#自定义事务组scm_tx_group和my_test_tx_group
service.vgroupMapping.scm_tx_group=default
复制代码
Seata资源准备
在配置中心里指定的地方,新建数据库seata_new,utf8mb4,运行下面sql。(这个是给seata服务端用来做全局事务管理的)
-- -------------------------------- 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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- 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 = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
复制代码
所有使用seata服务的项目数据库,都运行下面sql,增加一张表。(这个是在每一个事务参与者和事务发起者的数据库里增加一张表,用于本地事务回滚)
-- 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 NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) 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 = utf8mb4 COMMENT ='AT transaction mode undo table';
复制代码
运行Seata服务端
返回服务器,运行shell文件,指定注册IP为服务器IP,端口号默认8091
错误操作
sh /wa/seata/seata-1.5.2/bin/seata-server.sh -h 172.16.3.248 -p 8091
复制代码
启动异常
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
已经有人提过了,脚本问题
正确操作--到脚本所在位置执行脚本
cd /wa/seata/seata-1.5.2/bin
sh seata-server.sh -h 172.16.3.248 -p 8091
复制代码
如下图则成功,不成功检查报错,一般是nacos未配置
成功启动后注册至nacos
Client端配置
环境准备
官方Tips:请确保client与server的注册处于同一个namespace和group,不然会找不到服务---不一定,我使用的时候就不在一起
引入依赖,引入seata和nacos,nacos这里随意
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>${nacos-client.verison}</version>
</dependency>
复制代码
请求植入XID
spring-cloud-starter-alibaba-seata默认会传递XID,而seata-spring-boot-starter和seata-all不会,需要手动实现,官方文档这里有写
改造feign的请求拦截器,用于植入Seata用xid。同理其他的调用方式改造对应的拦截器即可
import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.seata.core.context.RootContext;
import org.springframework.context.annotation.Configuration;
/**
* @Classname FeignInterceptor
* @Date 2021/9/26 22:18
* @Author WangZY
* @Description openfeign植入seata用xid
*/
@Configuration
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String xid = RootContext.getXID();
requestTemplate.header(RootContext.KEY_XID, xid);
}
}
复制代码
绑定XID
XID的传递,这里有两种方式都可以实现,一种是基于过滤器,一种是基于拦截器。思路就是请求过来的时候绑定XID,请求结束后解绑XID。Filter没有案例,按照我这个写就行。
过滤器
import io.seata.core.context.RootContext;
import org.springframework.util.StringUtils;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @Classname SeataFilter
* @Date 2021/9/27 11:38
* @Author WangZY
* @Description Seata过滤器
*/
@WebFilter
public class SeataFilter implements Filter {
@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());
boolean isBind = false;
if (!StringUtils.isEmpty(xid)) {
RootContext.bind(xid);
isBind = true;
}
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
if (isBind) {
RootContext.unbind();
}
}
}
}
复制代码
拦截器
拦截器的案例,我们可以在alibaba-cloud包里直接拿过来用,引入这个依赖源码拉下来直接粘。
dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2021.1</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
复制代码
封装组件
这里我们可以顺道做一个组件,方便集成,例如我这里做了一个feign-seata的组件。结构很简单,我这里为了测试,过滤器和拦截器都放里面了,实际放一个即可,feign拦截器就是上面的代码,SeataAutoConfiguration.class就是一个扫描包的类,我之前的教你如何开发一个spring-boot-starter里面有提到这个作用。
项目配置
application.properties配置文件引入
替换对应的事务组
seata.tx-service-group=XXX
seata.service.vgroup-mapping.XXX=default
seata.enabled=true
seata.enable-auto-data-source-proxy=true
seata.tx-service-group=替换事务组
seata.registry.type=nacos
# 和nacos上面的服务名一致
seata.registry.nacos.application=seata-server
seata.registry.nacos.server-addr=172.16.3.85:8847,172.16.3.86:8847,172.16.3.87:8847
seata.registry.nacos.group=SEATA_GROUP
seata.registry.nacos.namespace=32d19a70-085d-430d-b2b3-7e8d06b37308
seata.registry.nacos.cluster=default
seata.registry.nacos.username=nacos
seata.registry.nacos.password=RJ0p-0p-0p-
seata.service.vgroup-mapping.替换事务组=default
复制代码
使用方法
@GlobalTransactional(rollbackFor = Exception.class,timeoutMills=60000)
全局事务默认只有60秒,长事务必须拆分,或者抽出无关逻辑,如果实在无法拆分,建议timeout参数调大
Seata注意事项
1.无法找到集群"defalut"
常见于各种网络问题、加密密钥问题导致nacos没有正常连接。 以下仅作为我在查询各种网络连接问题时的断点位置
io.seata.discovery.registry.nacos.NacosRegistryServiceImpl#getNamingProperties
io.seata.discovery.registry.nacos.NacosRegistryServiceImpl#lookup
com.alibaba.nacos.client.naming.core.HostReactor#getServiceInfo
com.alibaba.nacos.client.naming.net.NamingProxy#queryList
com.alibaba.nacos.client.naming.net.NamingProxy#callServer(java.lang.String, java.util.Map, java.util.Map, java.lang.String, java.lang.String)
复制代码
2.全局事务失效
问题表现为服务A是事务发起方,服务B是事务参与者,然后发起全局事务后报错回滚,最后结果是服务A回滚,B没有回滚,Seata服务端日志显示回滚成功。
解决思路:
首先是查看Seata的日志,发现服务A和B都正常注册成功,并且流程中发现Seata服务端日志打印了回滚成功。
开始排查配置,最终发现是feign依赖没有采用我修改后的版本,核心原因是XID没有传递成功。
3.nginx会抹掉header中的下划线
因为seata中XID传递时使用TX_XID为key进行传递,因此事务参与者接收nginx转发处理后的请求会找不到key为TX_XID的header,导致出现和问题2一样的全局事务失效问题。解决方法是修改nginx配置。
在nginx里的nginx.conf配置文件中的http部分中添加如下配置(默认off)
underscores_in_headers on;
复制代码
聊聊分布式事务
Seata in AT mode
Seata 的 AT 模式建立在关系型数据库的本地事务特性的基础之上,通过数据源代理类拦截并解析数据库执行的 SQL,记录自定义的回滚日志,如需回滚,则重放这些自定义的回滚日志即可。AT 模式虽然是根据 XA 事务模型(2PC)演进而来的,但是 AT 打破了 XA 协议的阻塞性制约,在一致性和性能上取得了平衡。
AT 模式是基于 XA 事务模型演进而来的,它的整体机制也是一个改进版本的两阶段提交协议。AT 模式的两个基本阶段是:
- 第一阶段:首先获取本地锁,执行本地事务,业务数据操作和记录回滚日志在同一个本地事务中提交,最后释放本地锁;
- 第二阶段:如需全局提交,异步删除回滚日志即可,这个过程很快就能完成。如需要回滚,则通过第一阶段的回滚日志进行反向补偿。
各种事务模式的选择
我个人在选择的时候比较看好AT和TCC。先来说说AT,相对于别的事务模式优势很明显,非阻塞和简单易用。TCC的话更多的是可操作范围大,更自由。其他诸如本地消息表、消息队列、Saga或多或少相对麻烦或者有限制。这里推荐几篇好文,顺道说说我看完后的碎碎念。
掘金一千赞的文章,不仅仅是少见而且是真的牛逼。从基础到原理,辅以各种类型解决方案,如果你只想看理论,认准这篇就没错了。稍显不足的是,缺了点实践部分,用来做入门了解肯定是绰绰有余,但是真要上手进行实践,肯定是不行的。
看了几十篇,感觉大家理论这块确实整不出花活了,都是你抄抄我抄抄。这一篇算是比较全的,图也比较多,算是比较清晰的。
写在最后
网上看了好多,感觉分布式事务这块成熟的中间件好少,大多都是推荐用Seata的,别的就是自定义的居多。有RocketMq、TCC,总觉得这块不应该是这样,我个人觉得分布式事务在现在的开发环境中应该算是必备了吧,优先级应该比日志收集这种观测性的更高才对,竞品太少真的太意外了。
目前使用Seata应该算是轻度,没有接SkyWalking之类的监控,也没有做高可用,其实风险蛮高的。但是项目时间太紧,摆烂,不然这些应该去做的。分布式事务属于业务场景复杂的中间件了,通用的方案我现在一般就是用Seata的AT。除此之外对接外部系统的时候情况最麻烦,用过本地消息表的思想去处理,确保数据的最终一致性。
本文以实践为主,算是一篇使用说明书,本来想写成那种成知识体系的文章,但是鉴于我对分布式事务的了解尚浅,就不误人子弟了。