動手實現MySQL讀寫分離and故障轉移

前言

久違了,因爲最近新項目下來了,因此工做特別忙,致使遲遲沒更,上一篇發了手動搭建Redis集羣和MySQL主從同步(非Docker)以後,不少同窗對文中主從結構提到的讀寫分離感興趣,本打算在雙十一期間直接把讀寫分離分享給你們,奈何工做一直沒停下,因此這個週末抽空把這些分享出來。php

關於MySQL的讀寫分離的實現,有兩種方式,第一種方式即咱們手動在代碼層實現邏輯,來解析讀請求或者寫請求,分別分發到不一樣的數據庫中,實現讀寫分離;第二種方式就是基於MyCat中間件來實現讀寫分離的效果;這兩種方式我都會在這篇博客中進行詳細地介紹、搭建,而且分析其中的優劣。java

原理初探

從MySQL的主從同步開始談起,最開始咱們的數據庫架構是這樣的。mysql

主從架構

主庫負責了全部的讀寫操做,而從庫只對主庫進行了備份,就像我在上一篇文章中說的那樣,我認爲若是隻實現了一個備份,不能讀寫分離和故障轉移,不能下降Master節點的IO壓力,這樣的主從架構看起來性價比彷佛不是很高。web

咱們所但願的主從架構是,當咱們在寫數據時,請求所有發到Master節點上,當咱們須要讀數據時,請求所有發到Slave節點上。而且多個Slave節點最好能夠存在負載均衡,讓集羣的效率最大化。正則表達式

那麼這樣的架構就不夠咱們使用了,咱們須要找尋某種方式,來實現讀寫分離。那麼實際上有兩種方式。spring

  • 方法1:代碼層實現讀寫分離

    這種方法的優點就是比較靈活,咱們能夠按照本身的邏輯來決定讀寫分離的規則。若是使用了這樣的方法,咱們整個數據庫的架構就能夠用下面這張圖進行歸納:sql

    代碼層實現讀寫分離

  • 方法2:使用中間層(虛擬節點)進行請求的轉發

    這種方式最主要的特色就是咱們在除了數據庫之外地方,新構建了一個虛擬節點,而咱們全部的請求都發到這個虛擬節點上,由這個虛擬節點來轉發讀寫請求該相應的數據庫。數據庫

    這種方式的特色就是,其構建了一個獨立的節點來接收全部的請求,而不用在咱們的程序中配置多數據源,咱們的項目只須要將url指向這個虛擬節點,而後由這個虛擬節點來處理讀寫請求。不是有這麼一句話嗎,專業的事交給專業的人來作,大概是這麼個意思吧。而如今存在的MyCat等中間件,就是這樣的一個」專業的人「。apache

    使用虛擬節點讀寫分離

那麼下面我就會動手實現上述兩個讀寫分離的解決方案,代碼層實現讀寫分離使用中間件實現讀寫分離緩存

手動實現讀寫分離

實現讀寫分離的方法有不少,我這裏會說到兩種,第一種是使用MyBatis和Spring,手寫MyBatis攔截器來判斷SQL是讀或者寫,從而選擇數據源,最後交給Spring注入數據源,來實現讀寫分離;第二種是使用MyCat中間件,配置化地實現讀寫分離,每種方式都有其可取之處,能夠本身視狀況選用。

  • 環境說明

    這裏用到了個人上篇博客手動搭建Redis集羣和MySQL主從同步(非Docker)中所搭建的MySQL主從同步,若是手上沒有這套環境的,能夠先比着這篇博客進行搭建。可是須要注意的是,要將8.0版本的MySQL改成5.7。

    192.168.43.201:3306 Master

    192.168.43.202:3306 Slave

    開發環境:

    IDE:Eclipse

    Spring boot 2.1.7

    MySQL 5.7

    CentOS 7.3

  • 新建Maven項目

