MySQL-SpringBoot整合JPA實現資料讀寫分離

Anbang713發表於2018-10-21

在上篇部落格《MySQL-主從複製之同步主從資料》中,我們實現了讀庫和寫庫的資料同步。今天,我們繼續學習SpringBoot整合JPA如何實現資料讀寫分離。廢話不多話直接上程式碼。

一、配置資料來源

# 資料來源
spring.datasource.druid.write.url=jdbc:mysql://localhost:3380/test
spring.datasource.druid.write.username=root
spring.datasource.druid.write.password=Anbang713
spring.datasource.druid.write.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.druid.read.url=jdbc:mysql://localhost:3381/test
spring.datasource.druid.read.username=root
spring.datasource.druid.read.password=Anbang713
spring.datasource.druid.read.driver-class-name=com.mysql.jdbc.Driver

# JPA
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.database=mysql
spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.naming-strategy=org.hibernate.cfg.DefaultComponentSafeNamingStrategy
spring.jpa.show-sql=false

二、資料來源配置類

/**
 * 資料來源配置
 * 
 * @author Administrator
 *
 */
@Configuration
public class DataSourceConfig {

  public final static String WRITE_DATASOURCE_KEY = "writeDruidDataSource";
  public final static String READ_DATASOURCE_KEY = "readDruidDataSource";

  @ConfigurationProperties(prefix = "spring.datasource.druid.read")
  @Bean(name = READ_DATASOURCE_KEY)
  public DataSource readDruidDataSource() {
    return new DruidDataSource();
  }

  @ConfigurationProperties(prefix = "spring.datasource.druid.write")
  @Bean(name = WRITE_DATASOURCE_KEY)
  @Primary
  public DataSource writeDruidDataSource() {
    return new DruidDataSource();
  }

  /**
   * 注入AbstractRoutingDataSource
   * 
   * @param readDruidDataSource
   * @param writeDruidDataSource
   * @return
   * @throws Exception
   */
  @Bean
  public AbstractRoutingDataSource routingDataSource(
      @Qualifier(READ_DATASOURCE_KEY) DataSource readDruidDataSource,
      @Qualifier(WRITE_DATASOURCE_KEY) DataSource writeDruidDataSource) throws Exception {
    DynamicDataSource dataSource = new DynamicDataSource();
    Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
    targetDataSources.put(WRITE_DATASOURCE_KEY, writeDruidDataSource);
    targetDataSources.put(READ_DATASOURCE_KEY, readDruidDataSource);
    dataSource.setTargetDataSources(targetDataSources);// 配置資料來源
    dataSource.setDefaultTargetDataSource(writeDruidDataSource);// 預設為主庫用於寫資料
    return dataSource;
  }
}

三、使用ThreadLocal使資料來源與執行緒繫結

public class DynamicDataSourceHolder {
  // 使用ThreadLocal把資料來源與當前執行緒繫結
  private static final ThreadLocal<String> dataSources = new ThreadLocal<String>();

  public static void setDataSource(String dataSourceName) {
    dataSources.set(dataSourceName);
  }

  public static String getDataSource() {
    return (String) dataSources.get();
  }

  public static void clearDataSource() {
    dataSources.remove();
  }
}

四、動態資料來源配置

public class DynamicDataSource extends AbstractRoutingDataSource {

  @Override
  protected Object determineCurrentLookupKey() {
    // 可以做一個簡單的負載均衡策略
    String lookupKey = DynamicDataSourceHolder.getDataSource();
    System.out.println("------------lookupKey---------" + lookupKey);
    return lookupKey;
  }
}

五、寫資料來源配置類

@Configuration
@EnableJpaRepositories(value = "com.study.mysql.jpa.dao",
    entityManagerFactoryRef = "writeEntityManagerFactory",
    transactionManagerRef = "writeTransactionManager")
public class WriteDataSourceConfig {

  @Autowired
  JpaProperties jpaProperties;
  @Autowired
  @Qualifier("writeDruidDataSource")
  private DataSource writeDruidDataSource;

