1.分布式事务的产生
事务是通过jdbc Connection对象进行开启,及提交/回滚,同jvm 使用@Transaction注解时,只要业务method没有结束,spring是不会退回connection到连接池的,但不同jvm是不是spring上下文来管理connection的(即不同本地事务)。微服务划分因尽量的高内聚低耦合,减少分布式事务,但是随着系统逐渐变得庞大,分布式事务也情况也会增多,需要一种简易分布式事务解决方案
2.常见分布式事务解决方案
- 本地事务:只是服务拆分,数据库未分库,系统较简单时且仅个别分布式事务场景,可以考虑牺牲系统耦合性,需要在不相关的领域模型写入其实没啥关系的sql语句,如为解决分布式事务可能有userMapper里可能有select user 和select order的语句。
- 基于MQ最终一致性方案:该方案性能较好,一般情况都是准实时一致,除非MQ消费不过来才会有一定延时,对程序员要求也较高。该方案比较时候电商购物等长事务场景(支付及支付成功消息在一个本地事务里),如kafka消费者端只要没有成功消费消息即消息处理报错了kafka会只一直消费该消息,直到该消息被正常处理并commit。可能出现消费端一致报错,这需要监控报警即使发现并解决,如果长时间不能解决,数据会较长时间不能达到一致性。
- TCC即Try、Confirm和Cancel,代码侵入太高,需要自己写补偿接口,如下单失败,还得在cancel代码片段写库存恢复接口,而且cancel里的数据库操作也可能会出错。
- XA事务:CP模型,强一致性,性能也较差,一把是有关全局事务管理器如后面的seata-server。低并发,要求强一致性,接受一定的性能损失,需要一种简单的方式解决分布式事务就应该选择XA事务。
3.分布式事务中间件:seata ,LCN,阿里GTS,sharding sphere的xa等
LCN前两年发展还不错,今天一看官网已打不开,由于开源资金,阿里的seata的顺势而来,github已宣布停滞维护,LCN的使用还是很方便的,一个全局事务注解即可完成(参考)。GTS阿里没有开源,就不用想了,但是开源了一个seata,目前还是比较活跃的。github地址,官方文档。seata 有个AT(自动事务)模式:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接
二阶段:commit:提交异步化,非常快速地完成 ; rollback:回滚通过一阶段的回滚日志进行反向补偿。
4.springcloud 2.2.1集成seata 1.4 官方文档
4.1 seata server端安装:下载https://github.com/seata/seata/releases,案例使用1.4.0版本测试,建议docker方式安装 。
注:202111测试docker安装的最新版本默认支持file模式的配置,注册中心不必使用registry.conf,file.conf
1.先默认启动
docker run --name seata-server \
-p 8091:8091 \
seataio/seata-server
2.拷贝容器内/seata-server下的整个resources拷到宿主机进行修改, docker cp seata-server:/seata-server/resources/ /etc/seata/resources
3.正式启动(进入docker exec -it seata-server /bin/sh)
docker run -d --name seata-server \
-p 8091:8091 \
-v /etc/seata/resources:/seata-server/resources \
seataio/seata-server
4.注意默认使用file模式,同#sh seata-server.sh -p 8091 -h 127.0.0.1 -m file,如果使用db储存事务消息可通过-e设置环境变量
或tar包解压安装,bin目录启动:./seata-server.sh 。
4.2 使用file方式配置seata-server
进入config目录修改配置: 首先是registry.conf ,注册中心与配置中心type 都是默认file,推荐使用nacos
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
type = "file"
file {
name = "file.conf"
}
}
如果前面registry.conf指定使用file,则在file.conf配置seata的事务日志存储相关参数,如果使用nacos则可以在nacos上进行修改。(registry.conf,file.conf都是seata server的配置文件)1.4版本file.conf配置文件里只显示给出了store相关配置项,可能官方推荐使用nacos进行配置。
这里还是先测试一波file方式配置,追加了除store之外的其它配置项。必须修改的是vgroupMapping.payment="default" ,vgroupMapping.study="default" ,这两个配置表示将payment与study两个服务映射到default“事务组。注意旧版本是vgroup_mapping ;如果使用store.mode=db则还需要配置DB连接参数并初始化化globleTable,branchTable,lockTable三张表(脚本位置https://github.com/seata/seata/tree/develop/script),用单独的库,非业务数据库。单机非高可用部署store.mode=file就狗了,如果要多节点高可用,必须使用db/redis,因为需要分布式锁支持
store {
## store mode: file、db、redis
mode = "file"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## 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.jdbc.Driver"
url = "jdbc:mysql://xxxxxx:3306/seata"
user = "root"
password = "xxxx"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
host = "127.0.0.1"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
transport {
# tcp udt 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,will not be used for UDT
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.payment="default"
vgroupMapping.study="default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1: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
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
4.2 springcloud alibaba 2.2.1微服务集成seata,注意该starter默认引入的seata包是1.1的,故这里单独引入1.4
业务库增加undo_log表,用于记录各个RM存储的branch_id(分支事务ID),xid(全局事务ID)及rollback_info(记录变更数据前后数据快照,是二阶段补偿的依据)
-- auto-generated definition
create table undo_log
(
id bigint auto_increment
primary key,
branch_id bigint not null,
xid varchar(100) not null,
context varchar(128) not null,
rollback_info longblob not null,
log_status int not null,
log_created datetime not null,
log_modified datetime not null,
constraint ux_undo_log
unique (xid, branch_id)
)
comment 'Seata分布式事务';
增加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
配置文件
spring:
application:
name: payment
profiles:
active: @package.environment@
cloud:
nacos:
config:
server-addr: @package.nacos.addr@ #注意不要http://,nacaos使用的是raft协议
file-extension: yml #后缀
namespace: @package.environment@ #配置命名空间,默认public
shared-configs:
- data-id: common.yml #配置所有工程共享的配置,注意里面的配置默认不能动态刷新,需要时可配置自动刷新
refresh: true
password: @package.nacos.password@ #nacos 1.3.X才支持
username: @package.nacos.username@
context-path: /nacos
discovery:
password: @package.nacos.password@
username: @package.nacos.username@
server-addr: @package.nacos.addr@ #注意不要http://,nacaos使用的是raft协议
namespace: @package.environment@ #服务命名空间,默认public
#分布式事务Seata配置
seata:
tx-service-group: ${spring.application.name}
enabled: true
enable-auto-data-source-proxy: true
service:
vgroup-mapping:
payment: default
grouplist:
default: 127.0.0.1:8091
#config:
# type: springCloudConfig
启动类配置 新版已支持自动代理,不需要自己手动配置datasoure代理了
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@MapperScan("com.XXX.user.mapper")
=============================================================================
//这里去除了dataSource自动配置,需要在自定义配置,参考如下
import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
@Configuration
@EnableConfigurationProperties({MybatisPlusProperties.class})
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy,
MybatisPlusProperties mybatisProperties) {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
Resource[] mapperLocaltions = resolver
.getResources(mybatisProperties.getMapperLocations()[0]);
bean.setMapperLocations(mapperLocaltions);
if (StringUtils.isNotBlank(mybatisProperties.getConfigLocation())) {
Resource[] resources = resolver.getResources(mybatisProperties.getConfigLocation());
bean.setConfigLocation(resources[0]);
}
return bean.getObject();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
4.3启动及测试
A服务
@GlobalTransactional
@Override
public ResultEntity test() {
ResultEntity result = restTemplate.getForObject("http://study/test/test2", ResultEntity.class);
int insert = orderMapper.insert(new ServiceOrder().setId("111").setOrderNo("aaaaa"));
int a=1/0;
return null;
}
B服务
@Override
@GlobalTransactional
public void test2() {
userMapper.updateRoleName();
}
启动A B服务
0.正常测试,A先调B服务,B服务的本地事务提交,然后A抛出异常,B服务能基于undo_log日志表进行回滚,B服务日志可看到rollbacked相关日志
!.测试异常场景seata server宕机:AB服务均立即报错can not connect to 192.168.203.132:8091 cause:can not register RM,err:can not connect to services-server,请求接口也会返回该错误,seata server恢复,AB服务立即停止异常信息输出。seata-server宕机,只影响@GlobalTransactional注解方法
@GlobalTransactional可能是基于aop实现的,不能连接server端即抛出异常
!!.测试异常场景B服务宕机:A服务抛出异常io.seata.common.exception.FrameworkException: No available service,A正常回滚,seata server 及A服务打印如rollbacked相关日志
!!!.断点打restTemplate调B服务处,检查该行修改已生效,此时再去手动修改A服务修改的那条记录,可发现是可以修改的没有锁定,断点打throw new CommonException("A服务异常")处,此时B修改已生效也可以修改,A抛出异常后服务方法走完,开始回滚事务,这里有个严重“BUG”,比如原始username =zs1 ,userMapper.updateUserName()修改为zs2,这时打个断点手动将zs2改为zs3,然后方法走完开始回滚发现不能正常回滚,死循环重试BUG????????这就是官方说的dirty data, 这时手动将zs3改回zs2,即可结束死循环并成功回滚zs1.为避免这种脏数据产生,应配合本地事务注解@Transaction ,全局事务开启,提交回滚可以监听的,默认实现是DefaultFailureHandlerImpl,需要自定义需要实现FailureHand接口,并自定义配置GlobalTransactionScanner 。 官方说明:脏数据需手动处理,根据日志提示修正数据或者将对应undo删除(可自定义实现FailureHandler做邮件通知或其他)
4.4 前面使用的是file,主流做法还是使用nacos做配置中心,注册中心
(注意:如果nacos开启了权限控制,要求nacos读取配置的seata-server,及微服务里nacos的java client都得配置username/password,否则报错403-unkown user)
修改seata server主配置registry.conf
#注意这里配置里application="seata-server",默认,这个与后面的大坑有关
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "xxxx:8091"
namespace = "public"
group = "SEATA_GROUP"
username = "xxxx"
password = "xxxx"
}
}
config {
type = "nacos"
nacos {
serverAddr = "xxxx:8091"
namespace = "public"
group = "SEATA_GROUP"
username = "xxxx"
password = "xxxx"
}
}
!!!新版1.4.2配置seata-server使用nacos作配置注册中心不必再使用registry.conf了,,直接docker映射出来的resources/application.yml配置nacos信息
配置好之后,启动seata,nacos
之前配置信息配置 是registry.file引入 file.conf里的配置,现改为nacos 故不再需要file.conf了,
使用nacos就是把原file.conf的配置全部移入nacos,但是手动移入麻烦,官方提供了一个较用以将配置写入nacos.
先在seata目录创建config.txt(里面就是存的原file.conf的配置项,只是要改为k-v形式)配置如下, 再将https://github.com/seata/seata/tree/develop/script/config-center/nacos 搞下来, 执行 sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -u nacos -w nacos -t test
(-t Tenant租户,就是namespace,其它参数说明详见nacos-config.sh),config.txt位置没放对,执行脚本会提示正确位。
将配置批量写进nacos(其实就是post请求),也可以手动去nacos里改。这里面有几个参数由中画线改为了驼峰如dbType,driverClassName,版本不同可能报错。(注意去掉配置中的注释)
service.vgroupMapping.payment=default
service.vgroupMapping.study=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
store.mode=file #store.mode=xx是切换开关 ,store.mode=file就不必配置db等其它了,如果要配置多节点高可用则需要db分布式锁支持
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=username
store.db.password=password
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
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson #这个在根据undolog回滚事务时Localedatetime反序列化出错可以考虑换这个
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
重启,然后访问nacos管理页面即可看到配置项,重启seata-server即可读取到配置信息。
Java client端不要作任何修改,测试回滚效果同file
4.5 其它
1.client端其它可选配置
#Seata配置,配置seata-server地址等
seata:
tx-service-group: ${spring.application.name}
enabled: true #是否开启seata分布式事务
enable-auto-data-source-proxy: true #是否开启数据源代理
service:
vgroup-mapping:
user: default #!!!!!!注意这里key不要写${spring.application.name}
grouplist:
default: 192.168.154.140:8091 #seata-server地址
enable-degrade: false # 降级开关
disable-global-transaction: false # 禁用全局事物(默认 false)
#seata client端配置,不配置用默认值也可以
client:
rm:
report-retry-count: 5 # 一阶段结果上报TC充实次数(默认5)
async-commit-buffer-limit: 10000 # 异步提交缓存队列长度(默认10000)
table-meta-check-enable: false # 自动刷新缓存中的表结构
report-success-enable: true
lock:
retry-interval: 10 # 校验或占用全局锁重试间隔(默认10ms)
retry-times: 30 # 校验或占用全局锁重试次数(默认30)
retry-policy-branch-rollback-on-conflict: true
tm:
commit-retry-count: 3 # 一阶段全局提交上报 TC 重试次数(默认 1 次, 建议大于 1)
rollback-retry-count: 3 # 一阶段全局回滚上报 TC 重试次数(默认 1 次, 建议大于 1)
undo:
data-validation: true # 二阶段回滚镜像校验(默认 true 开启)
log-serialization: jackson # undo 序列化方式(默认 jackson)
log-table: undo_log # 自定义undo表名(默认undo_log)
log:
exception-rate: 100 # 日志异常输出概率(默认 100)
2.优化配置nacos common.yml全局配置
#全局日志控制
logging.level:
root: info
com.alibaba.nacos.naming.beat.sender: info
com.alibaba.nacos.client.naming.updater: info
com.alibaba.nacos.client.config.impl.ClientWorker: error
#全局分布式事务开关
seata:
enabled: true
enable-auto-data-source-proxy: true
3.低版本的springcloud可以引入较低版本的seata,建议父工程锁定seata版本,或使用较高版本springcloud,避免踩到一些版本坑
<!--父工程锁定seata版本1.4.0-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seat.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!--子工程直接引入,也不必exclusive咯-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
4.5 总结
TC (Transaction Coordinator) - 事务协调者(seata-server服务端)
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器(第一个@GlobleTransation方法)
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器(每个@GlobleTransation方法)
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
- TM事务管理者开启全局事务
- RM处理各自分支事务(异步的)
- 分支事务完成后TC协调者汇总进行全局事务裁决(基于undo日志补偿)