爲了演示方便,這裏使用SpringBoot做爲測試的基礎框架,省去了不少Spring須要的xml配置。沒有用過SpringBoot的同窗也不要緊,我會一步一步地進行演示操做。

  • 導入依賴

    <parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>2.1.7.RELEASE</version>
    		<relativePath /> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
    		<!-- Web相關 -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<!-- 數據庫相關 -->
    		<dependency>
    			<groupId>com.oracle</groupId>
    			<artifactId>ojdbc7</artifactId>
    			<version>12.1.0</version>
    		</dependency>
    		<dependency>
    			<groupId>org.mybatis.spring.boot</groupId>
    			<artifactId>mybatis-spring-boot-starter</artifactId>
    			<version>2.0.0</version>
    		</dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    		<!-- 測試相關依賴 -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-test</artifactId>
    		</dependency>
        	<!-- -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>
    				spring-boot-configuration-processor
    			</artifactId>
    			<optional>true</optional>
    		</dependency>
    </dependencies>
    複製代碼
  • application.yml

    爲了測試項目儘可能簡單,因此咱們不用去過多地配置其它東西。只有一些基本配置和數據源配置。

    server: 
     port: 10001
    spring: 
     datasource:
     url: jdbc:mysql://192.168.43.201:3306/springtestdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
     username: Object
     password: Object971103.
     driver-class-name: com.mysql.cj.jdbc.Driver
       
    #MyBatis配置
    mybatis:
     mapper-locations: classpath:mapper/*.xml
     configuration:
     map-underscore-to-camel-case: true
    複製代碼
  • 編寫啓動類

    @SpringBootApplication
    public class ApplicationStarter {
    	public static void main(String[] args) {
    		SpringApplication.run(ApplicationStarter.class, args);
    	}
    }
    複製代碼
  • 啓動

    啓動

    出現以上信息表明啓動成功。嗯......這應該是一個數據庫相關的博客,好像講了太多的SpringBoot

    到這裏說明咱們的SpringBoot項目沒有問題,已經搭建成功,若是還不放心,能夠自行訪問一下http://localhost:10001這個路徑,若是出現SpringBoot的404,則表明啓動成功。

  • 新建Student實體並建立數據庫

    package cn.objectspace.springtestdemo.domain;
    public class Student {
    	private String studentId;
    	private String studentName;
    	public String getStudentId() {
    		return studentId;
    	}
    	public void setStudentId(String studentId) {
    		this.studentId = studentId;
    	}
    	public String getStudentName() {
    		return studentName;
    	}
    	public void setStudentName(String studentName) {
    		this.studentName = studentName;
    	}	
    }
    複製代碼
    CREATE TABLE student(
    	student_id VARCHAR(32),
        student_name VARCHAR(32)
    );
    複製代碼
  • 編寫StudentDao接口,並進行測試

    接口:

    package cn.objectspace.springtestdemo.dao;
    
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    
    import cn.objectspace.springtestdemo.domain.Student;
    
    @Mapper
    public interface StudentDao {
    	@Insert("INSERT INTO student(student_id,student_name)VALUES(#{studentId},#{studentName})")
    	public Integer insertStudent(Student student);
    	@Select("SELECT * FROM student WHERE student_id = #{studentId}")
    	public Student queryStudentByStudentId(Student student);
    }
    複製代碼

    測試類:

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {ApplicationStarter.class})// 指定啓動類
    public class DaoTest {
    	@Autowired StudentDao studentDao;
    	@Test
    	public void test01() {
    		Student student = new Student();
    		student.setStudentId("20191130");
    		student.setStudentName("Object6");
    		studentDao.insertStudent(student);
    		studentDao.queryStudentByStudentId(student);
    	}
    }
    複製代碼

    若是能夠正確往數據庫中插入數據,以下圖,則MyBatis搭建成功。

    MyBatis搭建成功

  • 正式搭建

    經過上面的準備工做,咱們已經能夠實現對數據庫的讀寫,可是並無實現讀寫分離,如今纔是開始實現數據庫的讀寫分離。

  • 修改application.yml

    剛纔咱們的配置文件中只有單數據源,而讀寫分離確定不會是單數據源,因此咱們首先要在application.yml中配置多數據源。

    server: 
     port: 10001
    spring: 
     datasource:
     master: 
     url: jdbc:mysql://192.168.43.201:3306/springtestdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
     username: Object
     password: Object971103.
     driver-class-name: com.mysql.cj.jdbc.Driver
     slave: 
     url: jdbc:mysql://192.168.43.202:3306/springtestdemo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
     username: Object
     password: Object971103.
     driver-class-name: com.mysql.cj.jdbc.Driver
        
        
    #MyBatis配置
    mybatis:
     mapper-locations: classpath:mapper/*.xml
     configuration:
     cache-enabled: true #開啓二級緩存
     map-underscore-to-camel-case: true
    複製代碼
  • DataSource的配置

    首先要先建立兩個ConfigurationProperties類,這一步不是非必須的,直接配置DataSource也是能夠的,可是我仍是比較習慣去寫這個Properties。

    • MasterProperpties

      package cn.objectspace.springtestdemo.config;
      
      import org.springframework.boot.context.properties.ConfigurationProperties;
      import org.springframework.stereotype.Component;
      
      @ConfigurationProperties(prefix = "spring.datasource.master")
      @Component
      public class MasterProperties {
      	private String url;
      	private String username;
      	private String password;
      	private String driverClassName;
      	public String getUrl() {
      		return url;
      	}
      	public void setUrl(String url) {
      		this.url = url;
      	}
      	public String getUsername() {
      		return username;
      	}
      	public void setUsername(String username) {
      		this.username = username;
      	}
      	public String getPassword() {
      		return password;
      	}
      	public void setPassword(String password) {
      		this.password = password;
      	}
      	public String getDriverClassName() {
      		return driverClassName;
      	}
      	public void setDriverClassName(String driverClassName) {
      		this.driverClassName = driverClassName;
      	}
      	
      }
      複製代碼
    • SlaveProperties

      package cn.objectspace.springtestdemo.config;
      
      import org.springframework.boot.context.properties.ConfigurationProperties;
      import org.springframework.stereotype.Component;
      
      @ConfigurationProperties(prefix = "spring.datasource.slave")
      @Component
      public class SlaveProperties {
      	private String url;
      	private String username;
      	private String password;
      	private String driverClassName;
      	public String getUrl() {
      		return url;
      	}
      	public void setUrl(String url) {
      		this.url = url;
      	}
      	public String getUsername() {
      		return username;
      	}
      	public void setUsername(String username) {
      		this.username = username;
      	}
      	public String getPassword() {
      		return password;
      	}
      	public void setPassword(String password) {
      		this.password = password;
      	}
      	public String getDriverClassName() {
      		return driverClassName;
      	}
      	public void setDriverClassName(String driverClassName) {
      		this.driverClassName = driverClassName;
      	}
      	
      }
      複製代碼
    • DataSourceConfig

      這個配置主要是對主從數據源進行配置。

      @Configuration
      public class DataSourceConfig {
      	private Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
          @Autowired
          private MasterProperties masterProperties;
      
          @Autowired
          private SlaveProperties slaveProperties;
      
          //默認是master數據源
          @Bean(name = "masterDataSource")
          @Primary
          public DataSource masterProperties(){
              logger.info("masterDataSource初始化");
              HikariDataSource dataSource = new HikariDataSource();
              dataSource.setJdbcUrl(masterProperties.getUrl());
              dataSource.setUsername(masterProperties.getUsername());
              dataSource.setPassword(masterProperties.getPassword());
              dataSource.setDriverClassName(masterProperties.getDriverClassName());
              return dataSource;
          }
      
          @Bean(name = "slaveDataSource")
          public DataSource dataBase2DataSource(){
              logger.info("slaveDataSource初始化");
              HikariDataSource dataSource = new HikariDataSource();
              dataSource.setJdbcUrl(slaveProperties.getUrl());
              dataSource.setUsername(slaveProperties.getUsername());
              dataSource.setPassword(slaveProperties.getPassword());
              dataSource.setDriverClassName(slaveProperties.getDriverClassName());
              return dataSource;
          }
      }
      複製代碼
  • 動態數據源的切換

    這裏使用到的主要是Spring提供的AbstractRoutingDataSource,其提供了動態數據源的功能,能夠幫助咱們實現讀寫分離。其determineCurrentLookupKey()能夠決定最終使用哪一個數據源,這裏咱們本身建立了一個DynamicDataSourceHolder,來給他傳一個數據源的類型(主、從)。

    package cn.objectspace.springtestdemo.dao.split;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    /** * * @Description: spring提供了AbstractRoutingDataSource,提供了動態選擇數據源的功能,替換原有的單一數據源後,便可實現讀寫分離: * @Author: Object * @Date: 2019年11月30日 */
    public class DynamicDataSource extends AbstractRoutingDataSource{
        //注入主從數據源
    	@Resource(name="masterDataSource")
    	private DataSource masterDataSource;
    	@Resource(name="slaveDataSource")
    	private DataSource slaveDataSource;
    	@Override
        public void afterPropertiesSet() {
            setDefaultTargetDataSource(masterDataSource);
            Map<Object, Object> dataSourceMap = new HashMap<>();
            //將兩個數據源set入目標數據源
            dataSourceMap.put("master", masterDataSource);
            dataSourceMap.put("slave", slaveDataSource);
            setTargetDataSources(dataSourceMap);
    
            super.afterPropertiesSet();
        }
    	@Override
    	protected Object determineCurrentLookupKey() {
            //肯定最終的目標數據源
    		return DynamicDataSourceHolder.getDbType();
    	}
    }
    複製代碼
  • DynamicDataSourceHolder的實現

    這個類由咱們本身實現,主要是提供給Spring咱們須要用到的數據源類型。

    package cn.objectspace.springtestdemo.dao.split;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /** * @Description: 獲取DataSource * @Author: Object * @Date: 2019年11月30日 */
    public class DynamicDataSourceHolder {
    	private static Logger logger = LoggerFactory.getLogger(DynamicDataSourceHolder.class);
    	private static ThreadLocal<String> contextHolder = new ThreadLocal<>();
    	public static final String DB_MASTER = "master";
    	public static final String DB_SLAVE="slave";
    	/** * @Description: 獲取線程的DbType * @Param: args * @return: String * @Author: Object * @Date: 2019年11月30日 */
    	public static String getDbType() {
    		String db = contextHolder.get();
    		if(db==null) {
    			db = "master";
    		}
    		return db;
    	}
    	/** * @Description: 設置線程的DbType * @Param: args * @return: void * @Author: Object * @Date: 2019年11月30日 */
    	public static void setDbType(String str) {
    		logger.info("所使用的數據源爲:"+str);
    		contextHolder.set(str);
    	}
    	
    	/** * @Description: 清理鏈接類型 * @Param: args * @return: void * @Author: Object * @Date: 2019年11月30日 */
    	public static void clearDbType() {
    		contextHolder.remove();
    	}
    }
    複製代碼
  • MyBatis攔截器的實現

    最後就是咱們實現讀寫分離的核心了,這個類能夠對SQL進行判斷,是讀SQL仍是寫SQL,從而進行數據源的選擇,最終調用DynamicDataSourceHolder的setDbType方法,將數據源類型傳入。

    package cn.objectspace.springtestdemo.dao.split;
    
    import java.util.Locale;
    import java.util.Properties;
    
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
    import org.apache.ibatis.mapping.BoundSql;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.SqlCommandType;
    import org.apache.ibatis.plugin.Interceptor;
    import org.apache.ibatis.plugin.Intercepts;
    import org.apache.ibatis.plugin.Invocation;
    import org.apache.ibatis.plugin.Plugin;
    import org.apache.ibatis.plugin.Signature;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import org.springframework.transaction.support.TransactionSynchronizationManager;
    
    /** * @Description: MyBatis級別攔截器,根據SQL信息,選擇不一樣的數據源 * @Author: Object * @Date: 2019年11月30日 */
    @Intercepts({ 
    	@Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
    	@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class })
    	})
    @Component
    public class DynamicDataSourceInterceptor implements Interceptor {
    	private Logger logger = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class);
    	// 驗證是否爲寫SQL的正則表達式
    	private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
    
    	/** * 主要的攔截方法 */
    	@Override
    	public Object intercept(Invocation invocation) throws Throwable {
    		// 判斷當前是否被事務管理
    		boolean synchronizationActive = TransactionSynchronizationManager.isActualTransactionActive();
    		String lookupKey = DynamicDataSourceHolder.DB_MASTER;
    		if (!synchronizationActive) {
                //若是是非事務的,則再判斷是讀或者寫。
    			// 獲取SQL中的參數
    			Object[] objects = invocation.getArgs();
    			// object[0]會攜帶增刪改查的信息,能夠判斷是讀或者是寫
    			MappedStatement ms = (MappedStatement) objects[0];
    			// 若是爲讀,且爲自增id查詢主鍵,則使用主庫
    			// 這種判斷主要用於插入時返回ID的操做,因爲日誌同步到從庫有延時
    			// 因此若是插入時須要返回id,則不適用於到從庫查詢數據,有可能查詢不到
    			if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)
    					&& ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
    				lookupKey = DynamicDataSourceHolder.DB_MASTER;
    			} else {
    				BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
    				String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
    				// 正則驗證
    				if (sql.matches(REGEX)) {
    					// 若是是寫語句
    					lookupKey = DynamicDataSourceHolder.DB_MASTER;
    				} else {
    					lookupKey = DynamicDataSourceHolder.DB_SLAVE;
    				}
    			}
    		} else {
    			// 若是是經過事務管理的,通常都是寫語句,直接經過主庫
    			lookupKey = DynamicDataSourceHolder.DB_MASTER;
    		}
    
    		logger.info("在" + lookupKey + "中進行操做");
    		DynamicDataSourceHolder.setDbType(lookupKey);
    		// 最後直接執行SQL
    		return invocation.proceed();
    	}
    
    	/** * 返回封裝好的對象,或代理對象 */
    	@Override
    	public Object plugin(Object target) {
    		// 若是存在增刪改查,則直接攔截下來,不然直接返回
    		if (target instanceof Executor)
    			return Plugin.wrap(target, this);
    		else
    			return target;
    	}
    
    	/** * 類初始化的時候作一些相關的設置 */
    	@Override
    	public void setProperties(Properties properties) {
    		// TODO Auto-generated method stub
    
    	}
    
    }
    複製代碼
  • 代碼梳理

    經過上文中的程序,咱們已經能夠實現讀寫分離了,可是這麼看着仍是挺亂的。因此在這裏從新梳理一遍上文中的代碼。

    其實邏輯並不難:

    1. 經過@Configuration實現多數據源的配置。
    2. 經過MyBatis的攔截器,DynamicDataSourceInterceptor來判斷某條SQL語句是讀仍是寫,若是是讀,則調用DynamicDataSourceHolder.setDbType("slave"),不然調用DynamicDataSourceHolder.setDbType("master")。
    3. 經過AbstractRoutingDataSource的determineCurrentLookupKey()方法,返回DynamicDataSourceHolder.getDbType();也就是咱們在攔截器中設置的數據源。
    4. 對注入的數據源執行SQL。
  • 測試

    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = {ApplicationStarter.class})// 指定啓動類
    public class DaoTest {
    	@Autowired StudentDao studentDao;
    	@Test
    	public void test01() {
    		Student student = new Student();
    		student.setStudentId("20191130");
    		student.setStudentName("Object6");
    		studentDao.insertStudent(student);
    		studentDao.queryStudentByStudentId(student);
    	}
    }
    複製代碼

    測試結果:

    代碼層測試成功

    至此,代碼層讀寫分離已完整地實現。

