SpringBoot+MybatisPlus 多数据源读写分离

SpringBoot+MybatisPlus 多数据源读写分离

此文章主要讲了使用 SpringAop 来进行对数据源的切换。

pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.17</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.learn.project</groupId>
    <artifactId>read-write-split</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>read-write-split</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <mysql.version>5.1.45</mysql.version>
        <mybatis-plus.version>3.5.3</mybatis-plus.version>
        <druid.version>1.2.16</druid.version>
        <pagehelper.version>1.4.1</pagehelper.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>${pagehelper.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/**</include>
                </includes>
            </resource>
        </resources>
    </build>

</project>
application.yml
server:
  port: 9000
spring:
  application:
    name: read-write-split
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      # 主数据源(写 改 删)
      master:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.177.130:3306/master_db?characterEncoding=utf8&verifyServerCertificate=false&useSSL=true&serverTimezone=Asia/Shanghai
        username: root
        password: 123
        initialSize: 5
        minIdle: 5
        maxActive: 20
      # 从数据源_1 (读)
      slave1:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.177.129:3306/master_db?characterEncoding=utf8&verifyServerCertificate=false&useSSL=true&serverTimezone=Asia/Shanghai
        username: slave
        password: 123
        initialSize: 5
        minIdle: 5
        maxActive: 20
      # 从数据源_2 (读)
      slave2:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.177.129:3306/master_db?characterEncoding=utf8&verifyServerCertificate=false&useSSL=true&serverTimezone=Asia/Shanghai
        username: slave
        password: 123
        initialSize: 5
        minIdle: 5
        maxActive: 20
mybatis-plus:
  global-config:
    banner: true
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0
  configuration:
    map-underscore-to-camel-case: true
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: com.learn.project.domain
enums

用于对数据源的切换

package com.learn.project.enums;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
public enum DBTypeEnum {
    /**
     * 主
     */
    Master("master"),
    /**
     * 从
     */
    Slave("slave");

    private final String value;

    DBTypeEnum(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}
config
  • DbContextHolder

本地缓存存储使用的数据源以及对从数据库的轮询选择

package com.learn.project.config;

import com.learn.project.enums.DBTypeEnum;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Slf4j
public class DbContextHolder {

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    private static final AtomicInteger COUNTER = new AtomicInteger(-1);

    private static final int MAX_COUNTER = 9999;

    public static void set(String dbSource) {
        // 判断枚举是否为null
        if (null == dbSource) {
            throw new RuntimeException("指定数据源不能不空");
        }
        CONTEXT_HOLDER.set(dbSource);
    }


    public static String get() {
        return CONTEXT_HOLDER.get() == null ? DBTypeEnum.Master.toString() : CONTEXT_HOLDER.get();
    }

    public static void master() {
        log.info(">>>>> 切换到Master");
        set(DBTypeEnum.Master.toString());
    }

    public static void slave() {
        int i = COUNTER.getAndIncrement() % 2;
        if (COUNTER.get() > MAX_COUNTER) {
            COUNTER.set(-1);
        }
        // 如果 i 是单数 切 数据源1
        if (i == 0) {
            log.info(">>>>> 切换到Slave2");
            set(DBTypeEnum.Slave.getValue() + 2);
        } else {
            log.info(">>>>> 切换到Slave1");
            set(DBTypeEnum.Slave.getValue() + 1);
        }
    }

    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}
  • MybatisPlusConfig

关于对 MybatisPlusConfig 的配置,这里只配置了分页。

package com.learn.project.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        paginationInnerInterceptor.setDbType(DbType.MYSQL);
        //分页查询超过最大页码 显示第一页
        paginationInnerInterceptor.setOverflow(true);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        return interceptor;
    }
}
  • MyMetaObjectHandler

对实体类某些字段的自动填充

package com.learn.project.config;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill....");
        this.setFieldValByName("createdAt", LocalDateTime.now(), metaObject);
        this.setFieldValByName("createdBy", "lijiajun1", metaObject);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill....");
        this.setFieldValByName("updatedAt", LocalDateTime.now(), metaObject);
        this.setFieldValByName("updatedBy", "lijiajun2", metaObject);
    }
}
  • DataSourceConfig

配置多数据源

package com.learn.project.config;

