原创

Java架构师方案—多数据源原理及应用(一)(附完整项目代码)

读完本篇文章你将学到:原生jdbc开发流程,datasource数据源层,多数据源组件原理。以及它们三者之间的关系架构

1. 原生jdbc数据库开发

使用原生jdbc来访问数据库的流程为:

Class.forName(“com.mysql.jdbc.Driver”);
String url = “jdbc:mysql://hostip:3306/test?user=root&password=123456″;
Connection con = DriverManager.getConnection(url);
Statement statement = con.createStatement();

在原生的jdbc访问过程中,重要的领域对象包括:Driver ,DriverManager ,Connection ,Statement 等等。

其实主要与数据库交互的是connection对象,连接的创建会向操作系统申请网络连接资源并占用,直到conneciton对象销毁才还给操作系统,因为每次都要申请并释放操作系统资源,所以原生开发在业务繁忙的项目中资源的释放和申请就会非常频繁。

为了避免这种频繁资源申请和释放情况,DataSource层使用连接池方式解决了这个问题。

DataSource领域模型也是建立在这些原生领域对象之上建立的代理层。

原生jdbc访问过程包括:

  • Class.forName加载驱动对象并注册到DriverManager,Driver初始化的静态代码如下

    static {
    try {
    java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
    throw new RuntimeException(“Can’t register driver!”);
    }
    }
    
  • DriverManager.getConnection:注册完驱动对象后,随后传入数据库IP地址和登入用户名密码,使用Driver对象创建Connection对象,几段代码如下:

    Connection con = aDriver.driver.connect(url, info);
    return (con);
    
  • 使用connection对象结合statement和preparestatement等方式执行sql

分析:我们每次执行sql我们都需要去初始化connection,执行完后又释放连接,这个过程中包括锁的金证以及与远程数据库的交互过程,因此每次connection的建立和释放所需要的成本。因此为了降低连接的建立和释放的消耗,这里就需要datasource领域模型来实现。

2. 数据源领域模型

原生jdbc提到的连接重复建立和释放的成本问题,在datasource领域模型中使用了池技术思想,实现了连接的重复使用,避免了每次使用时都重新建立与数据库之间的连接,会产生较大的系统开销。以空间换时间的方式来提高效率。

datasource的主要API如下

DataSource只有两个方法(确切的说是一个方法的两个重载版本),用于建立与此 DataSource 对象所表示的数据源的连接。

Connection getConnection()
Connection getConnection(String username, String password)

在阿里的DruidDataSource实现类中,我们能看到类的connections连接对象容器。

// store
private volatile DruidConnectionHolder[] connections;

connections连接数组结合datasource的池技术机制实现来管理数据源的连接对象。我们也可以把datasource看作业务程序和DriverManager之间的中间层,这个中间层有管理connection对象功能以及实现事务机制。

看下图,清楚DataSource与原生jdbc的关系架构。

alt

从关系图能看出:

  • DataSource是基于jdbc的一层抽象,实现了两个主要功能:维护连接池,实现事务管理。
  • jdbc才是真正去与数据库打交道的角色,而与用户程序交互的是DataSource代理层。

3. 多数据源组件AbstractRoutingDataSource

多数据源按字面理解就是 DataSource有多个,用户程序可以自动选择不同的DataSource来为自己的业务服务。因此。

  • 多数据源组件具有维护多个DataSource对象的功能。
  • 而且数据源的切换动作由用户程序触发。
  • 多数据源组件是更高的代理层,直接管理DataSource层。
  • datasource的连接池维护这数据库连接,多数据源组件的datasource池维护所有datasource。

根据Java面向对象的思想,用户程序会觉得多数据源组件AbstractRoutingDataSource就是数据源对象,因为多数据源组件实现了DataSource接口,有数据源接口的两个api方法,如下所示:

Connection getConnection() throws SQLException;

Connection getConnection(String username, String password)
  throws SQLException;

因此,我们可以把多数据源组件看作维护多个数据源的代理层,它们之间的关系如下图所示:

alt

