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