springboot實現讀寫分離

weixin_33866037發表於2017-07-28

server:本demo開發工具採用springSTS
前提讀寫分離庫已經搭建好
1.首先新建一個springboot專案。
2.專案新建成功之後,個人習慣在springboot入口寫一個配置檔案類與Application平級。如圖


7009681-4c99199f4d6145a8.png

下面逐一說明一下註解的含義。
@EnableWebMvc 說明啟用了spring mvc
@Configuration 讓spring boot 專案啟動時識別當前配置類(讓spring容器知道這個類是一個xml的配置類)
@ComponentScan 掃描註解
@MapperScan(basePackages = "com.wz.mail.mapper") 掃描dao
3.說明一下spring boot中的配置檔案 application.properties 個人比較喜歡使用 application.yum(好處是比較有層級感)配置檔案中的內容如下

## context-path代表專案名稱 埠 以及超時時間
server:
  context-path: /mail-producer  
  port: 8001
  session:
    timeout: 900     
## Spring配置:
spring: 
  http: 
    encoding:
      charset: UTF-8 
## 序列化將時間預設序列化為該格式的時間;not_null如果有null預設過濾
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
    default-property-inclusion: NON_NULL      

##此處採用druid資料來源 主從配置基本一樣 master slave   資料庫ip要區分            
druid: 
    type: com.alibaba.druid.pool.DruidDataSource
    master:
        url: jdbc:mysql://localhost/mail?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
        initialSize: 5
        minIdle: 1
        #maxIdle: 10
        maxActive: 100
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall,log4j
        useGlobalDataSourceStat: true
    slave: 
        url: jdbc:mysql://localhost:3306/mail?characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
        initialSize: 5
        minIdle: 1
        #maxIdle: 10
        maxActive: 100
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall,log4j
        useGlobalDataSourceStat: true
 ##指定mybatis的配置檔案      
mybatis:
    mapper-locations: classpath:com/wz/mail/mapping/*.xml

4.現在我們配置了兩個資料來源 ,再啟動專案的時候得把這兩個資料來源都載入進來
(1)需要把這兩個資料來源先注入進來

package com.wz.mail.config;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

@Configuration//上邊有介紹
@EnableTransactionManagement //開啟事物spring提供的註解
public class DataSourceConfiguration {
    
    private static Logger LOGGER = LoggerFactory.getLogger(DataSourceConfiguration.class);

    //預設去找application.yum中的druid.type相當於將配置檔案中的該值賦值給dataSourceType
    @Value("${druid.type}")
    private Class<? extends DataSource> dataSourceType;
    
 
    @Bean(name = "masterDataSource")
    @Primary//優先選擇主資料來源(原因可寫可讀)
    @ConfigurationProperties(prefix = "druid.master") //意思是從application.yum中找druid.master開頭所有的資訊都要放到要建立的masterDataSource並且交給spring管理
    public DataSource masterDataSource() throws SQLException{
        DataSource masterDataSource = DataSourceBuilder.create().type(dataSourceType).build();
        LOGGER.info("========MASTER: {}=========", masterDataSource);
        return masterDataSource;
    }
 
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "druid.slave")
    public DataSource slaveDataSource(){
        DataSource slaveDataSource = DataSourceBuilder.create().type(dataSourceType).build();
        LOGGER.info("========SLAVE: {}=========", slaveDataSource);
        return slaveDataSource;
    }
  
    //druid監控介面需要用的到servlet
    @Bean
    public ServletRegistrationBean druidServlet() {
    
        ServletRegistrationBean reg = new ServletRegistrationBean();
        reg.setServlet(new StatViewServlet());
        reg.addUrlMappings("/druid/*");
        reg.addInitParameter("allow", "localhost");
        reg.addInitParameter("deny","/deny");
        LOGGER.info(" druid console manager init : {} ", reg);
        return reg;
  }

    @Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico, /druid/*");
        LOGGER.info(" druid filter register : {} ", filterRegistrationBean);
        return filterRegistrationBean;
    }
  
}

現在該啟動專案了只要出現兩個資料來源中的log說明資料來源啟動成功日誌如下:

2017-07-27 22:35:06.844  INFO 14424 --- [           main] c.w.mail.config.DataSourceConfiguration  : ========MASTER: {
    CreateTime:"2017-07-27 22:35:06",
    ActiveCount:0,
    PoolingCount:0,
    CreateCount:0,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:0,
    Connections:[
    ]
}=========
2017-07-27 22:35:07.178  INFO 14424 --- [           main] c.w.mail.config.DataSourceConfiguration  : ========SLAVE: {
    CreateTime:"2017-07-27 22:35:07",
    ActiveCount:0,
    PoolingCount:0,
    CreateCount:0,
    DestroyCount:0,
    CloseCount:0,
    ConnectCount:0,
    Connections:[
    ]
}=========

瀏覽器輸入http://localhost:8001/mail-producer/druid
成功訪問的druid監控臺

7009681-c3b8d67b2fab4b3d.png
Paste_Image.png

接下來該mybatis來整合資料來源,經典的SqlSessionFactory ,將這兩個資料來源交給SqlSessionFactory 來管理。然後怎麼區分哪個是主資料來源還是從資料來源呢?

首先實現讀寫分離就意味著有兩個資料來源,當寫操作時對主庫使用,當讀操作時對從庫使用。也就是說我們再啟動資料庫連線池時要啟動兩個。
但我們在真正使用的時候,可以在方法上加自定義註解的形式來區分讀還是寫。
思路:
首先配置兩個資料來源後(已經配置如上)要區分兩個資料來源。分別是主資料來源和從資料來源。
可以通過mybatis配置檔案把兩個資料來源注入到應用中。但是我們要想實現讀寫分離,也就
是什麼情況下用寫,什麼情況下用讀,這裡需要自己定義一個標識來區分。要實現一個即時
切換主從資料來源的標識並且能保證執行緒安全的基礎下運算元據源(原因是併發會影響資料來源
的獲取分不清主從,造成在從庫進行寫操作,影響mysql(mariadb)資料庫的機制,導致
伺服器異常。這裡使用threadocal來解決這個問題)
然後需要自定義註解,在方法上有註解則為只讀,沒有則為寫操作

package com.bhz.mail.config.database;

public class DataBaseContextHolder {

    //區分主從資料來源
    public enum DataBaseType {
        MASTER, SLAVE
    }
    //執行緒區域性變數
    private static final ThreadLocal<DataBaseType> contextHolder = new ThreadLocal<DataBaseType>();
    
    //往執行緒裡邊set資料型別
    public static void setDataBaseType(DataBaseType dataBaseType) {
        if(dataBaseType == null) throw new NullPointerException();
        contextHolder.set(dataBaseType);
    }
    
    //從容器中獲取資料型別
    public static DataBaseType getDataBaseType(){
        return contextHolder.get() == null ? DataBaseType.MASTER : contextHolder.get();
    }
    //清空容器中的資料型別
    public static void clearDataBaseType(){
        contextHolder.remove();
    }
    
}

將這兩種資料來源交給SqlSessionFactory 來管理。接下來寫一個mybatis的配置類相當於傳統的mybatis.xml
先配置資料來源,在注入到SqlSessionFactory (強依賴關係有先有後)
怎樣確保mybatis配置類中先載入資料來源在注入SqlSessionFactory 呢?程式碼如下:

package com.wz.mail.config;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.aspectj.apache.bcel.util.ClassLoaderRepository;
import org.aspectj.apache.bcel.util.ClassLoaderRepository.SoftHashMap;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 
 * @author wz
 *
 */
