seata client 與 seata 是怎么通訊的
下面這張圖弯予,就是一切的基礎(chǔ)喳张。
- seata server 向注冊(cè)中心注冊(cè)
- seata client 想注冊(cè)中心注冊(cè)
- seata client 通過(guò)注冊(cè)中心返回的seata server的地址與端口说庭,找到seata server
由此可見(jiàn)裳涛,注冊(cè)中行在這一場(chǎng)景下的作用就是讓client 找到server
下面我們來(lái)看注冊(cè)中心涮较,注冊(cè)中心分為兩類(lèi)左刽,file與非file襟雷,我們先來(lái)看看file 類(lèi)型
file 類(lèi)型的 注冊(cè)中心(file 類(lèi)型的配置方式)
file 類(lèi)型是個(gè)什么類(lèi)型呢刃滓,file類(lèi)型是用于做概念驗(yàn)證的注冊(cè)中心,它的目標(biāo)是通過(guò)配置文件耸弄,讓client 直接找到 seata server咧虎,從而免去第三方注冊(cè)中心的依賴,用來(lái)做快速驗(yàn)證计呈。
其核心的配置點(diǎn)如下
... 省略部分代碼
# service configuration, only used in client side 只在客戶端使用
service {
#transaction service group mapping
#配置事務(wù)組砰诵,如果注冊(cè)中心為nacos,需要在nacos中配置相應(yīng)的配置項(xiàng)捌显,這里就直接使用了文件配置茁彭。
vgroupMapping.my_test_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
#只在registry.conf 中 registry.type=file 時(shí)才使用此項(xiàng)配置,此項(xiàng)配置的作用就是制定seata server 在哪里扶歪。
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
... 省略部分代碼
}
此時(shí) seata client 與 seata server的關(guān)系就變?yōu)槿缦聢D所示
seata server 端的file.conf 內(nèi)容如下理肺,只定義了數(shù)據(jù)存儲(chǔ)方式。
## transaction log store, only used in seata-server
store {
## store mode: file善镰、db
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) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "mysql"
password = "mysql"
minConn = 1
maxConn = 10
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
}
}
nacos 方式的注冊(cè)中心
seata-server 的registry.conf 文件
registry {
# file 妹萨、nacos 、eureka炫欺、redis乎完、zk、consul品洛、etcd3树姨、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "localhost"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
nacos配置中心的部分
config {
# file摩桶、nacos 、apollo娃弓、zk典格、consul、etcd3
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = ""
group = "SEATA_GROUP" # 這個(gè)配置比較重要台丛。服務(wù)端對(duì)應(yīng)的配置項(xiàng)必須所屬這個(gè)配置項(xiàng)指定的組
username = "nacos"
password = "nacos"
}
}
}
客戶端對(duì)配置中心的指定耍缴,有不同的方式,原始方式是使用registry.conf挽霉,如果使用spring-cloud-alibaba防嗡,則可以通過(guò)spring 的application.yml(或者application.properties)文件指定
client 端的 registry.conf 文件內(nèi)容如下
registry {
# file 、nacos 侠坎、eureka蚁趁、redis、zk实胸、consul他嫡、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file庐完、nacos 钢属、apollo、zk门躯、consul淆党、etcd3
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = ""
group="SEATA_GROUP" 與 server 端的對(duì)應(yīng)
username = "nacos"
password = "nacos"
}
}
spring-cloud-alibaba client 端的配置方式
# ----------配置中心,如果無(wú)需使用配置中心讶凉,可以刪除此部分配置----------
# 設(shè)置配置中心服務(wù)端地址
#spring.cloud.nacos.config.server-addr=127.0.0.1:8848
# nacos認(rèn)證信息
spring.cloud.nacos.config.username=nacos
spring.cloud.nacos.config.password=nacos
spring.cloud.nacos.config.contextPath=/nacos
# 設(shè)置注冊(cè)中心服務(wù)端地址
spring.cloud.nacos.discovery.server-addr= 127.0.0.1:8848
# nacos認(rèn)證信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
seata client 端的內(nèi)部
client 內(nèi)部的回滾機(jī)制
按照官方文檔介紹染乌,seata會(huì)分析修改數(shù)據(jù)的sql,同時(shí)生成對(duì)應(yīng)的反向回滾SQL懂讯,這個(gè)回滾記錄在undo_log 表中荷憋。所以要求每一個(gè)client 都有一個(gè)對(duì)應(yīng)的undo_log表,定義如下
CREATE TABLE `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8;
那么發(fā)生分析的生成回滾SQL 的地方在哪里呢褐望?答案是在DataSource台谊。下面我們看看seata client 是如何辦到的。
一切的核心都在
io.seata.rm.datasource.DataSourceProxy
這里盡可能的簡(jiǎn)單譬挚,不做源碼分析,用DataSourceProxy(java.sql.Datasource)這種包含關(guān)系酪呻,同時(shí)DataSourceProxy的父類(lèi)AbstractDataSourceProxy實(shí)現(xiàn)了Datasource 接口减宣,所以DataSourceProxy 也是Datasource 的一個(gè)實(shí)現(xiàn),這使得DataSourceProxy有機(jī)會(huì)分析要執(zhí)行的SQL 與生成對(duì)應(yīng)的回滾SQL玩荠。
那么我們只要把DataSourceProxy 注冊(cè)成默認(rèn)的java.sql.Datasource實(shí)現(xiàn)漆腌,并提供給其他使用框架(mybatis, jdbctemplate)裝配贼邓,就達(dá)到我們的目的了。所以client端一切的配置都圍繞著把DataSourceProxy 注冊(cè)成默認(rèn)的java.sql.Datasource闷尿,或者把數(shù)據(jù)庫(kù)訪問(wèn)框架的Datasource配置DataSourceProxy 為來(lái)進(jìn)行的塑径。
明白了上面的思路,我們開(kāi)發(fā)時(shí)所做的工作就是完成 DataSourceProxy 的配置了填具。
從seata0.9 開(kāi)始统舀,提供了DataSource自動(dòng)代理功能,并且默認(rèn)是開(kāi)啟的劳景。這個(gè)騷操作是什么意思呢誉简。就是告訴你,你不用在去管DataSource放到DataSourceProxy中這個(gè)步驟了盟广,seata會(huì)自動(dòng)幫你完成這一步的操作闷串。然后你只需要將DataSource放到其他使用DataSource的地方就好啦。
client 端 DataSourceProxy的自動(dòng)裝配
seata 是如何自動(dòng)裝配的筋量?
一切的核心都在
io.seata.spring.boot.autoconfigure.SeataAutoConfiguration
和io.seata.spring.annotation.datasource.SeataDataSourceBeanPostProcessor
在SeataAutoConfiguration中
烹吵,通過(guò)seata.enableAutoDataSourceProxy
的值來(lái)判斷是否注冊(cè) SeataDataSourceBeanPostProcessor
bean。
這里有個(gè)巨大的坑 seata.enableAutoDataSourceProxy 這個(gè)配置寫(xiě)在桨武,yaml 文件里的時(shí)候肋拔,智能提示的配置項(xiàng)是enable-auto-data-source-proxy,無(wú)法生效玻募。必須寫(xiě)為enableAutoDataSourceProxy 的形式只损。
#智能提示給出的設(shè)置,無(wú)法生效七咧。
seata:
enable-auto-data-source-proxy: false
#正常生效的例子
seata:
enableAutoDataSourceProxy: false
這個(gè)SeataDataSourceBeanPostProcessor
實(shí)現(xiàn)了org.springframework.beans.factory.config.BeanPostProcessor
接口 跃惫,BeanPostProcessor
接口有兩個(gè)鉤子方法 postProcessBeforeInitialization
和 postProcessAfterInitialization
, 然后seata就在 postProcessAfterInitialization
中一頓操作猛如虎,完成了自動(dòng)代理功能艾栋。有興趣的自己看下源碼爆存。
BeanPostProcessor 鉤子流程如下
注意事項(xiàng)
- 接口中的兩個(gè)方法都要將傳入的bean返回,而不能返回null蝗砾,如果返回的是null那么我們通過(guò)getBean方法將得不到目標(biāo)先较。
- BeanFactory和ApplicationContext對(duì)待bean后置處理器稍有不同。ApplicationContext會(huì)自動(dòng)檢測(cè)在配置文件中實(shí)現(xiàn)了BeanPostProcessor接口的所有bean悼粮,并把它們注冊(cè)為后置處理器闲勺,然后在容器創(chuàng)建bean的適當(dāng)時(shí)候調(diào)用它,因此部署一個(gè)后置處理器同部署其他的bean并沒(méi)有什么區(qū)別扣猫。而使用BeanFactory實(shí)現(xiàn)的時(shí)候菜循,bean 后置處理器必須通過(guò)代碼顯式地去注冊(cè),在IoC容器繼承體系中的ConfigurableBeanFactory接口中定義了注冊(cè)方法
- 不要將BeanPostProcessor標(biāo)記為延遲初始化申尤。因?yàn)槿绻@樣做癌幕,Spring容器將不會(huì)注冊(cè)它們衙耕,自定義邏輯也就無(wú)法得到應(yīng)用。假如你在<beans />元素的定義中使用了'default-lazy-init'屬性勺远,請(qǐng)確信你的各個(gè)BeanPostProcessor標(biāo)記為'lazy-init="false"'橙喘。
關(guān)于生命周期送上一張福利
簡(jiǎn)單點(diǎn)看這個(gè)
自動(dòng)裝配的相關(guān)設(shè)置——均已1.1.0版本為例
開(kāi)啟與關(guān)閉
- 對(duì)于使用seata-spring-boot-starter的方式,默認(rèn)已開(kāi)啟數(shù)據(jù)源自動(dòng)代理,如需關(guān)閉胶逢,請(qǐng)配置seata.enableAutoDataSourceProxy=false厅瞎,該項(xiàng)配置默認(rèn)為true。
如需切換代理實(shí)現(xiàn)方式宪塔,請(qǐng)通過(guò)seata.useJdkProxy=false進(jìn)行配置,默認(rèn)為false磁奖,采用CGLIB作為數(shù)據(jù)源自動(dòng)代理的實(shí)現(xiàn)方式。 - 對(duì)于使用seata-all的方式某筐,請(qǐng)使用@EnableAutoDataSourceProxy來(lái)顯式開(kāi)啟數(shù)據(jù)源自動(dòng)代理功能比搭。如有需要,可通過(guò)該注解的useJdkProxy屬性進(jìn)行代理實(shí)現(xiàn)方式
的切換南誊。默認(rèn)為false,采用CGLIB作為數(shù)據(jù)源自動(dòng)代理的實(shí)現(xiàn)方式身诺。
其他版本設(shè)置
1.0.0: client.support.spring.datasource.autoproxy=true
0.9.0: support.spring.datasource.autoproxy=true
自動(dòng)配置開(kāi)啟時(shí)的配置示例-均已mybatis-plus 為例
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
return factoryBean.getObject();
}
}
application.yaml
spring:
application:
name: alicloudapp
datasource:
name: storageDataSource
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 2
max-active: 20
min-idle: 2
url: 'jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true'
username: root
password: openstack
driver-class-name: 'com.mysql.cj.jdbc.Driver'
name: storageDataSource
自動(dòng)配置關(guān)閉時(shí)的配置示例-均已mybatis-plus 為例
application.yaml
#seata 使用有幾個(gè)點(diǎn)
#1 自定義配置數(shù)據(jù)源參見(jiàn)SeataConfiguration,同時(shí)排除DataSourceAutoConfiguration
#2 配置微服務(wù)的seata 注冊(cè)名稱(chēng)抄囚,通過(guò)spring.cloud.alibaba.seata.tx-service-group=<service-group-name> 來(lái)配置霉赡,此配置需要和nacos 中的配置對(duì)應(yīng)。
#3 在nacos中幔托,需要配置對(duì)應(yīng)的配置性穴亏,已本工程為例,需要配置Data_ID=serivce.vgroup_mapp.<service-group-name>, GROUP=SEATA_GROUP 配置格式為text重挑,配置內(nèi)容為default的配置項(xiàng)嗓化,參見(jiàn)nacos-seata-configuration.png
#4 配置registry.conf 中的type 使用nacos 作為服務(wù)發(fā)現(xiàn)與配置中心
#5 seata server 需要修改 conf 目錄下的registry.conf 配置使用nacos 作為服務(wù)發(fā)現(xiàn)與配置中心,注意config段的配置項(xiàng)中谬哀,需要把nacos的group項(xiàng)添加上刺覆,并指定為SEATA_GROUP
#6 在使用spring-cloud-starter-alibaba-seata 的情況下,client端與server端需要保持版本一致史煎。
#7 在 seata 在 0.9 開(kāi)始會(huì)自動(dòng)代理(auto datasource proxy)datasource谦屑,需要選擇自定義裝配ProxyDataSource或者自動(dòng)代理裝配
spring:
autoconfigure:
# 自定義配置DataSource 和 ProxyDataSource時(shí),需要排除spring 原生的自動(dòng)化裝配類(lèi)
exclude: [org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure]
profiles: seata
cloud:
alibaba:
seata:
tx-service-group: storage-service-group
# 取消datasource自動(dòng)代理 enable-auto-data-source-proxy 這種連字符的寫(xiě)法是無(wú)法生效的篇梭,詳情看SeataAutoConfiguration源碼
seata:
enableAutoDataSourceProxy: false
MybatisPlusConfiguration.java
@Configuration
@MapperScan(basePackages = {"com.mycompany.alicloudapp.mapper"})
@Slf4j
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class MybatisPlusConfiguration {
@Profile("dev")
@Bean
public PerformanceMonitorInterceptor performanceMonitorInterceptor() {
return new PerformanceMonitorInterceptor();
}
/**
* MP 自帶分頁(yè)插件
*
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor page = new PaginationInterceptor();
page.setCountSqlParser(new JsqlParserCountOptimize(true));
return page;
}
/**
* @param sqlSessionFactory SqlSessionFactory
* @return SqlSessionTemplate
*/
@Autowired(required = true)
private DataSourceProperties dataSourceProperties;
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 從配置文件獲取屬性構(gòu)造datasource氢橙,注意前綴,這里用的是druid恬偷,根據(jù)自己情況配置,
* 原生datasource前綴取"spring.datasource"
*
* @return
*/
@Bean(name = "datasource")
@Primary
public DataSourceProxy druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
log.info("dataSourceProperties.getUrl():{}", dataSourceProperties);
druidDataSource.setUrl(dataSourceProperties.getUrl());
druidDataSource.setUsername(dataSourceProperties.getUsername());
druidDataSource.setPassword(dataSourceProperties.getPassword());
druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
druidDataSource.setInitialSize(1);
druidDataSource.setMaxActive(120);
druidDataSource.setMaxWait(60000);
druidDataSource.setMinIdle(1);
druidDataSource.setValidationQuery("Select 1 from DUAL");
druidDataSource.setTestOnBorrow(false);
druidDataSource.setTestOnReturn(false);
druidDataSource.setTestWhileIdle(true);
druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
druidDataSource.setMinEvictableIdleTimeMillis(25200000);
druidDataSource.setRemoveAbandoned(true);
druidDataSource.setRemoveAbandonedTimeout(1800);
druidDataSource.setLogAbandoned(true);
log.info("裝載dataSource........");
return new DataSourceProxy(druidDataSource);
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
// DataSource dataSourceProxy = new DataSourceProxy(druidDataSource);
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSourceProxy);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// bean.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml"));
bean.setMapperLocations(resolver.getResources("classpath*:/com.mycompany.alicloudapp.mapper/**/*.xml"));
SqlSessionFactory factory = null;
try {
factory = bean.getObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
return factory;
}
}
更簡(jiǎn)潔的寫(xiě)法是
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class MyDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
到此為止DataSourceProxy 相關(guān)的部分就配置完了