一、背景
SpringBoot 项目,通过引入seata-spring-boot-starter
来接入 Seata,Seata 的能力会通过 SpringBoot 的自动装配机制来引入。在学习的时候是梳理有什么强大的、科技感十足的能力,但在试点的时候则更多考虑的是有哪些安保机制,比如通过什么开关来启停某某功能,以保护程序。今天就梳理有什么安保机制,划清能力边界,让使用者不必太过担心;生产中安全永远是第一的嘛。
二、认知拉齐-starter 中提供什么能力
第二部分若真不熟悉,看不懂可跳过进入第三部分。
Pom 依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
复制代码
梳理自动装配类,直接从seata-spring-boot-starter
中找文件spring.factories
,其中有以下自动装配类:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.seata.spring.boot.autoconfigure.SeataDataSourceAutoConfiguration,\
io.seata.spring.boot.autoconfigure.SeataAutoConfiguration,\
io.seata.spring.boot.autoconfigure.HttpAutoConfiguration,\
io.seata.spring.boot.autoconfigure.SeataSagaAutoConfiguration
复制代码
1) SeataDataSourceAutoConfiguration
Seata 的 AT 和 AX 模式是基于数据源代理实现的,SeataDataSourceAutoConfiguration
中自动对数据源做了代理,不用使用者手动实现代理。
2) SeataAutoConfiguration
- 构建全局事务扫描器
GlobalTransactionScanner
,注入到容器中,其内部做 2 件事情- 会初始化 TM、RM 客户端
- 扫描
Bean
,对添加了全局事务注解的类(@GlobalTransactional
、@GlobalLock
、@TwoPhaseBusinessAction
)生成代理对象,添加对应的拦截器(拦截器内补充分布式事务的能力)
- 注入一个
FailureHandler
,其默认实现DefaultFailureHandlerImpl
中只会打印错误日志,建议重写,异常发生时及时告知使用者。
3)HttpAutoConfiguration
- 应用通过 http 协议通信时,通过 http header 透传 xid
4)SeataSagaAutoConfiguration
- Saga 模式才用的到,目前还不了解
三、探索 Seata 所提供的安保机制
3.1 引入 seata-spring-boot-starter 之后 Seata 功能就启动了嘛?
答案:不,有一个开关seata.enabled
,若为 true:则启动自动装配;若为 false:则不启用自动装配。比如就在开发测试环境验证,暂且还不上生产,就可以通过这个开关来实现。
1)详情:
application.yml 中留意这个配置:
seata:
enabled: true
复制代码
上述几个自动装配类中都有激活条件,ConditionalOnProperty
或 ConditionalOnExpression
其用意为,当seata.enabled=true
的情况下,自动装配类才生效。
@ConditionalOnProperty(prefix = SEATA_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
...
@ConditionalOnExpression("${seata.enabled:true}
复制代码
3.2 数据源是被自动代理嘛?
答案 :不,有开关
1)详情:
seata.enableAutoDataSourceProxy:true
,若为 true:则启动自动代理,若为 false:则不启用自动代理。
seata:
enableAutoDataSourceProxy: true
复制代码
生效源码见下方:
@ConditionalOnExpression("${seata.enabled:true} && ${seata.enableAutoDataSourceProxy:true} && ${seata.enable-auto-data-source-proxy:true}")
public class SeataDataSourceAutoConfiguration {
复制代码
3.3 所有的数据源都会被代理嘛
- 答案:不,有两种办法来避免
1)方法 1:自动代理+排除法:
- 开启自动代理
seata.enableAutoDataSourceProxy:true
- 指定不需要被代理的 DataSouce 的 bean 名称(数组形式)
seata: enableAutoDataSourceProxy: true excludesForAutoProxying: dataSource1,dataSource2,dataSource3 复制代码
2)方法 2:手动代理
- 关闭自动代理
seata.enableAutoDataSourceProxy:false
- 手动方式对数据源做代理,参考这个代码
SeataDataSourceProxy buildProxy(DataSource origin, String proxyMode) { if (BranchType.AT.name().equalsIgnoreCase(proxyMode)) { return new DataSourceProxy(origin); } if (BranchType.XA.name().equalsIgnoreCase(proxyMode)) { return new DataSourceProxyXA(origin); } throw new IllegalArgumentException("Unknown dataSourceProxyMode: " + proxyMode); } 复制代码
3.4 加了 Seata 事务注解就生效了嘛,被人不小心复制走的有办法不生效嘛
答案:算是有,目前已知有以下几种方法
1) 方法 1: 可指定 Seata 注解扫描的包路径,根据情况结合 排除 bean 名称的方法一起用
// add scannable packages
GlobalTransactionScanner.addScannablePackages(seataProperties.getScanPackages());
// add excludeBeanNames
GlobalTransactionScanner.addScannerExcludeBeanNames(seataProperties.getExcludesForScanning());
复制代码
2) 方法 2:使用显式的编程 API 开启事务(待验证)
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
try {
tx.begin(300000, "test-group");
accountService.addUserInfo(account.getAccountNo(),account.getAccountName(),account.getTip());
storeService.decreaseStock(store);
tx.commit();
} catch (TransactionException e) {
try {
tx.rollback();
} catch (TransactionException transactionException) {
transactionException.printStackTrace();
}
//e.printStackTrace();
}
复制代码
3.5 Seata 的全局事务能力是否可以动态启停?
答案:可以
1) 场景 1:先禁用再启用,可通过service.disableGlobalTransaction=true
在启东时禁用,然后在运行期变更配置为 false ,如果配置中心能力够强,还可单独给某个实例激活全局事务。对应的源码如下
public void onChangeEvent(ConfigurationChangeEvent event) {
if (ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION.equals(event.getDataId())) {
disableGlobalTransaction = Boolean.parseBoolean(event.getNewValue().trim());
//compareAndSet(expect, update), 要想更新成功的话,那么这个expect的值一定是1,否则是不会被更新的。
//即配置中心下达启用分布式锁,且客户端未启动
if (!disableGlobalTransaction && initialized.compareAndSet(false, true)) {
LOGGER.info("{} config changed, old value:true, new value:{}", ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION,
event.getNewValue());
initClient();
ConfigurationCache.removeConfigListener(ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION, this);
}
}
}
复制代码
2)场景 2:在运行期关闭已运行的全局事务能力,可通过service.disableGlobalTransaction=true
在运行期,关闭全局事务
3.6 Seata 是否有降级能力?
答案:有,有两个维度的降级能力,
1)方法 1:使用 3.5 中的是否禁用全局事务手动降级
- 方法 2:自动降级。配置个阈值,当自检周期(滑动时间窗口内)异常次数达到阈值后就禁用全局事务,当自检周期(滑动时间窗口内)连续成功后则恢复。通过设置
client.tm.degradeCheck=true
,结合degradeCheckAllowTimes
和degradeCheckPeriod
来设置阈值与自检周期;如下示例:
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
复制代码
3.7 Seata 是否可以不代理无需分布式事务的数据源?
答案:可以变通实现,有 2 种方法。
1)方法 1:DataSouce 从一个变成 2 个,即一个是原始 DataSource,另一个基于原始的 DataSource 代理,生成一个代理的 DataSouce,在使用到分布式事务的逻辑中才使用代理的 DataSouce,不使用分布式事务的逻辑中使用原始的 DataSource。但这种两个 DataSouce 其实是同一个,会使用相同的连接池(用连接池的情况下)。
2)方法 2:使用两个完全独立的 DataSouce,其中一个做与分布式事务无关的逻辑;另一个做代理,提供分布式事务的能力。
3.8 Seata 的全局事务能力是否可以按请求属性启用
答案:可以有,这里提供 1 种方法。
1)方法 1:把一个方法拆分,根据流量属性判断是否需要进入到开启分布式事务的方法中。
function A(param){
if(param.xx == 开启分布式事务){
A-2(param)
}else{
A-1(param)
}
}
function A-1(param){
}
//A-2上添加分布式事务注解
fcuntion A-2(param){
}
复制代码
四、总结
本篇梳理的内容是:当我们在试点分布式事务系统 Seata 的时候,对系统安全方面可能有哪些诉求,以及 Seata 能提供怎样的保护能力。毕竟我们引入新系统是为了解决问题而不是创造问题,没有足够使用经验,对内部机制掌握不够的情况下,全面的掌握 Seata 所提供的安保机制,按需禁用或开启、将新能力和老逻辑划清边界、以最小化测试单元的原则来推进,应该会更加顺利。