java多数据源几种实现方式以及demo

提示:多数据源实现方式、多数据源的使用场景。AbstractRoutingDataSource、DynamicDataSource框架、mybatisplus的Intercepter插件、java中多数据源的几种实现方式、mybatisPlus的插件实现多数据源

前言

最近工作中有一张表,实际数据量超过1亿了,导致一条普通的insert语句也耗时15秒,因此需要分表。在使用shardingSphere分表时,需要切换多数据源,因此特意调研了一下多数据源的几种实现方式。再次记录一下,感兴趣的同学可以下载代码,这样看起来更加清晰。gitee代码


一、多数据源的几种实现方式

java中实现多数据源,比较常见的方式有3种:

  1. abstractRootingDataSource
  2. mybatisplus的Intercepter插件
  3. DynamicDataSource 框架

其实最底层的核心原理,就是abstractRootingDataSource,剩下的两种,肯定也是以第一种为基础的,只不过封装了一层而已。

二、使用场景

一般来说,多数据源有以下两种使用场景:

  • 业务复杂(数据量大)。数据分布在不同的数据库中,数据库拆了, 应用没拆。 一个公司多个子项目,各用各的数据库,涉及数据共享…
  • 读写分离。为了解决数据库的读性能瓶颈(读比写性能更高, 写锁会影响读阻塞,从而影响读的性能)。

三、核心原理

1、原理

最核心的类就是AbstractRootingDataSource,因此我们着重介绍一下。
这个抽象类中,有3个比较重要的成员变量:

在这里插入图片描述

  1. 1、此时,我们仍然返回的是dynamicDatasource,只是,我们继承了AbstruceRootingDataSource,然后getConnection方法变成了由AbstruceRootingDataSource提供的connection了
  2. 这个getConnection方法内部,是: determineTargetDataSource().getConnection();
  3. 而2中底层是调用的模版方法,去获取最终的connection。因为是map中的get方法获取的,所以get的这个key是关键,
    lookupKey =
    determineCurrentLookupKey();resolvedDataSource.get(lookupKey);
  4. 而这个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

相关解释:

  1. getConnection 是核心的,返回哪个数据库的链接的。
  2. afterPropertiesSet 是初始化的操作
  3. 实现了datasource接口这个肯定好理解,我们要返回一个动态数据源,也是个数据源嘛
  4. 实现了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的用法,以及在我们的项目中,所遇到的问题及解决方案。

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值