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上面下載我的原始碼。原始碼可以執行。