基於MyCat中間件實現讀寫分離、故障轉移

  • 簡介

    在上文中咱們已經實現了使用手寫代碼的方式對數據庫進行讀寫分離,可是不知道你們發現了沒有,我只使用了一主一從。那麼爲何我有一主二從的環境卻只實現一主一從的讀寫分離呢?由於,在代碼層實現一主多從的讀寫分離我也不會寫。那麼假設數據庫集羣不止於一主二從,而是一主三從,一主四從,多主多從呢?若是Master節點宕機了,又該怎麼處理

    每次動態增長一個節點,咱們就要從新修改咱們的代碼,這不但會給開發人員形成很大的負擔,並且不符合開閉原則。

    因此接下來的MyCat應該能夠解決這樣的問題。而且我會直接使用一主二從的環境演示。

  • MyCat介紹

    這裏直接套官方文檔。

    一個完全開源的,面向企業應用開發的大數據庫集羣

    支持事務、ACID、能夠替代MySQL的增強版數據庫

    一個能夠視爲MySQL集羣的企業級數據庫,用來替代昂貴的Oracle集羣

    一個融合內存緩存技術、NoSQL技術、HDFS大數據的新型SQL Server

    結合傳統數據庫和新型分佈式數據倉庫的新一代企業級數據庫產品

    一個新穎的數據庫中間件產品

  • 環境說明

    MyCat 192.168.43.90

    MySQL master 192.168.43.201

    MySQL slave1 192.168.43.202

    MySQL slave2 192.168.43.203

    接上篇博客的MySQL數據庫一主二從,不過MySQL版本須要從8.0改成5.7,不然會出現密碼問題沒法鏈接。

    另外,咱們須要在每一個數據庫中都爲MyCat建立一個帳號並賦上權限

    CREATE USER  'user_name'@'host'  IDENTIFIED BY  'password';
    GRANT privileges ON  databasename.tablename  TO  ‘username’@‘host’;
    --可使用下面這句 賦予全部權限
    GRANT ALL PRIVILEGES ON *.* TO  ‘username’@‘host’;
    --最後刷新權限
    FLUSH PRIVILEGES;
    複製代碼

    在開始以前,先保證主從庫的搭建是成功的:

    一主二從成功

    如何安裝MyCat在這裏我就不說了,百度上有不少帖子有,按照上面的教程一步一步來其實沒有多大問題。咱們着重說說和咱們MyCat配置相關的兩個配置文件——schema.xml和server.xml,固然還有一個rules.xml,可是這裏暫時不介紹分庫分表,因此這個暫且不提。

  • 配置文件說明

    1. server.xml

      打開mycat安裝目錄下的/conf/server.xml文件,這個配置文件比較長,看着比較費腦,但其實對於初學者來講,咱們須要配置的地方並很少,因此不用太懼怕這種長篇幅的配置文件。(其實在上一篇文章的結尾,我也說過,面對一個新技術的時候首先不能懵逼,一步一步地去分析並接受他,扯遠了)配置文件簡化以後大概是這樣的一個結構。

      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE mycat:server SYSTEM "server.dtd">
      <mycat:server xmlns:mycat="http://io.mycat/">
          <system>
          </system>
          <user name="MyCat" defaultAccount="true">
          </user>
      
      </mycat:server>
      
      複製代碼

      這樣看起來是否是簡單多了,其實對於Server.xml,咱們主要配置的就是下面的user模塊,咱們把它展開,着重講講這部分的配置。

      <user name="這裏寫MyCat的用戶名 能夠自定義" defaultAccount="true">
         <property name="password">這裏寫MyCat的密碼</property>
         <property name="schemas">這裏配置MyCat的虛擬database</property>
                  <!-- 表級 DML 權限設置 -->
                  <!-- 這裏是咱們配置的mycat用戶對某張表的權限配置,咱們這裏暫不配置可是仍是說一 下。下文中的0000 1111 每一位 表明CRUD 1111就是有增刪改查的權限,0000就 是沒有這些權限。以此類推 <privileges check="false"> <schema name="TESTDB" dml="0110" > <table name="tb01" dml="0000"></table> <table name="tb02" dml="1111"></table> </schema> </privileges> -->
      </user>
      複製代碼

      user表明MyCat的用戶,咱們在使用MySQL的時候都會有一個用戶,MyCat做爲一個虛擬節點,咱們能夠把它想象成它就是一個MySQL,因此天然而然它也須要有一個用戶。可是他的用戶並非咱們用命令建立的,而是直接在配置文件中配置好的,咱們以後登陸MyCat,就是用這裏的用戶名和密碼進行登陸。至於如何配置,我在上面的配置中都寫好啦。跟着作就沒有問題。

    2. schema.xml

      打開MyCat安裝目錄的conf/schema.xml,這個配置文件是咱們須要關注的一個配置文件,由於咱們的讀寫分離、分庫分表、故障轉移、都配置在這個配置文件中。可是這個配置文件並不長,咱們能夠一點一點慢慢分析。

      首先是標籤中的內容。這個標籤主要是爲MyCat虛擬出一個數據庫,咱們鏈接到MyCat上能看到的數據庫就是這裏配置的,而分庫分表也主要在這個標籤中進行配置。這個標籤中的name屬性,就是爲虛擬數據庫指定一個名字,也是咱們鏈接MyCat看到的數據庫的庫名,dataNode是和下文的dataNode標籤中的name相對應的,表明這個虛擬的數據庫和下面的dataNode進行綁定。

      <schema name="MyCatDatabase" checkSQLschema="false" sqlMaxLimit="100" dataNode="這裏寫節點名,須要和dataNode中的name相對應">
      	<!-- 分庫分表 -->
      		<!--<table name="travelrecord" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />-->
      </schema>
      複製代碼

      第二個標籤是標籤,這個標籤是和咱們真實數據庫中的database聯繫起來的,name屬性是咱們對這個dataNode自定義的一個名字,要注意的是,這個名字須要和schema標籤中的dataNode內容一致,database屬性寫的是咱們真實數據庫中的真實database的名字。而dataHost的內容須要和以後標籤中的name屬性的值相對應。

      <dataNode name="這裏寫節點名,須要和schema中的dataNode相對應" dataHost="這裏也是一個自定義名字,須要和dataHost中的name相對應" database="這裏填MySQL真實的數據庫名" />
      複製代碼

      第三個標籤要說的是標籤,這個標籤是和咱們真實數據庫的主從、讀寫分離聯繫起來的標籤,什麼意思呢。這個標籤中有這麼兩個子標籤和分別表明咱們的寫庫讀庫,中配置的庫能夠用於讀或者寫,而中配置的庫只能用於讀。

      能夠看到schema.xml的配置是一環扣一環的,每一個標籤之間都有相互進行聯繫的屬性。咱們最後配置完的schema.xml應該長下面這個樣子:

      <?xml version="1.0"?>
      <!DOCTYPE mycat:schema SYSTEM "schema.dtd">
      <mycat:schema xmlns:mycat="http://io.mycat/">
          <schema name="這裏寫虛擬database名,須要和server.xml中的schema相對應" checkSQLschema="false" sqlMaxLimit="100" dataNode="這裏寫節點名,須要和dataNode中的name相對應">
      	<!-- 分庫分表 -->
      		<!--<table name="travelrecord" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />-->
          </schema>
      		<dataNode name="這裏寫節點名,須要和schema中的dataNode相對應" dataHost="這裏也是一個自定義名字,須要和dataHost中的name相對應" database="這裏填MySQL真實的數據庫名" />
              <dataHost name="這裏寫和dataNode中的dataHost相同的名字" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
                  <!-- 心跳語句,證實myCat和mySQL是相互鏈接的狀態-->
                  <heartbeat>show slave status</heartbeat>
      			<!-- 讀寫分離 -->
                  <writeHost host="節點的名字,隨便取" url="數據庫的url(IP:PORT)" user="數據庫中給MyCat建立的用戶名" password="數據庫中給MyCat建立的密碼">
      				<readHost host="節點的名字,隨便取" url="數據庫的url(IP:PORT)" user="數據庫中給MyCat建立的用戶名" password="數據庫中給MyCat建立的密碼">
      				</readHost>
      				<readHost host="節點的名字,隨便取" url="數據庫的url(IP:PORT)" user="數據庫中給MyCat建立的用戶名" password="數據庫中給MyCat建立的密碼">
      				</readHost>
                  </writeHost>
      			<!-- 主從切換 -->
      			<writeHost host="節點的名字,隨便取" url="數據庫的url(IP:PORT)" user="數據庫中給MyCat建立的用戶名" password="數據庫中給MyCat建立的密碼"></writeHost>
      			<writeHost host="節點的名字,隨便取" url="數據庫的url(IP:PORT)" user="數據庫中給MyCat建立的用戶名" password="數據庫中給MyCat建立的密碼"></writeHost>
              </dataHost>
      </mycat:schema>
      複製代碼
  • MyCat配置讀寫分離

    上文中咱們對MyCat的兩個配置文件進行了基本的解讀,那麼如今就開始搭建一個基於MyCat的讀寫分離。我這裏有三個數據庫,一主二從,再說一遍環境吧:

    192.168.43.201 master庫

    192.168.43.202 slave庫

    192.168.43.203 slave庫

    那麼對於server.xml和schema.xml的配置以下:

    server.xml:

    <user name="MyCat" defaultAccount="true">
    	<property name="password">123456</property>
    	<property name="schemas">MyCat</property>
    </user>
    複製代碼

    schema.xml:

    <?xml version="1.0"?>
    <!DOCTYPE mycat:schema SYSTEM "schema.dtd">
    <mycat:schema xmlns:mycat="http://io.mycat/">
            <schema name="MyCat" checkSQLschema="false" sqlMaxLimit="100" dataNode="mycatdb"></schema>
        	<!-- testcluster是我真實數據庫中的名字 -->
            <dataNode name="mycatdb" dataHost="mycluster" database="testcluster" />
        	<!-- 開啓讀寫分離必須將balance修改成1-->
            <dataHost name="mycluster" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
                    <heartbeat>show slave status</heartbeat>
    				<!-- 讀寫分離 -->
                    <writeHost host="Master201" url="192.168.43.201:3306" user="MyCat" password="123456">
    					<readHost host="Slave202" url="192.168.43.202:3306" user="MyCat" password="123456">
    					</readHost>
    					<readHost host="Slave203" url="192.168.43.203:3306" user="MyCat" password="123456">
    					</readHost>
                    </writeHost>
            </dataHost>
    </mycat:schema>
    複製代碼

    啓動MyCat並測試:

    啓動MyCat:

    ./mycat start

    鏈接MyCat:

    mysql -u MyCat -h 192.168.43.90 -P 8066 -p

    鏈接MyCat

    能夠正確看到MyCat中存在一個數據庫,名字叫MyCat,而這個數據庫是咱們虛擬出來的,並不真實存在,實際上就是咱們配置的起了做用。

    在MyCat庫中建立一張表

    CREATE TABLE student(student_id VARCHAR(32),student_name VARCHAR(32));

    MyCat建表

    從庫同步

    這樣就能夠證實咱們的mycat鏈接真實數據庫成功。那麼咱們下面就要開始證實讀寫分離,何謂讀寫分離呢?就是讀數據操做從從庫中讀取,而主庫只負責寫操做,下面咱們開始進行驗證

    在驗證以前,咱們須要將MyCat的日誌設置爲debug模式,由於在info模式下,是不能在日誌中顯示SQL語句轉發到哪個數據庫中進行查詢的

    如何設置:

    打開conf/log4j2.xml

    修改MyCat日誌等級

    執行一條插入語句

    INSERT INTO student(student_id,student_name) VALUES('20191130','Object');

    插入數據

    查看日誌:

    查看寫日誌

    能夠看到,INSERT語句是在201中寫入的,201是Master庫,也就是寫庫。

    寫在咱們來執行一條讀語句

    SELECT * FROM student;

    查看讀日誌1

    能夠看到,SELECT 語句是在202中執行的,202是Slave庫,也就是讀庫。

    再執行一次:

    查看讀日誌2

    這個時候讀語句在203中執行,仍是讀庫,這兩個讀庫是基於負載均衡規則來進行讀取的

    這樣就完成了讀寫分離的配置,當咱們須要進行INSERT/UPDATE/DELETE時,會直接到Master中進行寫入,而後同步到Slave庫,而要進行SELECT操做時,就改成去Slave中讀,不影響Master的寫入,這種讀寫分離,拓展了MySQL主從同步的功能,能夠在容災備份的同時,提高數據庫的性能

  • MyCat配置故障轉移

    咱們在上文中已經完成了MyCat關於讀寫分離的配置,那麼咱們大膽假設,假如咱們的Master數據庫忽然宕機了,那麼是否整個集羣就喪失了寫功能呢

    在沒有故障轉移以前,這個答案是確定的,當主庫宕機時,從庫做爲讀庫,是不會有寫的功能的,整個集羣也就喪失了寫的功能,這是咱們不但願看到的。

    咱們但願看到的場景是:當主庫宕機,某一個從庫自動變爲主庫,承擔寫的功能,保證整個集羣的可用性

    那麼咱們開始進行配置,其實思路很簡單,MyCat的標籤中有一個switchType屬性,其決定了切換的條件。

    switchType指的是切換的模式,目前的取值也有4種:

    1. switchType='-1' 表示不自動切換

    2. switchType='1' 默認值,表示自動切換

    3. switchType='2' 基於MySQL主從同步的狀態決定是否切換,心跳語句爲 show slave status

    4. switchType='3'基於MySQL galary cluster的切換機制(適合集羣)(1.4.1),心跳語句爲 show status like 'wsrep%'。

    咱們直接將switchType修改成2,而後將兩個讀庫配置爲第一個寫庫同級的寫庫。

    配置文件以下:

    <?xml version="1.0"?>
    <!DOCTYPE mycat:schema SYSTEM "schema.dtd">
    <mycat:schema xmlns:mycat="http://io.mycat/">
        <schema name="MyCat" checkSQLschema="false" sqlMaxLimit="100" dataNode="mycatdb">
        </schema>
        <dataNode name="mycatdb" dataHost="mycluster" database="testcluster" />
        <dataHost name="mycluster" maxCon="1000" minCon="10" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
                <heartbeat>show slave status</heartbeat>
    			<!-- 讀寫分離 -->
                <writeHost host="Master201" url="192.168.43.201:3306" user="MyCat" password="123456">
    				<readHost host="Slave202" url="192.168.43.202:3306" user="MyCat" password="123456">
    				</readHost>
    				<readHost host="Slave203" url="192.168.43.203:3306" user="MyCat" password="123456">
    				</readHost>
                </writeHost>
    			<!-- 主從切換 -->
    			<writeHost host="Slave202" url="192.168.43.202:3306" user="MyCat" password="123456"></writeHost>
    			<writeHost host="Slave203" url="192.168.43.203:3306" user="MyCat" password="123456"></writeHost>
            </dataHost>
    </mycat:schema>
    複製代碼

    重啓MyCat

    如今咱們來停掉Master庫,而後執行寫操做,看看是什麼結果。

    service mysqld stop

    MyCat日誌:

    模擬主庫宕機

    故障轉移日誌

    執行INSERT INTO student(student_id,student_name)VALUES('test','testdown');

    MyCat日誌

    主庫宕機後寫入
    能夠看到,如今當咱們執行完這個語句時,他自動切換到202數據庫進行寫入,而202是Slave而非master,這就說明MyCat對寫庫進行了自動切換,咱們的MySQL集羣依舊能夠提供寫的功能。

    固然,此時咱們MySQL的主從架構已經被破壞,若是須要恢復主從結構,就須要手動地從新去恢復咱們的主從架構。咱們須要將201和203做爲Slave,202做爲Master,由於Master擁有最完整的數據。