从关系图可以看出:

  • 多数据源代理是数据源层中所有数据源对象的代理对象
  • 多数据源组件维护一个Map数据结构
  • 用户线程通过ThreadLocal来传递变量StringKey信息,指定对应的数据源
  • 用户通过Object determineCurrentLookupKey()方法来指定StringKey

4. 多数据源下读写分离应用

根据多数据源的关系结构图,可以分析出:只要用户程序根据自己的业务,在执行某项业务时候指定Stringkey,就能做到切换数据源的目的。

因此在读写分离的应用场景中,客户端程序需要识别Mybatis的Dao层执行的方法是读还是写。

Mybatis框架提供了获取方法执行的sql类型的Api

SqlCommandType type = sqlSessionFactory.getConfiguration().getMappedStatement(key).getSqlCommandType();

key的值能通过切面的JoinPoint来获得,这里我不再赘述,大家可以结合项目的RwSeparateDataSourceAspect类代码来看。

这里涉及到Spring容器中的@Aspect切面编程,这里就不对切面开发进行剖析。

5. 聚合后台项目多数据源应用

有的公司产品的后台管理项目,如果只是给运营使用的话,一般项目都是一个体量比较大的maven聚合项目,在这样的一个项目中,我们就需要去访问不同的产品的数据库,这里我们就要主动的去动态切换数据源。

这种聚合项目也是要进行切面开发的,不像读写分离场景需要在Dao层进行切面开发,这里是基于业务层面进行切面开发,因此我们切点会放在Controller层或者service层。

总而言之,从多数据源的原理和应用来看,多数据源结合了ThreadLcoal传参+切面编程+池技术等多种技术方案。读写分离场景还结合了Mybatis框架的API技术,来判断sql的类型。

因此多数据源分可以简单拆分成两个阶段:用户程序指定Datasource的key;用户程序根据Key来获取对应的connection对象。

6. 读写分离和不同业务数据源切换项目测试

执行业务时动态切换数据源
  • 准备两个数据库myutilproject0和myutilproject1,并创建user表
create database myutilproject0;
create database myutilproject1;

create table user(
id int(11) not null primary key,
name varchar(100),
gender varchar(10));
  • 分别往两个数据库插入数据,执行DynamicDatasourceMethodApplicationTests测试类

测试类执行完成后,两张表中有了数据,分别往两个库插入了数据。

alt

分别插入两个不同库中的表,由下面两个方法完成,不同的业务使用@DBType注解来动态切换数据源

    @DBType(DataSourceType.MASTER)
    public void insert2Master(User user){
        userMapper.insertSelective(user);
    }

    @DBType(DataSourceType.SLAVE)
    public void insert2Slave(User user){
        userMapper.insertSelective(user);
    }
  • 我在这里埋一个坑

看UserService类的几个方法

  1. select2查询slave库数据
  2. select1查询master数据并调用select2
  3. select3查询master数据并调用select2

执行DynamicDatasourceAnnoApplicationTests测试类,分别运行select1和select3。这两个方法调用了select2,打印结果如下

select1
MASTER:11--jackdking-master==male

select2
SLAVE:11--jackdking-master==male

select3
MASTER:11--jackdking-master==male

select2
SLAVE:11--jackdking-slave==male

你会发现: select1调用select2时候,select2切换数据源失败;但是select3调用select1时候,数据源切换生效了。 这个坑大家自己可以思考下,有疑问可来咨询我!

读写分离切换数据源
  • 打开读写分离开关

将RwSeparateDataSourceAspect类中的下的pointcut注释去掉。

//    @Pointcut("execution(* org.jackdking.core.dao.*.*(..))")
    public void declareJoinPoint(){
    }
  • 运行RwSeparateDataSourceAspect测试类

读写方法执行后,你会发现select方法查出从库数据:11--jackdking-slave==male。而主库写入了一条数据。

alt

至此,读写分离就实现了,注意在测试数据动态源切换的时候一定要把读写注掉,否则会影响动态切换测试。

到这里,文章就学完了,识别下面小程序码,进入本文的技术知识点做题页面,来看看你能掌握多少,满分10分。

alt

还要配置状态转移信息以及事件处理器逻辑的开发。完整的demo项目,请关注公众号“前沿科技bot“并发送"数据源切换"获取。

alt

正文到此结束