美文网首页
Spring Data JPA 使用主从数据源

Spring Data JPA 使用主从数据源

作者: lz做过前端 | 来源:发表于2021-12-23 19:38 被阅读0次

Mysql 配置主从复制

参考:Mysql主从复制-半同步复制
这里我配置了master主库,slave从库slave0和slave1

创建 Spring boot工程

maven pom

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <lombok.maven.plugin.encoding>UTF-8</lombok.maven.plugin.encoding>
    <lombok.version>1.18.20</lombok.version>
    <lombok.maven.plugin.version>1.18.20.0</lombok.maven.plugin.version>
</properties>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.1</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</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>8.0.25</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
    </dependency>
</dependencies>

application.yml

spring:
  jpa:
    properties:
      hibernate:
        enable_lazy_load_no_trans: true
        show_sql: true
        use_sql_comments: true
        format_sql: true
    hibernate:
      ddl-auto: update
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    database-platform: org.hibernate.dialect.MySQL8Dialect
    open-in-view: false

server:
  port: 9300

app:
  datasource:
    parameters: useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
    username: &username root
    password: &password 123456
    driver-class-name: &driver-class-name com.mysql.cj.jdbc.Driver
    master: &default-db-config
      url: jdbc:mysql://localhost:3201/test?${app.datasource.parameters}
      username: *username
      password: *password
      driver-class-name: *driver-class-name
      configuration:
        maximum-pool-size: 30
    slave0:
      <<: *default-db-config
      url: jdbc:mysql://localhost:3202/test?${app.datasource.parameters}
    slave1:
      <<: *default-db-config
      url: jdbc:mysql://localhost:3203/test?${app.datasource.parameters}

数据源相关配置

DataSource

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * 多数据源配置
 */
