spring-boot入門(六)多資料來源
spring-boot入門(六)多資料來源
我們現在可以通過自定義的資料來源,用spring boot迅速的搭建起一個訪問資料庫的應用,有時候一個系統往往會和多個數據庫進行互動。當然可以通過遠端服務呼叫方式訪問多個數據庫,每個服務負責不同的資料庫訪問,但是多資料來源的方式可能會更加的快捷和高效,這依賴於系統的架構設計。
1. 多資料來源的配置
與單資料來源配置大致相同,需要引入各種spring boot和jdbc驅動,以及資料庫連線池等等。以spring-boot入門(五)自定義資料來源:druid為基礎在此之上繼續新增其它資料來源。
1.1 配置application.yml檔案
在application.yml檔案中配置資料庫的連線資訊
boc:
datasource:
url: jdbc:mysql://localhost:3306/db1?useSSL=false&requireSSL=false
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
ccb:
datasource:
url: jdbc:mysql://localhost:3306/db2?useSSL=false&requireSSL=false
driver-class -name: com.mysql.jdbc.Driver
username: root
password: root
druid:
filters: stat, wall
maxActive: 20
initialSize: 1
maxWait: 60000
minIdle: 10
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
removeAbandoned: true
removeAbandonedTimeout: 1800
logAbandoned: false
上面配置了兩個資料來源,boc和ccb分別是db1和db2,連線池使用的是阿里的druid。
1.2 初始化配置
初始化配置,首先定義一個DruidDataSourceProperties此類用於封裝druid的配置屬性。可以通過@ConfigurationProperties註解方便快速的注入上面定義的druid連線池。
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author JasonLin
* @version V1.0
* @date 2017/12/4
*/
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "druid")
public class DruidDataSourceProperties {
private String filters;
private int maxActive;
private int initialSize;
private int maxWait;
private int minIdle;
private long timeBetweenEvictionRunsMillis;
private long minEvictableIdleTimeMillis;
private String validationQuery;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;
private int maxOpenPreparedStatements;
private boolean removeAbandoned;
private int removeAbandonedTimeout;
private boolean logAbandoned;
}
@Configuration和@ConfigurationProperties(prefix = “druid”),該bean通過字首為druid的property屬性自動初始化並注入該bean。
接下來開始注入資料來源:
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* @author JasonLin
* @version V1.0
* @date 2017/12/1
*/
@Configuration
public class CustomConfiguration {
@Autowired
private DruidDataSourceProperties druidDataSourceProperties;
@Bean
@Primary
@ConfigurationProperties(prefix = "boc.datasource")
public DataSourceProperties bocDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties(prefix = "ccb.datasource")
public DataSourceProperties ccbDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties(prefix = "boc.datasource")
public DruidDataSource bocDataSource(@Qualifier("bocDataSourceProperties") DataSourceProperties dataSourceProperties) throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
InitDruidDataSource(druidDataSource, dataSourceProperties);
return druidDataSource;
}
@Bean
@ConfigurationProperties(prefix = "ccb.datasource")
public DruidDataSource ccbDataSource(@Qualifier("ccbDataSourceProperties") DataSourceProperties dataSourceProperties) throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
InitDruidDataSource(druidDataSource, dataSourceProperties);
return druidDataSource;
}
@Bean
public JdbcTemplate bocJdbcTemplate(@Qualifier("bocDataSource") DataSource bocDataSource) {
return new JdbcTemplate(bocDataSource);
}
@Bean
public JdbcTemplate ccbJdbcTemplate(@Qualifier("ccbDataSource") DataSource ccbDataSource) {
return new JdbcTemplate(ccbDataSource);
}
private void InitDruidDataSource(DruidDataSource druidDataSource, DataSourceProperties properties) throws SQLException {
druidDataSource.setUrl(properties.getUrl());
druidDataSource.setUsername(properties.getUsername());
druidDataSource.setPassword(properties.getPassword());
druidDataSource.setDriverClassName(properties.getDriverClassName());
//屬性型別是字串,通過別名的方式配置擴充套件外掛,常用的外掛有:
//監控統計用的filter:stat
//日誌用的filter:log4j
//防禦sql注入的filter:wall
druidDataSource.setFilters(druidDataSourceProperties.getFilters());
//最大連線池數量
druidDataSource.setMaxActive(druidDataSourceProperties.getMaxActive());
// 初始化時建立物理連線的個數。初始化發生在顯示呼叫init方法,或者第一次getConnection時
druidDataSource.setInitialSize(druidDataSourceProperties.getInitialSize());
//獲取連線時最大等待時間,單位毫秒。配置了maxWait之後,預設啟用公平鎖,併發效率會有所下降,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖。
druidDataSource.setMaxWait(druidDataSourceProperties.getMaxWait());
//最小連線池數量
druidDataSource.setMinIdle(druidDataSourceProperties.getMinIdle());
//有兩個含義:
//1) Destroy執行緒會檢測連線的間隔時間,如果連線空閒時間大於等於minEvictableIdleTimeMillis則關閉物理連線。
//2) testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明
druidDataSource.setTimeBetweenEvictionRunsMillis(druidDataSourceProperties.getTimeBetweenEvictionRunsMillis());
//連線保持空閒而不被驅逐的最小時間
druidDataSource.setMinEvictableIdleTimeMillis(druidDataSourceProperties.getMinEvictableIdleTimeMillis());
druidDataSource.setValidationQuery(druidDataSourceProperties.getValidationQuery());
//建議配置為true,不影響效能,並且保證安全性。申請連線的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,
//(空閒時測試)執行validationQuery檢測連線是否有效。
druidDataSource.setTestWhileIdle(druidDataSourceProperties.isTestWhileIdle());
//申請連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能。
druidDataSource.setTestOnBorrow(druidDataSourceProperties.isTestOnBorrow());
//歸還連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能。
druidDataSource.setTestOnReturn(druidDataSourceProperties.isTestOnReturn());
//移除洩露的連結
druidDataSource.setRemoveAbandoned(druidDataSourceProperties.isRemoveAbandoned());
//洩露連線的定義時間(要超過最大事務的處理時間)
druidDataSource.setRemoveAbandonedTimeout(druidDataSourceProperties.getRemoveAbandonedTimeout());
//移除洩露連線發生是是否記錄日誌
druidDataSource.setLogAbandoned(druidDataSourceProperties.isLogAbandoned());
}
}
DataSourceProperties是資料來源屬性的封裝bean,類似DruidDataSourceProperties。為每一個數據源配置一個DruidDataSource,在多個數據源的情況下,必須通過@Primary指定一個主資料來源。這裡將boc作為主資料來源
@Bean
@Primary
@ConfigurationProperties(prefix = "boc.datasource")
public DruidDataSource bocDataSource(@Qualifier("bocDataSourceProperties") DataSourceProperties dataSourceProperties) throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
InitDruidDataSource(druidDataSource, dataSourceProperties);
return druidDataSource;
}
@Qualifier(“bocDataSourceProperties”) ,區分同一型別不同命名的bean。
定義完資料來源後,可通過以下方式使用不同的資料來源
到這裡我們就可以通過JdbcTemplate來使用不同的資料來源了。
2. 測試資料來源
這裡通過在兩個資料庫分別建立1張表,通過不同的服務來寫入和查詢。該部分的程式碼結構如下:
AccountBOC、AccountCCB是兩個實體類AccountBocService(Impl)、AccountCcbService(Impl)是服務類及實現,DAO層簡化刪除。下面是相關的一些程式碼:
兩個實體類,除了在不同的資料庫其餘都相同
@Setter
@Getter
@Entity
@Table(name = "account_boc")
public class AccountBOC implements Serializable {
private static final long serialVersionUID = 1820150874495571704L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private Long customer;
@Column
private BigDecimal balance;
}
@Setter
@Getter
@Entity
@Table(name = "account_ccb")
public class AccountCCB implements Serializable{
private static final long serialVersionUID = 1476688047241463855L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private Long customer;
@Column
private BigDecimal balance;
}
服務實現:
@Service("accountBocService")
@Transactional(rollbackFor = Exception.class)
public class AccountBocServiceImpl implements AccountBocService {
private static final String UPDATE_BALANCE = "UPDATE account_boc SET balance = balance+? WHERE customer = ?;";
private final Log log = LogFactory.getLog(getClass());
@Autowired
private JdbcTemplate bocJdbcTemplate;
@Override
public void updateBalance(Long customer, BigDecimal amount) {
bocJdbcTemplate.update(UPDATE_BALANCE, amount, customer);
if(customer.equals(10002L)){
throw new RuntimeException("test rollbace on runtimeException.");
}
log.info(String.format("update balance ,[customer:%s,amount%s]", customer, amount));
}
}
@Service("accountCcbService")
@Transactional(rollbackFor = Exception.class)
public class AccountCcbServiceImpl implements AccountCcbService {
private static final String UPDATE_BALANCE = "UPDATE account_ccb SET balance = balance+? WHERE customer = ?;";
private final Log log = LogFactory.getLog(getClass());
@Autowired
private JdbcTemplate ccbJdbcTemplate;
@Autowired
private AccountBocService accountBocService;
@Override
public void transferAccountToBoc(Long customer, BigDecimal amount) {
updateBalance(customer, amount.multiply(new BigDecimal("-1")));
accountBocService.updateBalance(customer, amount);
log.info(String.format("transfer balance to boc ,[customer:%s,amount:%s]", customer, amount));
}
@Override
public void updateBalance(Long customer, BigDecimal amount) {
ccbJdbcTemplate.update(UPDATE_BALANCE, amount, customer);
log.info(String.format("update balance ,[customer:%s,amount:%s]", customer, amount));
}
}
在AccountCcbServiceImpl 中我們呼叫了AccountBocService的方法,模擬賬戶的資金進行轉移。
@RunWith(SpringRunner.class)
@SpringBootTest
public class AccountServiceTest {
@Autowired
private AccountCcbService accountCcbService;
@Autowired
private AccountBocService accountBocService;
@Test
public void testTransferAccountCcbToBoc() {
accountCcbService.transferAccountToBoc(10001L, new BigDecimal(-100));
}
@Test
public void testTransferAccountCcbToBocOnException() {
accountCcbService.transferAccountToBoc(10002L, new BigDecimal(100));
}
@Test
public void testUpdateCcbBalance() {
accountCcbService.updateBalance(10001L, new BigDecimal(100));
}
@Test
public void testUpdateBocBalance() {
accountBocService.updateBalance(10001L, new BigDecimal(100));
}
}
測試類,對服務進行測試。執行testTransferAccountCcbToBoc(),testUpdateCcbBalance() 及testUpdateBocBalance()我們發現數據都能正確的更新到不同的資料庫。但是當我們執行testTransferAccountCcbToBocOnException() 時發現ccb庫的資料更新了,但是boc庫的資料更新失敗了。注意AccountBocServiceImpl 的更新方法:
當customer為10002的時候我們丟擲了一個異常來模擬boc資料庫寫入失敗的情況。我們在一個方法(事務)裡面操作了不同的資料庫,但是某一個數據庫寫入失敗,我們希望的是整體回滾而不是部分回滾,這是多資料來源情況下的分散式事務問題。
由此可見我們雖然已經能夠操作不同的資料來源,但是在一個方法(事務)裡面同時操作多個數據源的情況下,可能會存在分散式事務問題。在下一章節我們將使用atomikos來解決分散式事務問題,保證同一個事務下的多資料來源操作能同時成功或者回滾。
PS:
1、@ConfigurationProperties(prefix = “druid”) 會將application.yml裡面的配置自動注入到類中
2、當使用多資料來源時必須通過@Primary指定主要的資料來源,使用@ConfigurationProperties與此類似
3、多資料來源如果使用預設的datasourceManager會產生跨庫事物問題
4、@Bean bean名稱預設為方法名稱
END