spring-boot入门(六)多数据源

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq447995687/article/details/79796421

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

猜你喜欢

转载自blog.csdn.net/qq447995687/article/details/79796421
今日推荐