項目中需要數據庫分布式事物的實現,於是采用了atumikos技術。
因為生產上需要穩定,所以采用了springboot 1.5.9.RELEASE版本。
本文代碼gitlab下載地址: https://gitlab.com/atomikos/springBootMultDB-druidOracle.git
新建一個springboot項目,然后依次添加本文所有代碼。我的項目結構如下:
1、pom.xml 代碼
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>springBootMultDB</groupId> <artifactId>springBootMultDB-druidOracle</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springBootMultDB-druidOracle</name> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 排除spring boot默認使用的tomcat,使用jetty --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>11.2.0.4.0</version> </dependency> </dependencies> </project>
這里的ojdbc6的jar注意一下,不是maven中央庫能夠下載到的,所以需要去oracle官方下載驅動jar包:
https://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html
下載好驅動jar包之后,cmd命令窗口進入jar包所在目錄,執行mvn安裝jar到本地repository庫,mvn命令:
mvn install:install-file -Dfile=d:/java-jar/ojdbc6.jar -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0.4.0 -Dpackaging=jar
把-Dfile的參數替換成你本地的ojdbc6的目錄即可。
-Dversion版本參數查看方法:打開ojdbc6.jar,進入META-INF,打開MANIFEST.MF,找到 Implementation-Version的值,我這里是: 11.2.0.4.0
2、application.properties配置
server.port=8082
spring.application.name=springBootMultDB-druidOracle
# Oracle 1
spring.datasource.test1.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.test1.username=system
spring.datasource.test1.password=ZHUwen12
spring.datasource.test1.minPoolSize = 3
spring.datasource.test1.maxPoolSize = 25
spring.datasource.test1.maxLifetime = 20000
spring.datasource.test1.borrowConnectionTimeout = 30
spring.datasource.test1.loginTimeout = 30
spring.datasource.test1.maintenanceInterval = 60
spring.datasource.test1.maxIdleTime = 60
spring.datasource.test1.testQuery = select 1 from dual
# # Oracle 2
spring.datasource.test2.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.test2.username=zhuwen
spring.datasource.test2.password=ZHUwen12
spring.datasource.test2.minPoolSize = 3
spring.datasource.test2.maxPoolSize = 25
#連接最大存活時間
spring.datasource.test2.maxLifetime = 20000
#獲取連接失敗重新獲等待最大時間
spring.datasource.test2.borrowConnectionTimeout = 30
#登入超時
spring.datasource.test2.loginTimeout = 30
# 連接回收時間
spring.datasource.test2.maintenanceInterval = 60
#最大閑置時間,超過最小連接池連接的連接將將關閉
spring.datasource.test2.maxIdleTime = 60
spring.datasource.test2.testQuery = select 1 from dual
這里的oracle兩個庫配置,只有username不一樣,在oracle里面,兩個user可視為兩個數據庫。
3、App.java啟動類
package com.zhuguang; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.LoggerFactory; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import com.zhuguang.datasource.DBConfig1; import com.zhuguang.datasource.DBConfig2; /** * 非常感謝騰訊課堂燭光學院的lisa老師 * @author zhuwen * */ @SpringBootApplication @EnableConfigurationProperties(value = { DBConfig1.class, DBConfig2.class }) @MapperScan(basePackages = { "com.zhuguang.mapper" }) public class App { private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(App.class); public static void main(String[] args) { SpringApplication.run(App.class, args); } }
4、兩個Oracle數據庫配置類
package com.zhuguang.datasource; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.datasource.test1") public class DBConfig1 { private String url; private String username; private String password; private int minPoolSize; private int maxPoolSize; private int maxLifetime; private int borrowConnectionTimeout; private int loginTimeout; private int maintenanceInterval; private int maxIdleTime; private String testQuery; getter and setter... }
第二個數據庫配置類與DBConfig1一樣,唯一不同的地方在於 prefix = "spring.datasource.test2",類名叫DBConfig2
5、atomikos分布式數據源配置類
TestMyBatisConfig1
package com.zhuguang.datasource; import java.sql.SQLException; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import com.alibaba.druid.pool.xa.DruidXADataSource; @Configuration // basePackages 最好分開配置 如果放在同一個文件夾可能會報錯 @MapperScan(basePackages = "com.zhuguang.db1", sqlSessionTemplateRef = "testSqlSessionTemplate") public class TestMyBatisConfig1 { private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(TestMyBatisConfig1.class); // 配置數據源 @Primary @Bean(name = "dataSource1") public DataSource testDataSource(DBConfig1 testConfig) throws SQLException { //Atomikos統一管理分布式事務 AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); // Properties p = new Properties(); // p.setProperty ( "user" , testConfig.getUsername() ); // p.setProperty ( "password" , testConfig.getPassword() ); // p.setProperty ( "URL" , testConfig.getUrl() ); // xaDataSource.setXaProperties ( p ); //用druidXADataSource方式或者上面的Properties方式都可以 DruidXADataSource druidXADataSource = new DruidXADataSource(); druidXADataSource.setUrl(testConfig.getUrl()); druidXADataSource.setUsername(testConfig.getUsername()); druidXADataSource.setPassword(testConfig.getPassword()); xaDataSource.setUniqueResourceName("oracle1"); xaDataSource.setXaDataSource(druidXADataSource); xaDataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource"); xaDataSource.setMaxLifetime(testConfig.getMaxLifetime()); xaDataSource.setMinPoolSize(testConfig.getMinPoolSize()); xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize()); xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout()); xaDataSource.setLoginTimeout(testConfig.getLoginTimeout()); xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval()); xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime()); xaDataSource.setTestQuery(testConfig.getTestQuery()); LOG.info("分布式事物dataSource1實例化成功"); return xaDataSource; } @Primary @Bean(name = "testSqlSessionFactory") public SqlSessionFactory testSqlSessionFactory(@Qualifier("dataSource1") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } @Primary @Bean(name = "testSqlSessionTemplate") public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
再建一個一模一樣的TestMyBatisConfig1類,取名叫TestMyBatisConfig2,不同的地方是注解和bean命名:
package com.zhuguang.datasource; import java.sql.SQLException; import java.util.Properties; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.alibaba.druid.pool.xa.DruidXADataSource; import com.atomikos.jdbc.AtomikosDataSourceBean; import oracle.jdbc.xa.client.OracleXADataSource; // basePackages 最好分開配置 如果放在同一個文件夾可能會報錯 @Configuration @MapperScan(basePackages = "com.zhuguang.db2", sqlSessionTemplateRef = "test2SqlSessionTemplate") public class TestMyBatisConfig2 { private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(TestMyBatisConfig2.class); // 配置數據源 @Bean(name = "dataSource2") public DataSource testDataSource(DBConfig2 testConfig) throws SQLException { AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); // Properties p = new Properties(); // p.setProperty ( "user" , testConfig.getUsername() ); // p.setProperty ( "password" , testConfig.getPassword() ); // p.setProperty ( "URL" , testConfig.getUrl() ); // xaDataSource.setXaProperties ( p ); //用druidXADataSource方式或者上面的Properties方式都可以 DruidXADataSource druidXADataSource = new DruidXADataSource(); druidXADataSource.setUrl(testConfig.getUrl()); druidXADataSource.setUsername(testConfig.getUsername()); druidXADataSource.setPassword(testConfig.getPassword()); xaDataSource.setUniqueResourceName("oracle2"); xaDataSource.setXaDataSource(druidXADataSource); xaDataSource.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource"); xaDataSource.setMaxLifetime(testConfig.getMaxLifetime()); xaDataSource.setMinPoolSize(testConfig.getMinPoolSize()); xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize()); xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout()); xaDataSource.setLoginTimeout(testConfig.getLoginTimeout()); xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval()); xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime()); xaDataSource.setTestQuery(testConfig.getTestQuery()); LOG.info("分布式事物dataSource2實例化成功"); return xaDataSource; } @Bean(name = "test2SqlSessionFactory") public SqlSessionFactory testSqlSessionFactory(@Qualifier("dataSource2") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } @Bean(name = "test2SqlSessionTemplate") public SqlSessionTemplate testSqlSessionTemplate( @Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
6、為兩個數據庫分別建立USERS表
首先在兩個Oracle數據庫里都建立表:
CREATE TABLE users (
name varchar2(20 BYTE), age NUMBER(*,0) );
並建立entity:
package com.zhuguang.entity; public class Users { private String id; private String name; private Integer age; getter and setter.. }
7、為users建立mapper類
package com.zhuguang.db1.dao; import java.util.List; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import com.zhuguang.entity.Users; public interface DB1_UserMapper { @Select("SELECT * FROM USERS WHERE NAME = #{name}") Users findByName(@Param("name") String name); @Insert("INSERT INTO USERS(NAME, AGE) VALUES(#{name}, #{age})") int insert(@Param("name") String name, @Param("age") Integer age); @Delete("Delete from USERS") void deleteAll(); @Select("select 'oracle1' as id,t.* from USERS t") List<Users> queryAll(); }
package com.zhuguang.db2.dao; import java.util.List; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import com.zhuguang.entity.Users; //test1 DB public interface DB2_UserMapper { @Select("SELECT * FROM USERS WHERE NAME = #{name}") Users findByName(@Param("name") String name); @Insert("INSERT INTO USERS(NAME, AGE) VALUES(#{name}, #{age})") int insert(@Param("name") String name, @Param("age") Integer age); /** * 用於演示插入數據庫異常的情況 */ @Insert("INSERT INTO not_exists_table_USERS(NAME, AGE) VALUES(#{name}, #{age})") int insertNotExistsTable(@Param("name") String name, @Param("age") Integer age); @Delete("Delete from USERS") void deleteAll(); @Select("select 'oracle2' as id,t.* from USERS t") List<Users> queryAll(); }
8、建立Controller
package com.zhuguang.controller; import java.util.Date; import java.util.List; import javax.annotation.Resource; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.zhuguang.db1.service.DB1_UserService; import com.zhuguang.db2.service.DB2_UserService; import com.zhuguang.service.IndexService; /** * 感謝騰訊課堂燭光學院lisa老師 * @author zhuwen * */ @RestController public class IndexController { private static Logger log = LoggerFactory.getLogger(IndexController.class); @Autowired private DB1_UserService userService1; @Autowired private DB2_UserService userService2; @Autowired private IndexService indexService; //想查看數據源,可以這么注解 @Resource @Qualifier("dataSource1") private DataSource dataSource1; @RequestMapping("/insertDB1") public String insertTest001(String name, Integer age) { // userMapperTest01.insert(name, age); userService1.insertDB1(name, age); return "success insertDB1"; } @RequestMapping("/insertDB2") public String insertTest002(String name, Integer age) { userService2.insertDB2(name, age); return "success insertDB2"; } /** * atomikos效果:分布式事物。兩個數據庫都插入值 * * @param name * @param age * @return */ @RequestMapping("/insertTwoDBs") public String insertTwoDBs(String name, Integer age) { indexService.insertTwoDBs(name, age); return "success insertTwoDBs"; } /** * atomikos效果:分布式事物。 演示發生異常分布式事物回滾 * * @param name * @param age * @return */ @RequestMapping("/insertTwoDBsWithError") public String insertTwoDBsWithError(String name, Integer age) { indexService.insertTwoDBsWithError(name, age); return "success insertTwoDBs"; } /** * atomikos效果:分布式事物。 演示發生異常分布式事物回滾 * 直接調用mapper方式 * @param name * @param age * @return */ @RequestMapping("/insertTwoDBsUseMapperWithError") public String insertTwoDBsUseMapperWithError(String name, Integer age) { indexService.insertTwoDBsUseMapperWithError(name, age); return "success insertTwoDBsUseMapperWithError"; } /** * 獲取兩個數據庫的所有數據 * @return */ @RequestMapping("/queryAll") public List queryAll() { List list = indexService.queryAll(); list.add(new Date().toLocaleString()); //加上時間戳,方便postman觀察結果 return list; } /** * 刪除兩個數據庫的所有數據 * @return */ @RequestMapping("/deleteAll") public String deleteAll() { indexService.deleteAll(); return "success delete all"; } }
9、建立service
package com.zhuguang.service; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import com.zhuguang.db1.dao.DB1_UserMapper; import com.zhuguang.db1.service.DB1_UserService; import com.zhuguang.db2.dao.DB2_UserMapper; import com.zhuguang.db2.service.DB2_UserService; import com.zhuguang.entity.Users; @Service public class IndexService { @Autowired private DB1_UserMapper db1UserMapper; @Autowired private DB2_UserMapper db2UserMapper; @Autowired private DB1_UserService db1UserService; @Autowired private DB2_UserService db2UserService; /** * atomikos效果:分布式事物。兩個數據庫都插入值 * @return */ @Transactional public void insertTwoDBs(String name, Integer age) { db1UserMapper.insert(name, age); db2UserMapper.insert(name, age); } @Transactional public void deleteAll() { db1UserMapper.deleteAll(); //不同數據庫。test1,test2 //userService2.insertDB2(name, age); db2UserMapper.deleteAll();//test2 // int i = 1 / 0;// } /** * atomikos效果:分布式事物。 * 演示發生異常分布式事物回滾 * 這里無論error 1、2、3,任何一處發生異常,分布式事物都會回滾 */ @Transactional //(rollbackFor = { Exception.class }) public void insertTwoDBsWithError(String name, Integer age) { db1UserService.insert2DB1(name, age); db2UserService.insert2DB2(name, age); //int i = 1 / 0; // error 1 } /** * atomikos效果:分布式事物。 * 演示發生異常分布式事物回滾 * 這里無論error 1、2、3,任何一處發生異常,分布式事物都會回滾 * 此方法效果等同於insertTwoDBsWithError */ @Transactional public void insertTwoDBsUseMapperWithError(String name, Integer age) { db1UserMapper.insert(name, age); db2UserMapper.insert(name, age); db2UserMapper.insertNotExistsTable(name, age); } public List queryAll() { List all = new ArrayList(); List<Users> list1 = db1UserService.queryAll(); if(CollectionUtils.isEmpty(list1)) { all.add("db1 沒有任何數據!"); }else { all.addAll(list1); } List<Users> list2 = db2UserService.queryAll(); if(CollectionUtils.isEmpty(list2)) { all.add("db2 沒有任何數據!"); }else { all.addAll(list2); } return all; } }
10、使用postman驗證
主要驗證:/insertTwoDBsUseMapperWithError 這個效果,是否任何一處DB產生錯誤,都會使分布式事物回滾。
11、druid監控頁面
啟動App.java之后,只要在瀏覽器里輸入 http://localhost:8082/druid/index.html 就可以進入druid監控頁面:
但是由於這里使用的是atomikos分布式事物DataSource,不是原本的druidDataSource,所以這里幾乎監控不到任何有價值的東西,基本作廢。
end.