SpringBoot 2.0 集成Atomikos、Durid 實現多數據源、分佈式事務

使用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處理分佈式事務

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章