提示:多数据源实现方式、多数据源的使用场景。AbstractRoutingDataSource、DynamicDataSource框架、mybatisplus的Intercepter插件、java中多数据源的几种实现方式、mybatisPlus的插件实现多数据源
文章目录
- 前言
- 一、多数据源的几种实现方式
- 二、使用场景
- 三、核心原理
- 1、原理
- 2、实现步骤
- 四、代码实现
- 1.基础实现
- 1.1、pom依赖
- 1.2、配置文件
- 1.3、配置类1: DataSourceConfig
- 1.4、配置类2: DynamicDataSource
- 1.5、controller
- 1.6、mapper
- 2.代码优化
- 2.1、注解wr
- 2.2、 @WR("W")
- 2.3、aop
- 3、mybatisPlus的插件实现多数据源
- 3.1、MyMybatisInterceptor
- 3.2、DataSourceConfig
- 3.3、DynamicDataSource
- 总结
前言
最近工作中有一张表,实际数据量超过1亿了,导致一条普通的insert语句也耗时15秒,因此需要分表。在使用shardingSphere分表时,需要切换多数据源,因此特意调研了一下多数据源的几种实现方式。再次记录一下,感兴趣的同学可以下载代码,这样看起来更加清晰。gitee代码
一、多数据源的几种实现方式
java中实现多数据源,比较常见的方式有3种:
- abstractRootingDataSource
- mybatisplus的Intercepter插件
- DynamicDataSource 框架
其实最底层的核心原理,就是abstractRootingDataSource,剩下的两种,肯定也是以第一种为基础的,只不过封装了一层而已。
二、使用场景
一般来说,多数据源有以下两种使用场景:
- 业务复杂(数据量大)。数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目,各用各的数据库,涉及数据共享…
- 读写分离。为了解决数据库的读性能瓶颈(读比写性能更高, 写锁会影响读阻塞,从而影响读的性能)。
三、核心原理
1、原理
最核心的类就是AbstractRootingDataSource,因此我们着重介绍一下。
这个抽象类中,有3个比较重要的成员变量:
- 1、此时,我们仍然返回的是dynamicDatasource,只是,我们继承了AbstruceRootingDataSource,然后getConnection方法变成了由AbstruceRootingDataSource提供的connection了
- 这个getConnection方法内部,是: determineTargetDataSource().getConnection();
- 而2中底层是调用的模版方法,去获取最终的connection。因为是map中的get方法获取的,所以get的这个key是关键,
lookupKey =
determineCurrentLookupKey();resolvedDataSource.get(lookupKey); - 而这个key呢,就需要程序员自己在这个接口中去实现 determineCurrentLookupKey
方法了。(返回的是一个key值,我们自定义的key)
2、实现步骤
实现多数据源大概需要3部,(AbstractRoutingDataSource)
1.继承 abstractRootingDataSource
2.返回当前数据源标识 重写 determineCurrentLookupKey 方法
3.获取全部的数据源map super.setTargetDataSources(targetDataSources);
四、代码实现
1.基础实现
1.1、pom依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.6.2</version>
</dependency> <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.6.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!--Druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.6.2</version>
</dependency>
</dependencies>
1.2、配置文件
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
datasource1:
url: jdbc:mysql://localhost:3306/mytest?serverTimezone=UTC&useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: 123456
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
datasource2:
url: jdbc:mysql://localhost:3306/mytest2?serverTimezone=UTC&useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: 123456
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
server:
port: 9001
mybatis:
mapper-locations: classpath:mapper/**/*.xml
1.3、配置类1: DataSourceConfig
package zheng.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;
/**
* @author: ztl
* @date: 2024/02/06 22:59
* @desc:
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
}
1.4、配置类2: DynamicDataSource
相关解释:
- getConnection 是核心的,返回哪个数据库的链接的。
- afterPropertiesSet 是初始化的操作
- 实现了datasource接口这个肯定好理解,我们要返回一个动态数据源,也是个数据源嘛
- 实现了InitializingBean,是因为我们想要初始化set一些值,用到了afterPropertiesSet方法。当然,你也可以在构造方法中初始化操作,但是构造方法如果有多个的话,你难道要在每一个构造方法中都执行一个这个初始化的动作嘛?如果有10个构造,写10遍嘛?那100个构造呢,所以,spring为了避免这个局面,就用了afterPropertiesSet方法。
package zheng.config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* @author: ztl
* @date: 2024/02/08 22:29
* @desc:
*/
@Component
@Primary // 将动态数据源作为核心返回。
// (datasource1、datasource2、DynamicDataSource都会被spring扫描出来,如果只返回datasource1、2就没法动态切换了)
public class DynamicDataSource implements DataSource, InitializingBean {
// 当前使用的数据源
public static ThreadLocal<String > name = new ThreadLocal<>();
// 写
@Autowired
DataSource dataSource1;
// 读
@Autowired
DataSource dataSource2;
@Override
public Connection getConnection() throws SQLException {
if (name.get().equals("W")){
return dataSource1.getConnection();
}else {
return dataSource2.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;
}
@Override
public void afterPropertiesSet() throws Exception {
// todo: 这个是初始化的操作:(默认数据库是W库)
name.set("W");
}
}
1.5、controller
package zheng.controller;
import lombok.extern.slf4j.Slf4j;
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 zheng.config.DynamicDataSource;
import zheng.entity.Frend;
import zheng.service.FrendService;
import java.util.List;
/**
* @author: ztl
* @date: 2024/02/06 23:06
* @desc:
*/
@RestController
@RequestMapping("frend")
@Slf4j
public class FrendController {
@Autowired
private FrendService frendService;
@GetMapping(value = "select")
public List<Frend> select(){
log.info("select start ...");
// 读的操作,我们用读库
DynamicDataSource.name.set("R");
return frendService.list();
}
@GetMapping(value = "insert")
public void in(){
log.info("in start ...");
// 写的操作,我们用写库
DynamicDataSource.name.set("W");
Frend frend = new Frend();
frend.setName("ztl");
frendService.save(frend);
}
}
1.6、mapper
package zheng.mapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import zheng.entity.Frend;
import java.util.List;
public interface FrendMapper {
@Select("SELECT * FROM Frend")
List<Frend> list();
@Insert("INSERT INTO frend(`name`) VALUES (#{name})")
void save(Frend frend);
}
2.代码优化
在上面的代码上,加一个注解,然后直接在注解上指定具体的数据源,比如说1就是a数据源,2就是b数据源。
针对于在controller层写数据源源的,代码侵入量大,不方便。
我们有两种解决方案:
1、aop。 更加适用于 大数据量,业务复杂的场景。(有多个不同的库,不同业务导致的)
2、mybatis插件。 更加适用于,读写分离的操作。 因为mybatis的插件可以很方便的知道我们现在是查询操作还是增删改操作。(只适用于mybaits持久层框架,如果是hibernate就不行了)
当然,你也可以判断sql,如果sql中包含某个表,用a库,不包含某个表,用b库。不过像一个表还行,几十张表,通过表名去判断查不同的库的话,太费劲了
除了aop以外,我们还有另一种实现方式,就是mybatisPlus的插件。因为通过插件,我们可以知道这个sql是查询、还是insert,像那种读写分离的数据源,是非常的适合的。
2.1、注解wr
就是,切换数据源时的注解,真实开发中,一般一个service只代表一个类的增删改查,所以可以直接把这个注解写在service上,而不是metnhod上
package zheng.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author: ztl
* @date: 2024/02/21 22:34
* @desc:
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WR {
String value() default "W";
}
2.2、 @WR(“W”)
service,带了 @WR注解了
package zheng.service.impl;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import zheng.annotation.WR;
import zheng.entity.Frend;
import zheng.mapper.FrendMapper;
import zheng.service.FrendService;
import java.util.List;
/**
* @author: ztl
* @date: 2022/12/27 11:18
* @desc:
*/
@Service
public class FrendImplService implements FrendService {
@Autowired
FrendMapper frendMapper;
@Override
@WR("R")
public List<Frend> list() {
return frendMapper.list();
}
@Override
@WR("W")
public void save(Frend frend) {
//如果你想获取当前类的代理类(比如你是@Transaction,然后当前类自己调用自己类下的方法,
// 是不会生效的,因为是代理类,你可以先获取到代理类,然后用代理类去执行自己类的方法,)。你可以:
// FrendService o = (FrendService)AopContext.currentProxy();
// System.out.println(o);
// o.save(frend);
frendMapper.save(frend);
}
}
2.3、aop
package zheng.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import zheng.annotation.WR;
import zheng.config.DynamicDataSource;
/**
* @author: ztl
* @date: 2024/02/21 22:30
* @desc:
*/
@Component
@Aspect
public class DynamicDataSourceAspect {
/**
* within 和 execute类似。within能制定到包,execute能制定到类中的方法。
* 如果不指定within的话,spring会把全部的bean都扫描一下,我们目前只需要扫描service,
* 因为我们在service上加的注解,扫描其他bean没意义,白白浪费性能而已,
* 所以指定了service,并且带这个wr注解的话,就set一下多数据源的数据库的链接,
* @param point
* @param wr
*/
@Before("within(zheng.service.impl.*) && @annotation(wr)")
public void before(JoinPoint point, WR wr){
String name = wr.value();
DynamicDataSource.name.set(name);
System.out.println("==============before:"+name);
}
}
3、mybatisPlus的插件实现多数据源
DynamicDadaSourcePlugin
- @Intercepts 是固定的写法
- @Signature 是说你要给mybatis的哪个对象做代理。(插件其实是通过动态代理,在执行具体操作的时候进行增强)
- Executor mysql数据库底层是通过这个executor来执行数据库操作。
- method = “update” 其中增删改,都会调用这个update接口
- method = “update” 代表着查。这样的话,增删改查,就都包含了。
- invocation.getArgs();
拿到当前方法的全部参数。(update的时候,就是update的参数,select的时候,就是select的参数) - MappedStatement 封装了具体的sql
mybatis源码中,MybatisAutoConfiguration mybatis的自动配置类,会自动的将interceptors的数组给注入进来,所以我们只需要定义这个对象就行
因为执行增删改查的时候不是都要通过这个executor嘛,那我们对这个对象进行一个加强的操作,来达到我们切换数据源的目的。
spring中的bean只有一个无参构造函数的时候呢,spring就会自动调用这个无参构造函数,并把所有的参数都进行自动注入,所以我们要将interceptor自动注入,只需要创建一个这个类型的bean对象即可。(mybatis的自动配置类就会自动帮我们注入进来)
3.1、MyMybatisInterceptor
package com.zheng.plugin;
import com.zheng.config.DynamicDataSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
/**
* @author: ztl
* @date: 2024/03/02 22:32
* @desc:
*/
@Intercepts({
@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class}),
@Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class,
RowBounds.class, ResultHandler.class})
})
public class DynamicDadaSourcePlugin implements Interceptor {
/**
* 具体的方法
* @param invocation
* @return
* @throws Throwable
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 拿到当前方法的全部参数。(update的时候,就是update的参数,select的时候,就是select的参数)
Object[] objects = invocation.getArgs();
// MappedStatement 封装了具体的sql、当前的操作类型(查询、update之类的)
MappedStatement ms = (MappedStatement)objects[0];
// 读方法
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
System.out.println("111111+度方法");
DynamicDataSource.name.set("R");
}else {
System.out.println("22222+写方法");
DynamicDataSource.name.set("W");
}
// invocation.proceed() 这个是具体的调用
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor){
return Plugin.wrap(target,this);
}else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
3.2、DataSourceConfig
package com.zheng.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;
/**
* @author: ztl
* @date: 2024/02/06 22:59
* @desc:
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
}
3.3、DynamicDataSource
package com.zheng.config;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* @author: ztl
* @date: 2024/02/08 22:29
* @desc:
*/
@Component
@Primary // 将动态数据源作为核心返回。
// (datasource1、datasource2、DynamicDataSource都会被spring扫描出来,如果只返回datasource1、2就没法动态切换了)
public class DynamicDataSource extends AbstractRoutingDataSource {
// 当前使用的数据源
public static ThreadLocal<String > name = new ThreadLocal<>();
// 写
@Autowired
DataSource dataSource1;
// 读
@Autowired
DataSource dataSource2;
// 返回当前数据源的标识(此处是R/W)
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
@Override
public void afterPropertiesSet() {
// 拿到多数据源中,全部的数据源
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("W",dataSource1);
targetDataSources.put("R",dataSource2);
super.setTargetDataSources(targetDataSources);
// 设置默认的数据源
super.setDefaultTargetDataSource(dataSource1);
// 这个父类的方法还是需要的,不然spring没法把connection对象传递下去,
super.afterPropertiesSet();
}
}
总结
多数据源,到这就分享完毕了。下次应该会给大家分享一下,shardingsphere的用法,以及在我们的项目中,所遇到的问题及解决方案。