多数据源整合实现读写分离

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

概述

缓存不可能存储所有的订单数据,而且会存在缓存过期的情况,总会有一部分读的流量直接打到数据库,所有的写流量都会打到数据库,如果请求量增加,依然会造成系统性能的问题。这个时候我们可以搭建MySQL的主从复制架构,实现读写分离,让所有的写流量在主库执行,没有命中缓存的都流量走从库。

环境准备

  • Spring boot
  • mybatis-plus
  • MySQL

首先搭建一套master-slave的MySQL系统,然后在系统中自定义两个数据源(master数据源、slave数据源)。

主从架构搭建

CentOS安装MySQL请参考Centos系统安装MySQL数据库

  • 主库配置server-id,开启bin-log日志
  • 主库配置一个账号,用于配置到从库中进行主从复制
  • 主库配置需要同步的数据库binlog-do-db
  • 主库配置不需要同步的数据库binlog-ignore-db
  • 主库配置主从复制模式
  • 从库配置server-id,可以开启bin-log日志
  • 从库配置需要同步的数据库replicate_wild_do_table
  • 从库配置不需要同步的数据库replicate_wild_ignore_table
  • 主库、从库都重新启动
  • 从库配置主库之前配置的用于主从同步的账号

多数据源整合

项目地址

简单的写个数据源整合的Demo,实现一个应用使用多个数据源的场景。Spring boot作为开发框架、mybatis-plus作为ORM框架、MySQL作为持久层。数据源、mapper、xml文件都是两份(也可以将entity定义两份)。

共同使用一套controller、service,service中包含两个数据源的mapper,根据具体的业务读写场景选择不同的mapper即可实现对不同数据库的操作。

flowchart TD
id1((Cleint)) -->|Request| A(Controller)
A --> B(service)
B --> C(FirstMapper)
B --> D(SecondMapper)
C --> E[(First)]
D --> F[(Second)]
复制代码
  • 创建项目

image.png

采用Maven父子模块的方案,项目搭建完成,并启动成功,初始目录结构如下。

image-20220727103942273.png

  • 添加依赖
<!--mysql 驱动-->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- mybatis-plus -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

省略其他依赖...

  • 数据库配置文件
# master
spring.datasource.first.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.first.jdbc-url=jdbc:mysql://localhost:3306/monomer_order?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull
spring.datasource.first.username=root
spring.datasource.first.password=

# slave
spring.datasource.second.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.second.jdbc-url=jdbc:mysql://localhost:3306/vip-boot?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull
spring.datasource.second.username=root
spring.datasource.second.password=

package com.xinxing.learning.datasource.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
  • 添加数据源
import javax.sql.DataSource;

/**
 * 定义两个数据源
 */
@Configuration
@PropertySource("classpath:config/jdbc.properties")
public class DataSourceConfig {

    @Bean("first")
    @ConfigurationProperties(prefix = "spring.datasource.first")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean("second")
    @ConfigurationProperties(prefix = "spring.datasource.second")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
}
  • mybatis配置
package com.xinxing.learning.datasource.config;

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * 第一个数据源的mybatis-plus配置
 * 定义SqlSessionFactoryBean
 * 配置扫描mapper的路径,entity的Alias的路径,xml文件路径
 */
@Configuration
@MapperScan(basePackages = {"com.xinxing.learning.datasource.mapper.first"}, sqlSessionFactoryRef = "firstSqlSessionFactory")
public class FirstMybatisConfig {

    @Bean("firstSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("first") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        // 1、添加数据源
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 2、xml配置文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        String localMapperXmlPattern = "classpath*:/mapper/first/*.xml";
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources(localMapperXmlPattern));
        // 3、实体类别名
        String typeAliasesPackage = "com.xinxing.learning.datasource.entity";
        sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
        // 4、mybatis其他的属性配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        // 4.1、配置日志实现
        configuration.setLogImpl(StdOutImpl.class);
        // 4.2、此处可以添加其他mybatis配置 例如转驼峰命名
        configuration.setMapUnderscoreToCamelCase(true);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }
}
package com.xinxing.learning.datasource.config;

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * 第二个数据源的mybatis-plus配置
 * 定义SqlSessionFactoryBean
 * 配置扫描mapper的路径,entity的Alias的路径,xml文件路径
 */
