在实际开发中,经常可能遇到在一个应用中可能需要访问多个数据库的情况。以下是两种典型场景
业务复杂(数据量大)
数据分布在不同的数据库中,数据库拆了,应用没拆。一个公司多个子项目,各用各的数据库,设计数据共享…
读写分离
为解决 数据库的读性能瓶颈(读比写性能更高,写锁会影响读阻塞,从而影响读的性能)
很多数据库拥有主从架构,也就是说,一台主数据库服务器,是对外提供增删改业务的生产服务;另一(多)台从数据库服务器,主要进行读的操作
可以通过中间件(ShardingSphere、mycat、mysql-proxy、TDDL…),但是有一些规模较小的公司,没有专门的中间件团队搭建读写分离基础设施,因此需要业务开发人员自行实现读写分离
这里的架构与上图类似,不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert、update、delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力
一、实现多数据源
对于大多数 Java 应用,都使用了 Spring 框架,spring-jdbc 模块提供了 AbstractRoutingDataSource,其内部可以包含多个 DataSource,然后在运行时来动态的访问哪个数据库。这种方式访问数据库的架构如下图所示
测试样例一:普通的动态数据源
- 创建两个数据库
read
和write
CREATE DATABASE `read` /*!40100 DEFAULT CHARACTER SET utf8 */
CREATE TABLE `people` (
`name` VARCHAR(50) DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE DATABASE `write` /*!40100 DEFAULT CHARACTER SET utf8 */
CREATE TABLE `people` (
`name` VARCHAR(50) DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;
-
创建一个SpringBoot 项目 dynamic_datasource
-
导入依赖
- pom.xml
<dependencies>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
- 应用配置文件
- application.yaml
spring:
application:
name: dynamic_datasource
# 数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
# 读数据源
read:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/read?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# 写数据源
write:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/write?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# 端口号
server:
port: 3355
# 别名、xml文件配置
mybatis:
mapper-locations: classpath:com/vinjcent/mapper/**/*.xml
type-aliases-package: com.vinjcent.pojo
- 实体类
- People
package com.vinjcent.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class People {
private String name;
}
-
数据源配置类
- DataSourceConfiguration
package com.vinjcent.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class DataSourceConfiguration { @Bean(name = "readDatasource") @ConfigurationProperties(prefix = "spring.datasource.read") public DataSource readDatasource() { // 底层会自动拿到spring.datasource中的配置,创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } @Bean(name = "writeDatasource") @ConfigurationProperties(prefix = "spring.datasource.write") public DataSource writeDatasource() { // 底层会自动拿到spring.datasource中的配置,创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } }
-
动态数据源
- DynamicDataSource
package com.vinjcent.config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
@Component
@Primary // 将该Bean设置为主要注入Bean
public class DynamicDataSource implements DataSource, InitializingBean {
// 用于存储数据源的标识
public static ThreadLocal<String> name = new ThreadLocal<>();
// 写
private final DataSource writeDataSource;
// 读
private final DataSource readDataSource;
@Autowired
public DynamicDataSource(@Qualifier("readDatasource") DataSource readDataSource,
@Qualifier("writeDatasource") DataSource writeDataSource) {
this.readDataSource = readDataSource;
this.writeDataSource = writeDataSource;
}
@Override
public Connection getConnection() throws SQLException {
return name.get().equals("w") ? writeDataSource.getConnection() : readDataSource.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
/**
* 初始化bean的initialization接口
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
// TODO 初始化
name.set("w");
}
}
- 测试接口
- PeopleController
package com.vinjcent.controller;
import com.vinjcent.config.DynamicDataSource;
import com.vinjcent.pojo.People;
import com.vinjcent.service.PeopleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("people")
public class PeopleController {
private final PeopleService peopleService;
@Autowired
public PeopleController(PeopleService peopleService) {
this.peopleService = peopleService;
}
@GetMapping("/list")
public List<People> getAllPeople() {
// 修改对应数据源
DynamicDataSource.name.set("r");
return peopleService.list();
}
@GetMapping("/insert")
public String addPeople() {
// 修改对应数据源
DynamicDataSource.name.set("w");
peopleService.save(new People("vinjcent"));
return "添加成功";
}
}
- 主启动类(需要排除掉SpringBoot数据源自动配置类,否则会报错)
package com.vinjcent;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@MapperScan("com.vinjcent.mapper")
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 排除SpringBoot数据源自动配置类
public class DynamicDatasourceApplication {
public static void main(String[] args) {
SpringApplication.run(DynamicDatasourceApplication.class, args);
}
}
- 测试访问接口,查看是否读写分离
测试实例二:实现 AbstractRoutingDataSource
应用直接操作的是 AbstractRoutingDataSource 的实现类,告诉 AbstractRoutingDataSource 访问哪个数据库,然后由 AbstractRoutingDataSource 从事先配好的数据源(readDataSource、writeDataSource)选择一个,来访问对应的数据库
流程
- 当执行数据库持久化操作,只要继承了 Spring 就一定会通过 DataSourceUtils 获取 Connection
- 通过 Spring 注入的 DataSource 获取 Connection 即可执行数据库操作
- 所以思路就是:只需配置一个 DataSource 的 bean,然后根据业务动态提供 Connection 即可
- 其实 Spring 已经提供了一个 DataSource 实现类用于动态切换数据源—AbstractRoutingDataSource
- 分析 AbstractRoutingDataSource 即可实现动态数据源切换
AbstractRoutingDataSource 分析
// targetDataSources 保存了key和数据库连接的映射关系
private Map<Object, Object> targetDataSources;
// 标识默认的连接
private Object defaultTargetDataSource;
// 这个数据结构是通过 targetDataSources 构建而来,存储结构也是数据库标识和数据源的映射关系
private Map<Object, DataSource> resolvedDataSources;
AbstractRoutingDataSource 实现了 InitializingBean 接口,并实现了 afterPropertiesSet() 方法。afterPropertiesSet() 方法是初始化bean的时候执行,通常用作数据初始化。resolvedDataSources 就是在这里赋值
public void afterPropertiesSet() {
if (this.targetDataSources == null) {
throw new IllegalArgumentException("Property 'targetDataSources' is required");
} else {
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
}
}
- 因此,我们只需要创建 AbstractRoutingDataSource 实现类 DynamicDataSource ,然后初始化 targetDataSources 和
key
为数据源的标识(可以是枚举、字符串都行,因为标识是Object类型) - 后续当调用 AbstractRoutingDataSource.getConnection 会接着调用提供的模板方法:determineTargetDataSource
- 通过 determineDataSource 该方法返回的数据库标识,从 resolvedDataSources 中拿到对应的数据源(类似于一个策略模式)
- 所以最终,只需要 DynamicDataSource 中实现 determineTargetDataSource 为其提供一个数据库标识
- 修改 DynamicDataSource 动态数据源类
package com.vinjcent.config;
import com.vinjcent.constants.DataSourceConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Component
@Primary // 将该Bean设置为主要注入Bean
public class DynamicDataSource extends AbstractRoutingDataSource {
// 用于存储数据源的标识
public static ThreadLocal<String> name = new ThreadLocal<>();
// 写
private final DataSource writeDataSource;
// 读
private final DataSource readDataSource;
@Autowired
public DynamicDataSource(@Qualifier("readDatasource") DataSource readDataSource,
@Qualifier("writeDatasource") DataSource writeDataSource) {
this.readDataSource = readDataSource;
this.writeDataSource = writeDataSource;
}
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
// 初始化完bean之后调用该方法
@Override
public void afterPropertiesSet() {
// 为targetDataSources 初始化所有数据源
Map<Object, Object> sources = new HashMap<>();
sources.put(DataSourceConstants.READ_DATASOURCE, readDataSource);
sources.put(DataSourceConstants.WRITE_DATASOURCE, writeDataSource);
super.setTargetDataSources(sources);
// 为 defaultTargetDataSource 设置默认的数据源
super.setDefaultTargetDataSource(readDataSource);
// resolvedDataSources 负责最终切换的数据源map
super.afterPropertiesSet();
}
}
- 添加数据源常量类
- DataSourceConstants
package com.vinjcent.constants;
public class DataSourceConstants {
// 读数据源
public static final String READ_DATASOURCE = "read";
// 写数据源
public static final String WRITE_DATASOURCE = "write";
}
- 修改 PeopleController 类
package com.vinjcent.controller;
import com.vinjcent.config.DynamicDataSource;
import com.vinjcent.constants.DataSourceConstants;
import com.vinjcent.pojo.People;
import com.vinjcent.service.PeopleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("people")
public class PeopleController {
private final PeopleService peopleService;
@Autowired
public PeopleController(PeopleService peopleService) {
this.peopleService = peopleService;
}
@GetMapping("/list")
public List<People> getAllPeople() {
// 修改对应数据源
DynamicDataSource.name.set(DataSourceConstants.READ_DATASOURCE);
return peopleService.list();
}
@GetMapping("/insert")
public String addPeople() {
// 修改对应数据源
DynamicDataSource.name.set(DataSourceConstants.WRITE_DATASOURCE);
peopleService.save(new People("vinjcent"));
return "添加成功";
}
}
- 测试访问接口,查看是否读写分离