優劣分析

關於這兩種方式的優劣,相信若是仔細看完這篇文章的同窗都會有一個深入的體會。

代碼層實現讀寫分離,主要的優勢就是靈活,能夠本身根據不一樣的需求對讀寫分離的規則進行定製化開發,但其缺點也十分明顯,就是當咱們動態增減主從庫數量的時候,都須要對代碼進行一個或多或少的修改。而且當主庫宕機了,若是咱們沒有實現相應的容災邏輯,那麼整個數據庫集羣將喪失對外的寫功能。

使用MyCat中間件實現讀寫分離,優勢十分明顯,咱們只須要進行配置就能夠享受讀寫分離帶來的效率的提高,不用寫一行代碼,而且當主庫宕機時,咱們還能夠經過配置的方式進行主從庫的自動切換,這樣即便主庫宕機咱們的整個集羣也不會喪失寫的功能。其缺點可能就是咱們得多付出一臺服務器做爲虛擬節點了吧,畢竟服務器也是須要成本的。

兩種方式如何抉擇:若是你目前的項目比較小,或者乾脆是一個畢業設計、課程設計之類的,不會有動態增減數據庫的需求,那麼本身動手實現一個數據庫的讀寫分離會比較適合你,畢竟答辯的時候,能夠一行一行代碼跟你的導師和同窗解(zhuang)釋(bi)。若是項目比較大了,數據庫節點有可能進行增減,而且須要主從切換之類的功能,那麼就使用第二種方式吧。這種配置化的實現能夠下降次日洗頭時候下水管堵塞的概率。

結語

我之後儘可能不拖更!

歡迎你們訪問個人我的博客:Object's Blog

相關文章
相關標籤/搜索