使用JTA處理分佈式事務
Spring Boot通過Atomkos或Bitronix的內嵌事務管理器支持跨多個XA資源的分佈式JTA事務,當部署到恰當的J2EE應用服務器時也會支持JTA事務。
當發現JTA環境時,Spring Boot將使用Spring的 JtaTransactionManager 來管理事務。自動配置的JMS,DataSource和JPA beans將被升級以支持XA事務。可以使用標準的Spring idioms,比如 @Transactional ,來參與到一個分佈式事務中。如果處於JTA環境,但仍想使用本地事務,你可以將 spring.jta.enabled 屬性設置爲 false 來禁用JTA自動配置功能。
常用的事務管理器有:Atomikos、Bitronix、Narayana。
本文主要圍繞Atomikos展開,另外的常用事務管理器可執行搜索瞭解。
Atomikos簡介
Atomikos是一種流行的開源事務管理器,可以嵌入到Spring Boot應用程序中,你可以使用spring-boot-starter-jta-atomikos啓動器來拉取適當的Atomikos庫,Spring Boot可以自動配置Atomikos,並確保將適當的依賴設置應用到你的Spring bean中,以實現正確的啓動和關閉順序。
默認情況下,Atomikos事務日誌被寫入應用程序的主目錄中的transaction-logs目錄(應用程序jar文件所在的目錄),你可以通過在application.properties文件中設置spring.jta.log-dir來定製這個目錄的位置,從spring.jta.atomikos.properties開始的屬性還可以用於定製Atomikos UserTransactionServiceImp,
請參閱 AtomikosProperties Javadoc 獲取完整的詳細信息。
爲了確保多個事務管理器可以安全地協調相同的資源管理器,每個Atomikos實例必須配置唯一的ID,默認情況下,這個ID是Atomikos運行的機器的IP地址。爲了確保生產中具有唯一性,你應該爲應用程序的每個實例配置spring.jta.transaction-manager-id屬性的不同值。
與SpringBoot集成
SpringBoot默認提供了配置依賴,可直接導入jar包。
compile group: 'org.springframework.boot', name: 'spring-boot-starter-jta-atomikos', version: '2.0.4.RELEASE'
compile group: 'com.alibaba', name: 'druid', version: '1.1.10'
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.46'
compile group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '1.3.2'
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.0.4.RELEASE'
配置多數據源
配置atomikos事務管理器,並配置druid作爲數據源並且進行監控
注入數據源使用使用的是com.atomikos.jdbc.AtomikosDataSourceBean,所以參照此類,可以制定以下配置,再使用
@ConfigurationProperties註解根據前綴將配置注入該datasource,省取繁瑣的設置配置。
##Spring表數據庫配置
spring.jta.atomikos.datasource.spring.max-pool-size=25
spring.jta.atomikos.datasource.spring.min-pool-size=3
spring.jta.atomikos.datasource.spring.max-lifetime=20000
spring.jta.atomikos.datasource.spring.borrow-connection-timeout=10000
spring.jta.atomikos.datasource.spring.unique-resource-name=spring
spring.jta.atomikos.datasource.spring.xa-properties.url=jdbc:mysql://127.0.0.1:3306/spring?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=UTC
spring.jta.atomikos.datasource.spring.xa-properties.username=root
spring.jta.atomikos.datasource.spring.xa-properties.password=root
spring.jta.atomikos.datasource.spring.xa-properties.driverClassName=com.mysql.jdbc.Driver
# 初始化大小,最小,最大
spring.jta.atomikos.datasource.spring.xa-properties.initialSize=10
spring.jta.atomikos.datasource.spring.xa-properties.minIdle=20
spring.jta.atomikos.datasource.spring.xa-properties.maxActive=100
## 配置獲取連接等待超時的時間
spring.jta.atomikos.datasource.spring.xa-properties.maxWait=60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
spring.jta.atomikos.datasource.spring.xa-properties.timeBetweenEvictionRunsMillis=60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
spring.jta.atomikos.datasource.spring.xa-properties.minEvictableIdleTimeMillis=300000
spring.jta.atomikos.datasource.spring.xa-properties.testWhileIdle=true
spring.jta.atomikos.datasource.spring.xa-properties.testOnBorrow=false
spring.jta.atomikos.datasource.spring.xa-properties.testOnReturn=false
# 打開PSCache,並且指定每個連接上PSCache的大小
spring.jta.atomikos.datasource.spring.xa-properties.poolPreparedStatements=true
spring.jta.atomikos.datasource.spring.xa-properties.maxPoolPreparedStatementPerConnectionSize=20
# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆
spring.jta.atomikos.datasource.spring.xa-properties.filters=stat,slf4j,wall
spring.jta.atomikos.datasource.spring.xa-data-source-class-name=com.alibaba.druid.pool.xa.DruidXADataSource
#------------------------------ 分隔符-------------------------------------
##test表數據庫配置
spring.jta.atomikos.datasource.test.max-pool-size=25
spring.jta.atomikos.datasource.test.min-pool-size=3
spring.jta.atomikos.datasource.test.max-lifetime=20000
spring.jta.atomikos.datasource.test.borrow-connection-timeout=10000
spring.jta.atomikos.datasource.test.unique-resource-name=test
spring.jta.atomikos.datasource.test.xa-properties.url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=UTC
spring.jta.atomikos.datasource.test.xa-properties.username=root
spring.jta.atomikos.datasource.test.xa-properties.password=root
spring.jta.atomikos.datasource.test.xa-properties.driverClassName=com.mysql.jdbc.Driver
spring.jta.atomikos.datasource.test.xa-properties.initialSize=10
spring.jta.atomikos.datasource.test.xa-properties.minIdle=20
spring.jta.atomikos.datasource.test.xa-properties.maxActive=100
spring.jta.atomikos.datasource.test.xa-properties.maxWait=60000
spring.jta.atomikos.datasource.test.xa-properties.timeBetweenEvictionRunsMillis=60000
spring.jta.atomikos.datasource.test.xa-properties.minEvictableIdleTimeMillis=300000
spring.jta.atomikos.datasource.test.xa-properties.testWhileIdle=true
spring.jta.atomikos.datasource.test.xa-properties.testOnBorrow=false
spring.jta.atomikos.datasource.test.xa-properties.testOnReturn=false
spring.jta.atomikos.datasource.test.xa-properties.poolPreparedStatements=true
spring.jta.atomikos.datasource.test.xa-properties.maxPoolPreparedStatementPerConnectionSize=20
# 配置監控統計攔截的filters,去掉後監控界面sql無法統計,'wall'用於防火牆
spring.jta.atomikos.datasource.test.xa-properties.filters=stat,slf4j,wall
spring.jta.atomikos.datasource.test.xa-data-source-class-name=com.alibaba.druid.pool.xa.DruidXADataSource
接着將配置注入數據源,並且設置durid監控中心:
@Configuration
@EnableConfigurationProperties
@EnableTransactionManagement(proxyTargetClass = true)
public class MybatisConfiguration {
/**
* spring數據庫配置前綴.
*/
final static String SPRING_PREFIX = "spring.jta.atomikos.datasource.spring";
/**
* test數據庫配置前綴.
*/
final static String TEST_PREFIX = "spring.jta.atomikos.datasource.test";
/**
* The constant logger.
*/
final static Logger logger = LoggerFactory.getLogger(MybatisConfiguration.class);
/**
* 配置druid顯示監控統計信息
* 開啓Druid的監控平臺 http://localhost:8080/druid
*
* @return servlet registration bean
*/
@Bean
public ServletRegistrationBean druidServlet() {
logger.info("Init Druid Servlet Configuration ");
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
// IP白名單,不設默認都可以
// servletRegistrationBean.addInitParameter("allow", "192.168.2.25,127.0.0.1");
// IP黑名單(共同存在時,deny優先於allow)
servletRegistrationBean.addInitParameter("deny", "192.168.1.100");
//控制檯管理用戶
servletRegistrationBean.addInitParameter("loginUsername", "root");
servletRegistrationBean.addInitParameter("loginPassword", "dashuai");
//是否能夠重置數據 禁用HTML頁面上的“Reset All”功能
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
/**
* 註冊一個filterRegistrationBean
*
* @return filter registration bean
*/
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
//添加過濾規則
filterRegistrationBean.addUrlPatterns("/*");
//添加不需要忽略的格式信息
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
/**
* 配置Spring數據庫的數據源
*
* @return the data source
*/
@Bean(name = "SpringDataSource")
@ConfigurationProperties(prefix = SPRING_PREFIX) // application.properties中對應屬性的前綴
public DataSource springDataSource() {
return new AtomikosDataSourceBean();
}
/**
* 配置Test數據庫的數據源
*
* @return the data source
*/
@Bean(name = "TestDataSource")
@ConfigurationProperties(prefix = TEST_PREFIX) // application.properties中對應屬性的前綴
public DataSource testDataSource() {
return new AtomikosDataSourceBean();
}
}
再分別對每個數據源進行sessionfactory的配置:
@Configuration
@MapperScan(basePackages = {"com.dashuai.learning.jta.mapper.spring"}, sqlSessionFactoryRef = "springSqlSessionFactory")
public class SpringDataSourceConfiguration {
/**
* The constant MAPPER_XML_LOCATION.
*/
public static final String MAPPER_XML_LOCATION = "classpath:mapper/spring/*.xml";
/**
* The Open plat form data source.
*/
@Autowired
@Qualifier("SpringDataSource")
DataSource springDataSource;
/**
* 配置Sql Session模板
*
* @return the sql session template
* @throws Exception the exception
*/
@Bean
public SqlSessionTemplate springSqlSessionTemplate() throws Exception {
return new SqlSessionTemplate(springSqlSessionFactory());
}
/**
* 配置SQL Session工廠
*
* @return the sql session factory
* @throws Exception the exception
*/
@Bean
public SqlSessionFactory springSqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(springDataSource);
//指定XML文件路徑
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_XML_LOCATION));
return factoryBean.getObject();
}
}
Test數據源與Spring數據源大同小異,詳情可查看源碼。
配置到達這裏就完成了。再寫一個測試用例,測試多數據源和事務效果:
@RunWith(SpringRunner.class)
@SpringBootTest
public class JtaApplicationTests {
@Autowired
UserService userService;
@Autowired
PeopleService peopleService;
@Test
@Transactional
public void contextLoads() {
User user=new User();
user.setUserName("你妹哦");
user.setPassword("我去");
user.setAge(20);
userService.insertUser(user);
People people = new People();
people.setName("你大爺的");
people.setAge(50);
people.setSex("男");
peopleService.insertPeople(people);
}
}
由於是測試用例,默認@Transactional在全部成功執行完成會回滾,經測試沒問題。
再寫一個接口對兩個表進行添加操作,並且其中一條語句執行失敗,查看回滾效果:
@Override
@Transactional
public Boolean insertUserAndPeople(User user, People people) throws RuntimeException {
peopleMapper.insert(people);
try {
userMapper.insertSelective(user);
} catch (Exception e) {
throw new RuntimeException("拋出runtime異常,導致回滾數據");
}
return true;
}
@PostMapping(value = "/insertPeopleAndUser", produces = "application/json;charset=UTF-8")
@ApiOperation(value = "添加兩個表", notes = "測試分佈式事務", response = ApiResult.class)
@ApiImplicitParams({
@ApiImplicitParam(name = "peopleName", value = "人名", required = true, dataType = "String"),
@ApiImplicitParam(name = "userName", value = "用戶信息", required = true, dataType = "String")
})
public ApiResult insertPeopleAndUser(String peopleName,String userName) throws Exception {
User user=new User();
user.setUserName(userName);
user.setPassword("15251251");
user.setAge(22);
People people = new People();
people.setName(peopleName);
people.setAge(20);
people.setSex("男");
Boolean isSuccess = peopleService.insertUserAndPeople(user, people);
if (isSuccess) {
return ApiResult.prepare().success("同時添加兩表成功!");
}
return ApiResult.prepare().error(JSONParseUtils.object2JsonString(people), 500, "添加失敗,全部回滾");
}
由於User表的name字段設置了唯一,所以只需插入重複即會報錯。
注意,如果@Tranactional失效,可以思考以下問題:
1.mysql是否使用的是innodb;
2.Spring AOP只在拋出RuntimeException時纔回滾,就是需要在捕獲異常的最後加上throw new RuntimeException;
3.嘗試手動回滾。給註解加上參數如:@Transactional(rollbackFor=Exception.class)
分別向spring庫裏的user表和test庫裏的tet表插入一條數據
接着,返回接口添加user表已有的name,觸發異常,查看回滾效果。
出現唯一索引異常
查看錶插入情況:
可以看到,people表並沒有插入數據,也就是當出現異常時,全部數據都回滾了。
學習之路,本就如同逆流而上,不進即退。加油!
本例子的源碼已上傳至github:
https://github.com/liaozihong/SpringBoot-Learning/tree/master/SpringBoot-JTA-Atomikos
參考鏈接:
Spring boot atomikos 配置好後 @Transactional 註解不生效
Spring Boot(二三) - 使用JTA處理分佈式事務