1. 程式人生 > >springboot + jpa(hiberbate)or springboot + mybatis實現主從分離

springboot + jpa(hiberbate)or springboot + mybatis實現主從分離

springboot+ jpa 以及spring+mybatis 都已經實現主從,這篇主要講解下springboot +jpa的實現,兩種方式的原始碼我都會貼上github地址。

github原始碼地址:

springboot + jpa : https://github.com/ShiLeiJava/separation2

spring boot+ mybatis :https://github.com/ShiLeiJava/separation

通過mysql實現主從配置的思路。
   通過spring AOP @Before 通知,線上程進入service方法之前拿到service方法上面的自定義註解@ReadDataSource或者@WriteDataSource來判斷,在ThreadLocal變數中設定是拿slave的key,還是拿Master的key。然後通過資料來源proxy通過key來獲取對應的資料來源將其注入到jpa中。可以在這邊配置多個slave,並對其做一些負載均衡。

一、專案配置

1、yml檔案配置

  jpa:
    hibernate:
      naming:
        physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
      ddl-auto: update  # 第一次簡表create  後面用update
    show-sql: true

多資料來源配置

#讀寫分離配置
mysql:
  datasource:
    readSize: 1  #讀庫個數
    type: com.alibaba.druid.pool.DruidDataSource
    write:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://192.168.1.114:3306/jpatest?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
      username: xxx
      password: xxx
    read:
      url: jdbc:mysql://192.168.1.138:3306/jpatest?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=true
      username: xxxx
      password: xxxx
      driver-class-name: com.mysql.jdbc.Driver

其中readSize是代表讀庫的個數,在代理類中使用,可以對slave做一些負載均衡

2、資料庫的配置 

 A、資料來源配置

/**
 * Created by Leo_lei on 2018/11/8
 */
@Configuration
public class DataSourceConfiguration {

    private static Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class);

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


    //寫庫
    @Primary
    @Qualifier("writeDataSource")
    @Bean("writeDataSource")
    @ConfigurationProperties(prefix = "mysql.datasource.write")
    public DataSource writeDataSource(){
        log.info("-------------------- writeDataSource init ---------------------");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }


    //讀庫
    @Qualifier("readDataSource")
    @Bean(name = "readDataSource")
    @ConfigurationProperties(prefix = "mysql.datasource.read")
    public DataSource readDataSourceOne() {
        log.info("-------------------- read DataSourceOne init ---------------------");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

}

@Qualifier註解是解決如果有多個例項或者不存在例項情況下會丟擲異常,這樣就無法啟動專案。新增這個註解是為了更加細粒的注入。

B、本地執行緒上下文配置

/**
 * 本地執行緒,資料來源上下文
 * Created by Leo_lei on 2018/11/8
 */
public class DataSourceContextHolder {

	private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);

	//執行緒本地環境
	private static final ThreadLocal<String> local = new ThreadLocal<String>();

    public static ThreadLocal<String> getLocal() {
        return local;
    }

    /**
     * 讀庫
     */
    public static void setRead() {
        local.set(DataSourceType.read.getType());
        log.info("資料庫切換到讀庫...");
    }

    /**
     * 寫庫
     */
    public static void setWrite() {
        local.set(DataSourceType.write.getType());
        log.info("資料庫切換到寫庫...");
    }

    public static String getReadOrWrite() {
        return local.get();
    }

    public static void clear(){
    	local.remove();
    }
}

每次訪問API都是獨立的執行緒,我們可以通過AOP,在執行Service方法前來設定本地執行緒變數ThreadLocal的值來設定當前訪問哪個資料來源。

C、定義的資料來源型別


/**
 * Created by Leo_lei on 2018/11/8
 */
public enum  DataSourceType {

    read("read", "從庫"),
    write("write", "主庫");

    private String type;

    private String name;

    DataSourceType(String type, String name) {
        this.type = type;
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

定義了讀庫和寫庫。這個主要是作為一個key,AOP的時候將這個key設定到ThreadLocal變數中,然後在資料來源代理類proxy通過key去獲取到當前要使用的資料來源。

D、AOP配置 ---- 主要配置的是service層面的AOP

@Aspect
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
@Component
public class DataSourceAopInService implements PriorityOrdered {

private static Logger log = LoggerFactory.getLogger(DataSourceAopInService.class);

	@Before("execution(* com.leo.separation2.service..*.*(..)) "
			+ " and @annotation(com.leo.separation2.config.ReadDataSource) ")
	public void setReadDataSourceType() {
		//如果已經開啟寫事務了,那之後的所有讀都從寫庫讀
		if(!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())){
			DataSourceContextHolder.setRead();
		}

	}

	@Before("execution(* com.leo.separation2.service..*.*(..)) "
			+ " and @annotation(com.leo.separation2.config.WriteDataSource) ")
	public void setWriteDataSourceType() {
	    DataSourceContextHolder.setWrite();
	}
    
	@Override
	public int getOrder() {
		return 1;
	}

}

這邊有兩個方法,@Before中的引數指的是,在service包下面,如果方法上有註解@ReadDataSource 或者@WirteDataSource,那麼分別不同的方法設定不同的資料來源

在讀的AOP中,添加了一個判斷,是為了解決如果已經寫入過資料了,那麼接下來的查詢還是進入到讀庫,避免了寫和讀產生時間差的問題。

重寫order方法,是為了Aop在事務之前執行。

E、實現代理類,獲取到key。

public class DynamicDataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {

        System.out.println("最終拿到的是:"+DataSourceContextHolder.getReadOrWrite());

        String typeKey = DataSourceContextHolder.getReadOrWrite();
//
        if(typeKey == null){
            return DataSourceType.write.getType();
        }

        if (typeKey.equals(DataSourceType.write.getType())){
            System.err.println("使用資料庫write.............");
            return DataSourceType.write.getType();
        }

        //讀庫, 簡單負載均衡
//                int number = count.getAndAdd(1);
//                int lookupKey = number % readSize;
//                System.err.println("使用資料庫read-"+(lookupKey+1));
        return DataSourceType.read.getType()/*+(lookupKey+1)*/;

//        return DataSourceContextHolder.getReadOrWrite();
    }
}

這個類繼承AbstractRoutingDataSource。通過ThreadLocal拿到當前執行緒在AOP中設定的型別key。然後去分別判斷當前使用什麼key去資料來源的targerDataSource中找。if typeKey== null的話,則給他預設進入master。

這邊還可以對slave 做一個簡單的負載均衡。我例子中只使用了一個,我就不演示這個了,如果要實現這個,你需要在yml中加配置,還有在資料來源配置中加入bean實現。

F、配置JPAConfiguration --- 最重要的一個 配置了。這個配置我也是研究了好久,踩了很多的坑配起來,並讓springboot能夠啟動。


/**
 * Created by Leo_lei on 2018/11/13
 */
@Configuration
@EnableConfigurationProperties(JpaProperties.class)
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager",
        value = "com.leo.separation2.dao")
@AutoConfigureAfter(DataSourceConfiguration.class)
public class JpaEntityManager {

    @Autowired
    private JpaProperties jpaProperties;  //載入yml中jpa的配置

    @Autowired
    @Qualifier("writeDataSource")
    private DataSource writeDataSource; //載入master配置
    @Autowired
    @Qualifier("readDataSource")
    private DataSource readDataSource; //載入slave配置

    /**
     * 配置資料來源集合到 abstractRoutionDataSource中
     */    
    @Bean(name = "routingDataSource")
    public AbstractRoutingDataSource routingDataSource() {
        DynamicDataSourceRouter proxy = new DynamicDataSourceRouter();
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put(DataSourceType.write.getType(), writeDataSource);
        targetDataSources.put(DataSourceType.read.getType(), readDataSource);

        proxy.setDefaultTargetDataSource(writeDataSource); //將master資料來源設定為預設
        proxy.setTargetDataSources(targetDataSources);//將yml中配置的資料來源到target
        return proxy;
    }

    
    @Bean(name = "entityManagerFactoryBean")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder) {
        Map<String, String> properties = jpaProperties.getProperties();
        //要設定這個屬性,實現 CamelCase -> UnderScore 的轉換
        properties.put("hibernate.physical_naming_strategy",
                "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");


        return builder
                .dataSource(routingDataSource())//關鍵:注入routingDataSource
                .properties(properties)
                .packages("com.leo.separation2.entity") //jpa實體包路徑
                .persistenceUnit("myPersistenceUnit") 
                .build();
    }

    @Primary
    @Bean(name = "entityManagerFactory")
    public EntityManagerFactory entityManagerFactory(EntityManagerFactoryBuilder builder) {
        return this.entityManagerFactoryBean(builder).getObject();
    }

    @Primary
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactory(builder));
    }

}

那麼就從開頭講解下吧:

@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManagerFactory",
        transactionManagerRef = "transactionManager",
        value = "com.leo.separation2.dao")

實現自定義jpa配置,你需要從新定義一個entityManagerFactory,以及一個transationManager。

這個註解,是開啟自定義的jpa配置,讓springboot能夠識別這個配置。  其中value值是指實體所在的包。而兩個ref 一個是指

自定義EntityManagerFactory 的bean,一個是指TransactionManager bean,都是在下面定義的。

具體的我在配置裡面加入了註解。

完成以上配置,那麼你可以啟動程式跑起來測試了。

二、測試

同時我在service層中添加了兩個註解,然後封裝成了API 通過postman http請求,訪問成功,達到了自己預期的結果。大家可以去測試一下。

 

三、問題

1、在資料來源配置檔案中

在資料來源配置檔案中,你一定要新增@Qualifier這個註解,否則在啟動專案的時候會報錯,因為這個和JPAConfiguration的配置c中的

這兩個例項造成了衝突。會在程式啟動的時候報錯。由於一個bean有多個例項,會產生報錯。那麼你加了 這個註解就不會產生這個問題了。

2、

這是pom中的配置,如果version是2.xxxxx的時候啟動會無法識別我們再JPAConfiguration中配置的entityManagerFactory這個bean。如果修改為1.5.10是沒問題的。這個我也不知道是什麼問題,可能根據hibernate的版本有關係,好像是hibernate5 如果要自定義配置需要進行註冊。沒去深究。如果有哪位大神知道,請評論指點下。

完成以上配置就可以執行起來這個了。同時我也實現了Springboot +mybatis實現主從分離,機制也是一樣。就是資料庫配置略有不同。大家如果需要可以在github上面下載我的原始碼。原始碼可以執行。