@Configuration
@AutoConfigureAfter({DataSourceConfiguration.class})//這個檔案在DataSourceConfiguration載入完成之後再載入MybatisConfiguration 
public class MybatisConfiguration extends MybatisAutoConfiguration {

    @Resource(name="masterDataSource")
    private DataSource masterDataSource;
    
    @Resource(name="slaveDataSource")
    private DataSource slaveDataSource;
    
    @Bean(name="sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        //放入datasource 需要mybatis的AbstractRoutingDataSource 實現主從切換
        return super.sqlSessionFactory(roundRobinDataSourceProxy());
    }
    
    public AbstractRoutingDataSource roundRobinDataSourceProxy(){
        
        ReadWriteSplitRoutingDataSource proxy = new ReadWriteSplitRoutingDataSource();
        //proxy.
        SoftHashMap targetDataSource = new ClassLoaderRepository.SoftHashMap(); 
        targetDataSource.put(DataBaseContextHolder.DataBaseType.MASTER, masterDataSource);
        targetDataSource.put(DataBaseContextHolder.DataBaseType.SLAVE, slaveDataSource);
        //預設資料來源
        proxy.setDefaultTargetDataSource(masterDataSource);
        //裝入兩個主從資料來源
        proxy.setTargetDataSources(targetDataSource);
        return proxy;
    }
    
}
package com.wz.mail.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
//mybatis動態代理類
class ReadWriteSplitRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataBaseContextHolder.getDataBaseType();
    }

}

自定義只讀註解,含義就是將預設的主資料來源修改為只讀資料來源

package com.bhz.mail.config.database;

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 ReadOnlyConnection {

}

package com.wz.mail.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ReadOnlyConnectionInterceptor implements Ordered {

    public static final Logger LOGGER = LoggerFactory.getLogger(ReadOnlyConnectionInterceptor.class);
    
    @Around("@annotation(readOnlyConnection)")//在註解上加入切入點語法,實現方法
    public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ReadOnlyConnection readOnlyConnection) throws Throwable {
        try{
            LOGGER.info("---------------set database connection  read only---------------");
            DataBaseContextHolder.setDataBaseType(DataBaseContextHolder.DataBaseType.SLAVE);
            Object result = proceedingJoinPoint.proceed();//讓這個方法執行完畢
            return result;
        } finally {
            DataBaseContextHolder.clearDataBaseType();
            LOGGER.info("---------------clear database connection---------------");
        }
    }
    
    @Override
    public int getOrder() {
        return 0;
    }

}

程式碼已經OK,將註解寫到只讀方法上。@ReadOnlyConnection
開始測試begin 日誌列印如下

2017-07-30 21:35:13.499  INFO 8604 --- [nio-8001-exec-1] c.w.m.c.ReadOnlyConnectionInterceptor    : ---------------set database connection 2 read only---------------
2017-07-30 21:35:13.735  INFO 8604 --- [nio-8001-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
2017-07-30 21:35:13.761  INFO 8604 --- [nio-8001-exec-1] c.w.m.c.ReadOnlyConnectionInterceptor    : ---------------clear database connection---------------
測試總共多少條:2

由日誌可以看出使用的是隻讀資料來源並且使用之後清空容器裡的資料來源。

相關文章