  /**
   * 我們通過LocalContainerEntityManagerFactoryBean來獲取EntityManagerFactory例項
   * 
   * @return
   */
  @Bean(name = "writeEntityManagerFactoryBean")
  public LocalContainerEntityManagerFactoryBean writeEntityManagerFactoryBean(
      EntityManagerFactoryBuilder builder) {
    return builder.dataSource(writeDruidDataSource).properties(jpaProperties.getProperties())
        .packages("com.study.mysql.jpa.api") // 設定實體類所在位置
        .persistenceUnit("writePersistenceUnit").build();
  }

  /**
   * EntityManagerFactory類似於Hibernate的SessionFactory,mybatis的SqlSessionFactory
   * 總之在執行操作之前我們總要獲取一個EntityManager,這就類似於Hibernate的Session,mybatis的sqlSession。
   * 
   * @param builder
   * @return
   */
  @Bean(name = "writeEntityManagerFactory")
  @Primary
  public EntityManagerFactory writeEntityManagerFactory(EntityManagerFactoryBuilder builder) {
    return this.writeEntityManagerFactoryBean(builder).getObject();
  }

  /**
   * 配置事物管理器
   * 
   * @return
   */
  @Bean(name = "writeTransactionManager")
  @Primary
  public PlatformTransactionManager writeTransactionManager(EntityManagerFactoryBuilder builder) {
    return new JpaTransactionManager(writeEntityManagerFactory(builder));
  }
}

六、自定義註解

@Target({
    ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDateSource {
  String dataSource() default "";// 資料來源
}

七、定義切面,實現資料來源切換

@Aspect
@Component
public class DynamicDataSourceAspect {

  @Around("execution(public * com.study.mysql.jpa.core..*.*(..))")
  public Object around(ProceedingJoinPoint pjp) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
    Method targetMethod = methodSignature.getMethod();
    if (targetMethod.isAnnotationPresent(TargetDateSource.class)) {
      String targetDataSource = targetMethod.getAnnotation(TargetDateSource.class).dataSource();
      System.out.println("----------資料來源是:" + targetDataSource + "------");
      DynamicDataSourceHolder.setDataSource(targetDataSource);
    }
    // 執行方法
    Object result = pjp.proceed();
    DynamicDataSourceHolder.clearDataSource();
    return result;
  }
}

在完成上面的相關配置後,我們寫個簡單的學生增刪改查介面做測試。至此,我們的專案結構是這樣的:

當然在這裡,我們有必要看一下業務層實現類的程式碼,通過註解@TargetDataSource註解實現讀寫分離。

@Service
public class StudentServiceImpl implements StudentService {

  @Autowired
  private StudentDao studentDao;

  @Override
  @TargetDateSource(dataSource = DataSourceConfig.READ_DATASOURCE_KEY)
  public List<Student> findAll() {
    return studentDao.findAll();
  }

  @Override
  @TargetDateSource(dataSource = DataSourceConfig.READ_DATASOURCE_KEY)
  public Student findById(Integer id) {
    Optional<Student> students = studentDao.findById(id);
    if (students.isPresent() && students.get() != null) {
      return students.get();
    }
    return null;
  }

  @Override
  @Transactional
  @TargetDateSource(dataSource = DataSourceConfig.WRITE_DATASOURCE_KEY)
  public Integer save(Student entity) throws Exception {
    if (entity.getId() != null) {
      Student perz = studentDao.saveAndFlush(entity);
      return perz.getId();
    }
    Student perz = studentDao.save(entity);
    return perz.getId();
  }

}

八、測試

啟動SpringBoot啟動類,並通過http://localhost:8080/swagger-ui.html訪問我們的學生類介面。在測試之前,我們現在看下資料庫的資料。可以看到我們的主從資料庫資料是一樣的。(MySQL5.6-3380為主資料庫,用於寫資料;MySQL5.6-3381為從資料庫,用於讀資料)

那麼我們現在往資料庫插入一條資料,執行save介面:

首先可以看到,在切面類中列印的日誌,已經實現資料來源的自動切換了。

然後我們看下資料庫的資料,可以看到兩邊的資料是一模一樣的。

最後,我們測試一下讀的時候是從哪個資料來源讀的。

可以看到,在讀請求的時候,是從從資料庫讀的資料。至此,我們使用SpringBoot整合JPA實現讀寫分離的目的已經達到。

原始碼地址: https://gitee.com/chengab/MySQL

參考部落格:https://www.jb51.net/article/111588.htm

 

相關文章