在作者之前的 十二條後端開發經驗分享,純乾貨 文章中介紹的 優雅得Springboot + mybatis配置多數據源方式 里有很多小伙伴在評論區留言詢問多個數據源同時在一個方法中使用時,事務是否會正常有效,這裡作者 理論 + 實踐 給大家解答一波,老規矩,附作者github地址: https:// ...
在作者之前的 十二條後端開發經驗分享,純乾貨 文章中介紹的 優雅得Springboot + mybatis配置多數據源方式
里有很多小伙伴在評論區留言詢問多個數據源同時在一個方法中使用時,事務是否會正常有效,這裡作者 理論 + 實踐
給大家解答一波,老規矩,附作者github地址:
一. 數據源跨庫但是不跨 MySql
實例
這個形式就是數據源在同一個 MySQL
下,但是 jdbc-url
上的資料庫配置不同,涉及多個資料庫時,如果方法中發生異常,只有開啟事務的數據源會發生回滾,其他數據源不會回滾。看到這裡可能有點迷惑,什麼是 只有開啟事務的數據源會發生回滾,其他數據源不會回滾?
下麵給出代碼驗證:
主數據源配置
@Slf4j
@EnableTransactionManagement
@EnableAspectJAutoProxy
@Configuration
@MapperScan(basePackages = "ltd.newbee.mall.core.dao", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class Db1DataSourceConfig {
@Primary
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(DruidProperties druidProperties) {
DruidDataSource build = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(build);
}
/**
* @param datasource 數據源
* @return SqlSessionFactory
* @Primary 預設SqlSessionFactory
*/
@Primary
@Bean(name = "masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource datasource,
Interceptor interceptor) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(datasource);
// mybatis掃描xml所在位置
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/*.xml"));
bean.setTypeAliasesPackage("ltd.**.core.entity");
bean.setPlugins(interceptor);
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setLogicDeleteField("isDeleted");
dbConfig.setLogicDeleteValue("1");
dbConfig.setLogicNotDeleteValue("0");
globalConfig.setDbConfig(dbConfig);
bean.setGlobalConfig(globalConfig);
log.info("masterDataSource 配置成功");
return bean.getObject();
}
@Primary
@Bean(name = "masterTransactionManager")
public DataSourceTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
從數據源配置
@Slf4j
@ConditionalOnProperty(value = "transactional.mode", havingValue = "seata")
@EnableTransactionManagement
@EnableAspectJAutoProxy
@Configuration
@MapperScan(basePackages = "ltd.newbee.mall.slave.dao", sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class Db2DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource(DruidProperties druidProperties) {
DruidDataSource build = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(build);
}
/**
* @param datasource 數據源
* @return SqlSessionFactory
* @Primary 預設SqlSessionFactory
*/
@Bean(name = "slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource datasource,
Interceptor interceptor) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(datasource);
// mybatis掃描xml所在位置
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:slavemapper/*.xml"));
bean.setTypeAliasesPackage("ltd.**.slave.entity");
bean.setPlugins(interceptor);
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setLogicDeleteField("isDeleted");
dbConfig.setLogicDeleteValue("1");
dbConfig.setLogicNotDeleteValue("0");
globalConfig.setDbConfig(dbConfig);
bean.setGlobalConfig(globalConfig);
log.info("slaveDataSource 配置成功");
return bean.getObject();
}
@Bean(name = "slaveTransactionManager")
public DataSourceTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
劃重點-上述代碼在每個數據源中都配置了 DataSourceTransactionManager
(事務管理器),並且在主配置中添加 @Primary
註解,表示預設事務管理器優先使用主數據源的事務管理器。 下麵給出測試代碼:
/**
* Springboot測試類
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class MultiDataSourceTest {
@Autowired
private MultiDataService multiDataService;
@Test
public void testRollback() {
multiDataService.testRollback();
}
}
/**
* MultiDataService實現類
*/
@Slf4j
@Service
public class MultiDataServiceImpl implements MultiDataService {
@Autowired
private TbTable1Service tbTable1Service;
@Autowired
private TbTable2Service tbTable2Service;
@Autowired
private PlatformTransactionManager transactionManager;
@Override
public void testRollback() {
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
try {
TbTable1 tbTable1 = new TbTable1();
tbTable1.setName("test1");
// 插入table1表
boolean save1 = tbTable1Service.save(tbTable1);
TbTable2 tbTable2 = new TbTable2();
tbTable2.setName("test2");
// 插入table2表
boolean save2 = tbTable2Service.save(tbTable2);
int i = 1 / 0;
transactionManager.commit(transaction);
Assert.isTrue(save1 && save2);
} catch (Exception e) {
log.info(e.getMessage(), e);
transactionManager.rollback(transaction);
}
}
}
執行結果:table1表回滾成功,table2表回滾失敗。由此結果,對於 只有開啟事務的數據源會發生回滾,其他數據源不會回滾?
我們的解釋就是 Spring
中預設使用的事務管理器是使用主數據源配置還是從數據源配置由我們通過 @Primary
決定,當我們把 @Primary
切換在從數據源配置上,執行結果:table2表回滾成功,table1表回滾失敗。那怎麼解決這個問題?
當涉及到跨庫或者跨 MySQL
實例,想要保證事務操作,我們這裡先給出XA
事務解決方案。附 XA
事務的說明:
XA 是由 X/Open 組織提出的分散式事務規範,XA 規範主要定義了事務協調者(Transaction Manager)和資源管理器(Resource Manager)之間的介面。
事務協調者(Transaction Manager),因為 XA 事務是基於兩階段提交協議的,所以需要有一個協調者,來保證所有的事務參與者都完成了準備工作,也就是 2PC 的第一階段。如果事務協調者收到所有參與者都準備好的消息,就會通知所有的事務都可以提交,也就是 2PC 的第二階段。
資源管理器(Resource Manager),負責控制和管理實際資源,比如資料庫。
(劃重點)XA 的 MySQL 實現使 MySQL 伺服器能夠充當資源管理器,在全局事務中處理 XA 事務。連接到 MySQL 伺服器的客戶端程式充當事務協調者
XA 事務的執行流程
XA 事務是兩階段提交的一種實現方式,根據 2PC 的規範,XA 將一次事務分割成了兩個階段,即 Prepare 和 Commit 階段。
Prepare 階段,TM 向所有 RM 發送 prepare 指令,RM 接受到指令後,執行數據修改和日誌記錄等操作,然後返回可以提交或者不提交的消息給 TM。如果事務協調者 TM 收到所有參與者都準備好的消息,會通知所有的事務提交,然後進入第二階段。
Commit 階段,TM 接受到所有 RM 的 prepare 結果,如果有 RM 返回是不可提交或者超時,那麼向所有 RM 發送 Rollback 命令;如果所有 RM 都返回可以提交,那麼向所有 RM 發送 Commit 命令,完成一次事務操作。
下麵給出兩種基於 XA
事務的解決方案:
Springboot
項目中可以使用jta
,完成對XA
協議的支持,缺點就是jta
需要改造數據源配置Springboot
項目引入seata
,seata
支持XA
協議,且引入seata-spring-boot-starter
依賴對業務無侵入,缺點需要引入seata-server
降低了系統可用性
Springboot
項目中可以啟用 jta
- 引入
spring-boot-starter-jta-atomikos
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
- 修改主從數據源
DataSource
配置,進行包裝添加XA
數據源支持,如下;
@Primary
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource dataSource(DruidProperties druidProperties) {
DruidXADataSource dataSource = druidProperties.dataSource(new DruidXADataSource());
dataSource.setUrl("jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8");
dataSource.setUsername("root");
dataSource.setPassword("");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
atomikosDataSourceBean.setUniqueResourceName("master-xa");
atomikosDataSourceBean.setXaDataSource(dataSource);
return atomikosDataSourceBean;
}
- 添加
JtaTransactionManager
@Bean
public JtaTransactionManager transactionManager() throws Exception {
JtaTransactionManager transactionManager = new JtaTransactionManager();
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(true);
userTransactionManager.setTransactionTimeout(3000);
transactionManager.setUserTransaction(userTransactionManager);
transactionManager.setAllowCustomIsolationLevels(true);
return transactionManager;
}
- 完成測試,代碼如下:
/**
* Springboot測試類
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class MultiDataSourceTest {
@Autowired
private MultiDataService multiDataService;
@Test
public void jtaTestRollback() {
multiDataService.jtaTestRollback();
}
}
/**
* MultiDataService實現類
*/
@Slf4j
@Service
public class MultiDataServiceImpl implements MultiDataService {
@Autowired
private TbTable1Service tbTable1Service;
@Autowired
private TbTable2Service tbTable2Service;
@Autowired
private JtaTransactionManager jtaTransactionManager;
@Override
public void jtaTestRollback() {
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus transaction = jtaTransactionManager.getTransaction(transactionDefinition);
try {
TbTable1 tbTable1 = new TbTable1();
tbTable1.setName("test1");
boolean save1 = tbTable1Service.save(tbTable1);
TbTable2 tbTable2 = new TbTable2();
tbTable2.setName("test2");
boolean save2 = tbTable2Service.save(tbTable2);
int i = 1 / 0;
jtaTransactionManager.commit(transaction);
Assert.isTrue(save1 && save2);
} catch (Exception e) {
log.info(e.getMessage(), e);
jtaTransactionManager.rollback(transaction);
}
}
}
可以看到我們使用的是 JtaTransactionManager
, 執行結果:table1表回滾成功,table2表回滾成功。驗證OK
引入 seata
,添加XA協議支持
- 下載安裝啟動
seata-server
,這裡給出官網教程:https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html - 在 Springboot中引入seata最新依賴
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
- 在yml文件中添加
seata
配置
seata:
config:
type: file
registry:
type: file
application-id: newbeemall # Seata 應用編號,預設為 ${spring.application.name}
tx-service-group: newbeemall-group # Seata 事務組編號,用於 TC 集群名
# 服務配置項,對應 ServiceProperties 類
service:
# 虛擬組和分組的映射
vgroup-mapping:
newbeemall-group: default
# 分組和 Seata 服務的映射
grouplist:
default: 127.0.0.1:8091
data-source-proxy-mode: XA
enabled: true
- 完成測試,代碼如下:
/**
* Springboot測試類
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class MultiDataSourceTest {
@Autowired
private MultiDataService multiDataService;
@Test
public void seataTestRollback() {
multiDataService.seataTestRollback();
}
}
/**
* MultiDataService實現類
*/
@Slf4j
@Service
public class MultiDataServiceImpl implements MultiDataService {
@Autowired
private TbTable1Service tbTable1Service;
@Autowired
private TbTable2Service tbTable2Service;
@GlobalTransactional
@Override
public void seataTestRollback() {
log.info("當前 XID: {}", RootContext.getXID());
TbTable1 tbTable1 = new TbTable1();
tbTable1.setName("test1");
boolean save1 = tbTable1Service.save(tbTable1);
TbTable2 tbTable2 = new TbTable2();
tbTable2.setName("test2");
boolean save2 = tbTable2Service.save(tbTable2);
int i = 1 / 0;
}
}
如上代碼,使用 seata
時需要啟用 @GlobalTransactional
註解,並且在事務中傳遞 XID
( RootContext.getXID()
),執行結果:table1表回滾成功,table2表回滾成功。驗證OK
二. 數據源分佈在不同 MySql
實例
當數據源分佈在不同 MySql
實例時,這時候其實已經進入分散式事務的範疇,由上可知,XA
事務可以解決分散式環境下事務問題,也就是說上述最後兩種解決方案都可以解決分散式事務問題,但是實際使用過程中,我們建議使用 seata
,理由是他不僅支持 XA
事務還支持 AT、Saga、TCC
事務模型。引入 seata
官網介紹
Seata 是一款開源的分散式事務解決方案,致力於提供高性能和簡單易用的分散式事務服務。Seata 將為用戶提供了 AT、TCC、SAGA 和 XA 事務模式,為用戶打造一站式的分散式解決方案。
總結
關於多數據源事務的問題,不管跨不跨庫其實都屬於分散式事務的問題。推薦使用 seata
解決。
實踐代碼放在newbeemall項目:https://github.com/wayn111/newbee-mall/tree/springboot2.7 分支下
歡迎大家點贊、關註、評論,想要跟作者溝通技術問題的話可以加我微信【waynaqua】,歡迎大家前來交流。