@Configuration
public class DataSourceConfig {
    /**
     * master
     */
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "app.datasource.master")
    public DataSourceProperties masterDataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * slave0
     */
    @Bean("slave0DataSourceProperties")
    @ConfigurationProperties(prefix = "app.datasource.slave0")
    public DataSourceProperties slave0DataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * slave1
     */
    @Bean("slave1DataSourceProperties")
    @ConfigurationProperties(prefix = "app.datasource.slave1")
    public DataSourceProperties slave1DataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "app.datasource.master.configuration")
    public DataSource masterDataSource(DataSourceProperties dataSourceProperties) {
        return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean("slave0DataSource")
    @ConfigurationProperties(prefix = "app.datasource.slave0.configuration")
    public DataSource slave0DataSource(@Qualifier("slave0DataSourceProperties") DataSourceProperties dataSourceProperties) {
        return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean("slave1DataSource")
    @ConfigurationProperties(prefix = "app.datasource.slave1.configuration")
    public DataSource slave1DataSource(@Qualifier("slave1DataSourceProperties") DataSourceProperties dataSourceProperties) {
        return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }
}

数据源切换思路

动态切换数据源时序
  • 利用ThreadLocal作为单个请求(每个请求单独一个线程)的全局容器连接Service方法控制使用哪个数据源和EntityManager使用的数据源,这样EntityManager使用的数据源就是在Service方法上要求的数据源,即可做到写Service方法时决定使用哪个数据源
  • 利用org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource::determineCurrentLookupKey()暴露获取DataSource的逻辑
  • 在service方法上通过配置Annotation,告诉EntityManager使用哪个数据源
    • 自定义Annotation
    • 利用Spring AOP识别Annotation,织入设置数据源逻辑

数据源切换配置

  • Annotation
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 主数据源
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Master {
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 从数据源
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Slave {
}
  • 多数据源 key
public enum DBInstanceEnum {
    MASTER, SLAVE0, SLAVE1;
}
  • 数据源容器
import lombok.extern.slf4j.Slf4j;
import lombok.val;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 数据源选择器
 */
@Slf4j
public final class DBContextHolder {

    private static final ThreadLocal<DBInstanceEnum> contextHolder = ThreadLocal.withInitial(() -> DBInstanceEnum.MASTER);

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

    public static void switchToMaster() {
        contextHolder.set(DBInstanceEnum.MASTER);
        log.info("switch to master db");
    }

    public static void switchToSlave() {
        // 1.3改进:支持多个从库的负载均衡
        val next = router.incrementAndGet();
        val index = next % 2;
        if (index == 0) {
            contextHolder.set(DBInstanceEnum.SLAVE0);
            log.info("switch to slave0 db");
        } else {
            contextHolder.set(DBInstanceEnum.SLAVE1);
            log.info("switch to slave1 db");
        }
        if (next > 9999) {
            router.set(-1);
        }
    }

    public static DBInstanceEnum get() {
        val db = contextHolder.get();
        log.info("get db{} from contextHolder", db);
        return db;
    }
}
  • 多数据源AOP配置
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 多数据源切换AOP
 */
@Aspect
@Component
@Slf4j
public class DataSourceSwitchAspect {

    /**
     * 切换为 master 数据源织入点
     */
    @Pointcut("@annotation(cc.gegee.study.week7.db.tags.Master) ")
    public void switchToMasterPointcut() {

    }

    /**
     * 切换为 slave 数据源织入点
     */
    @Pointcut("@annotation(cc.gegee.study.week7.db.tags.Slave) ")
    public void switchToSlavePointcut() {

    }

    @Before("switchToSlavePointcut()")
    public void readBefore() {
        DBContextHolder.switchToSlave();
    }

    /**
     * slave 结束后切回到主数据源
     */
    @AfterReturning("switchToSlavePointcut()")
    public void readAfter() {
        DBContextHolder.switchToMaster();
        log.info("after use slave db, set to master db");
    }

    @Before("switchToMasterPointcut()")
    public void writeBefore() {
        DBContextHolder.switchToMaster();
    }
}

动态数据源配置

将上面配置的数据源注入AbstractRoutingDataSource,并将数据源容器获取接入

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

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

@Component
public class RoutingDataSource extends AbstractRoutingDataSource {

    public RoutingDataSource(DataSource masterDataSource, @Qualifier("slave0DataSource") DataSource slave0DataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBInstanceEnum.MASTER, masterDataSource);
        targetDataSources.put(DBInstanceEnum.SLAVE0, slave0DataSource);
        targetDataSources.put(DBInstanceEnum.SLAVE1, slave1DataSource);
        this.setDefaultTargetDataSource(masterDataSource);
        this.setTargetDataSources(targetDataSources);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.get();
    }
}

自定义 JPA 的 EntityManager

将动态数据源注入EntityManager

import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.SharedEntityManagerCreator;

import javax.persistence.EntityManager;
import java.util.Objects;

import static cc.gegee.study.week7.Application.BASE_PACKAGE;
import static cc.gegee.study.week7.db.JpaEntityManagerMaster.ENTITY_MANAGER_FACTORY_BEAN;

/**
 * 自定义 JPA 的 EntityManager
 */
@Configuration
@EnableJpaRepositories(basePackages = {BASE_PACKAGE}, entityManagerFactoryRef = ENTITY_MANAGER_FACTORY_BEAN)
public class JpaEntityManagerMaster {

    public final static String ENTITY_MANAGER_FACTORY_BEAN = "masterEntityManagerFactoryBean";

    private final RoutingDataSource dataSource;

    private final EntityManagerFactoryBuilder builder;

    private final HibernateConfiguration hibernateConfiguration;

    public JpaEntityManagerMaster(RoutingDataSource dataSource, EntityManagerFactoryBuilder builder, HibernateConfiguration hibernateConfiguration) {
        this.dataSource = dataSource;
        this.builder = builder;
        this.hibernateConfiguration = hibernateConfiguration;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactoryBean() {
        return builder
                .dataSource(dataSource)
                .properties(hibernateConfiguration.getVendorProperties(dataSource))
                .packages(BASE_PACKAGE)
                .persistenceUnit("persistenceUnitMaster")
                .build();
    }

    @Bean
    public EntityManager masterEntityManager() {
        return SharedEntityManagerCreator.createSharedEntityManager(Objects.requireNonNull(masterEntityManagerFactoryBean().getObject()));
    }
}

测试

service添加CRUD方法

    @Master
    @Override
    public void add(DeviceModel model) {
        val device = Device.builder()
                .serialNumber(model.getSerialNumber())
                .deviceCategoryName(model.getDeviceCategoryName())
                .build();
        deviceRepository.save(device);
    }

    @Master
    @Override
    public void edit(UUID id, DeviceModel model) {
        val device = deviceRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("invalid id"));
        device.setSerialNumber(model.getSerialNumber());
        device.setDeviceCategoryName(model.getDeviceCategoryName());
        deviceRepository.save(device);
    }

    @Slave
    @Override
    public DeviceDto get(UUID id) {
        val device = deviceRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("invalid id"));
        return DeviceDto.builder()
                .serialNumber(device.getSerialNumber())
                .deviceCategoryName(device.getDeviceCategoryName())
                .build();
    }

    @Slave
    @Override
    public Page<DeviceDto> getPage(Pageable pageable) {
        return deviceRepository.findAll(pageable).map(x -> DeviceDto.builder()
                .serialNumber(x.getSerialNumber())
                .deviceCategoryName(x.getDeviceCategoryName())
                .build());
    }

配置好controller后调用,可以看到如下日志

2021-12-23 09:11:38.926  INFO 8678 --- [nio-9300-exec-1] cc.gegee.study.week7.db.DBContextHolder  : switch to master db
2021-12-23 09:11:48.976  INFO 8678 --- [nio-9300-exec-3] cc.gegee.study.week7.db.DBContextHolder  : switch to slave1 db
2021-12-23 09:11:48.977  INFO 8678 --- [nio-9300-exec-3] cc.gegee.study.week7.db.DBContextHolder  : get dbSLAVE1 from contextHolder

github地址

github地址

相关文章

网友评论

      本文标题:Spring Data JPA 使用主从数据源

      本文链接:https://www.haomeiwen.com/subject/mppfqrtx.html