import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.learn.project.enums.DBTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.springframework.beans.factory.annotation.Value;
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.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Slf4j
@Configuration
public class DataSourceConfig {

    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    /**
     * 写数据源
     * Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
     * 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
     */
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource writeDataSource() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave1")
    public DataSource readDataSource1() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave2")
    public DataSource readDataSource2() {
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    /**
     * 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
     */
    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        RoutingDataSource proxy = new RoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>(6);
        targetDataSources.put(DBTypeEnum.Master.getValue(), writeDataSource());
        targetDataSources.put(DBTypeEnum.Slave.getValue() + 1, readDataSource1());
        targetDataSources.put(DBTypeEnum.Slave.getValue() + 2, readDataSource2());
        proxy.setDefaultTargetDataSource(writeDataSource());
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }

    /**
     * 由于Spring容器中现在有多个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(routingDataSource());
        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(false);
        sqlSessionFactory.setConfiguration(configuration);
        return sqlSessionFactory.getObject();
    }

    /**
     * 指定数据源事务管理器
     *
     * @return DataSourceTransactionManager 事务管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(routingDataSource());
    }
}
  • RoutingDataSource

选择数据源

package com.learn.project.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Slf4j
public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        String dbSource = DbContextHolder.get();
        log.info("current datasource:{}", dbSource);
        return dbSource;
    }
}
DataSourceSelector

自定义注解

package com.learn.project.annotation;

import com.learn.project.enums.DBTypeEnum;
import java.lang.annotation.*;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.TYPE})
public @interface DataSourceSelector {
    /**
     * 指定的数据源
     *
     * @return DBTypeEnum 数据源
     */
    DBTypeEnum value() default DBTypeEnum.Master;
}
DataSourceAop
package com.learn.project.aop;

import com.learn.project.annotation.DataSourceSelector;
import com.learn.project.config.DbContextHolder;
import com.learn.project.enums.DBTypeEnum;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Aspect
@Component
public class DataSourceAop {

    /**
     * 切面
     */
    @Pointcut("@annotation(com.learn.project.annotation.DataSourceSelector)")
    public void dataSourcePointcut() {
    }

    @Around("dataSourcePointcut()&&@annotation(dataSourceSelector)")
    public Object execute(ProceedingJoinPoint pjp, DataSourceSelector dataSourceSelector) throws Throwable {
        try {
            if (dataSourceSelector.value().equals(DBTypeEnum.Master)) {
                DbContextHolder.master();
            } else if (dataSourceSelector.value().equals(DBTypeEnum.Slave)) {
                DbContextHolder.slave();
            } else {
                throw new RuntimeException("DataSourceSelector value error");
            }
            return pjp.proceed();
        } finally {
            DbContextHolder.clear();
        }
    }
}
service
  • 实体类
package com.learn.project.domain;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Data
@TableName("job_test_data")
public class JobData implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String jobName;
    private String jobClassName;
    private String type;
    @TableField(value = "created_at", fill = FieldFill.INSERT)
    private LocalDateTime createdAt;
}
  • JobDataMapper
package com.learn.project.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.learn.project.annotation.DataSourceSelector;
import com.learn.project.domain.JobData;
import com.learn.project.enums.DBTypeEnum;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@Mapper
public interface JobDataMapper extends BaseMapper<JobData> {
    /**
     * 获取任务数据列表
     *
     * @return list
     */
    @DataSourceSelector(DBTypeEnum.Slave)
    @Select("select * from job_test_data")
    List<JobData> getJobDataList();

    /**
     * 添加数据
     *
     * @param jobData 任务数据
     */
    @DataSourceSelector(DBTypeEnum.Master)
    @Insert("insert into job_test_data(job_name,job_class_name,type, created_at) values(#{jobName},#{jobClassName},#{type},now())")
    void saveJobData(JobData jobData);
}

使用@DataSourceSelector进行选择数据源

  • ProjectController
package com.learn.project.controller;

import com.learn.project.domain.JobData;
import com.learn.project.mapper.JobDataMapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author lijiajun
 * @date 2023/11/21
 */
@RestController
public class ProjectController {

    @Resource
    private JobDataMapper jobDataMapper;

    @GetMapping
    public List<JobData> getJobDataList() {
        return this.jobDataMapper.getJobDataList();
    }

    @PostMapping
    public void saveJobData(@RequestBody JobData data) {
        this.jobDataMapper.saveJobData(data);
    }
}

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值