1. 程式人生 > >spring-boot入門(六)多資料來源

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來解決分散式事務問題,保證同一個事務下的多資料來源操作能同時成功或者回滾。

本章節完整程式碼在:https://github.com/Json-Lin/spring-boot-practice/tree/master/spring-boot-practice-multiple-datasource

PS:
1、@ConfigurationProperties(prefix = “druid”) 會將application.yml裡面的配置自動注入到類中
2、當使用多資料來源時必須通過@Primary指定主要的資料來源,使用@ConfigurationProperties與此類似
3、多資料來源如果使用預設的datasourceManager會產生跨庫事物問題
4、@Bean bean名稱預設為方法名稱

END