@Configuration
@MapperScan(basePackages = {"com.xinxing.learning.datasource.mapper.second"}, sqlSessionFactoryRef = "secondSqlSessionFactory")
public class SecondMybatisConfig {

    @Bean("secondSqlSessionFactory")
    public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("second") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        // 1、添加数据源
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 2、xml配置文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        String localMapperXmlPattern = "classpath*:/mapper/second/*.xml";
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources(localMapperXmlPattern));
        // 3、实体类别名
        String typeAliasesPackage = "com.xinxing.learning.datasource.entity";
        sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
        // 4、mybatis其他的属性配置
        MybatisConfiguration configuration = new MybatisConfiguration();
        // 4.1、配置日志实现
        configuration.setLogImpl(StdOutImpl.class);
        // 4.2、此处可以添加其他mybatis配置 例如转驼峰命名
        configuration.setMapUnderscoreToCamelCase(true);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }
}
package com.xinxing.learning.datasource.mapper.second;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xinxing.learning.datasource.domain.vo.OrderInfoVO;
import com.xinxing.learning.datasource.entity.OrderInfo;
import com.xinxing.learning.datasource.domain.query.OrderInfoBaseQuery;
import org.apache.ibatis.annotations.Param;

/**
 * 用户订单mapper
 */
public interface SecondUserOrderInfoMapper {

    /**
     * 插入订单
     *
     * @param orderInfo 入参
     * @return 出参
     */
    int insertSelective(@Param("orderInfo") OrderInfo orderInfo);

    /**
     * 根据条件分页查询订单列表
     *
     * @param page               入参
     * @param orderInfoBaseQuery 入参
     * @return 出参
     */
    Page<OrderInfoVO> queryOrderInfoList(Page<OrderInfoVO> page, @Param("record") OrderInfoBaseQuery orderInfoBaseQuery);
}
  • 添加xml文件

省略。。。具体可参考项目

其他的entity、controller、service等省略,完整项目代码地址

配置的数据源中的数据库分别都有order_info表,表的结构是一样的,所以entity是共用一份。实现效果是插入数据到monomer_order数据库的order_info表中,查询数据是到vip-boot数据库的order_info表中。

image-20220727125445274.png

插入前monomer_order的order_info表中的数据情况:

image-20220727130741204.png

日志显示数据插入成功,到monomer_order的order_info表进行验证。

image-20220727130939560.png

monomer_order的order_info表中存在刚插入的数据。

image-20220727131151376.png

查看数据库vip-boot中的表order_info的数据条数。

image-20220727131354287.png

调用查询接口返回数据条数。

image-20220727131506962.png

整合多数据源成功。

内部根据不同请求操作不同数据库的流程如下图:

image-20220727164535915.png

总结

多数据源整合案例可以用来实现读写分离、数据复制等操作。如果vip-boot数据库是monomer-order数据库的从库,那么就实现了写数据到主库,读数据到从库。如果我们要做数据迁移,可以通过查询monomer-order数据库中的数据,然后写到vip-boot数据库中。核心就是一个数据源对应一套mapper、xml。需要操作哪个数据源,直接导入对应的mapper对象即可。

这套数据源整合案例还有很多需要优化的地方,比如怎么整合其他的数据库连接池,怎只用实现一套mapper、xml动态的实现不同数据源的操作等。

现在大多数项目中对于多数据源的读写分离操作,主要是采用第三方中间件,比如shardign-jdbc。但是还是可以通过自己实现的多数据源整合进行对第三方中间件的理解。

猜你喜欢

转载自juejin.im/post/7126127579790049288
今日推荐