前言
久違了,由於最近新項目下來了,所以工作特別忙,導致遲遲沒更,上一篇發了手動搭建Redis集羣和MySQL主從同步(非Docker)之後,很多同學對文中主從結構提到的讀寫分離感興趣,本打算在雙十一期間直接把讀寫分離分享給大家,奈何工作一直沒停下,所以這個週末抽空把這些分享出來。
關於MySQL的讀寫分離的實現,有兩種方式,第一種方式即我們手動在代碼層實現邏輯,來解析讀請求或者寫請求,分別分發到不同的數據庫中,實現讀寫分離;第二種方式就是基於MyCat中間件來實現讀寫分離的效果;這兩種方式我都會在這篇博客中進行詳細地介紹、搭建,並且分析其中的優劣。
原理初探
從MySQL的主從同步開始談起,最開始我們的數據庫架構是這樣的。
主庫負責了所有的讀寫操作,而從庫只對主庫進行了備份,就像我在上一篇文章中說的那樣,我認爲如果只實現了一個備份,不能讀寫分離和故障轉移,不能降低Master節點的IO壓力,這樣的主從架構看起來性價比似乎不是很高。
我們所希望的主從架構是,當我們在寫數據時,請求全部發到Master節點上,當我們需要讀數據時,請求全部發到Slave節點上。並且多個Slave節點最好可以存在負載均衡,讓集羣的效率最大化。
那麼這樣的架構就不夠我們使用了,我們需要找尋某種方式,來實現讀寫分離。那麼實際上有兩種方式。
-
方法1:代碼層實現讀寫分離
這種方法的優勢就是比較靈活,我們可以按照自己的邏輯來決定讀寫分離的規則。如果使用了這樣的方法,我們整個數據庫的架構就可以用下面這張圖進行概括:
-
方法2:使用中間層(虛擬節點)進行請求的轉發
這種方式最主要的特點就是我們在除了數據庫以外地方,新構建了一個虛擬節點,而我們所有的請求都發到這個虛擬節點上,由這個虛擬節點來轉發讀寫請求該相應的數據庫。
這種方式的特點就是,其構建了一個獨立的節點來接收所有的請求,而不用在我們的程序中配置多數據源,我們的項目只需要將url指向這個虛擬節點,然後由這個虛擬節點來處理讀寫請求。不是有這麼一句話嗎,專業的事交給專業的人來做,大概是這麼個意思吧。而現在存在的MyCat等中間件,就是這樣的一個”專業的人“。
那麼下面我就會動手實現上述兩個讀寫分離的解決方案,代碼層實現讀寫分離和使用中間件實現讀寫分離
手動實現讀寫分離
實現讀寫分離的方法有很多,我這裏會說到兩種,第一種是使用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的同學也沒關係,我會一步一步地進行演示操作。
-
導入依賴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
<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
爲了測試項目儘量簡單,所以我們不用去過多地配置其它東西。只有一些基本配置和數據源配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
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
-
編寫啓動類
1 2 3 4 5 6
@SpringBootApplication public class ApplicationStarter { public static void main(String[] args) { SpringApplication.run(ApplicationStarter.class, args); } }
-
啓動
出現以上信息代表啓動成功。嗯……這應該是一個數據庫相關的博客,好像講了太多的SpringBoot
到這裏說明我們的SpringBoot項目沒有問題,已經搭建成功,如果還不放心,可以自行訪問一下http://localhost:10001這個路徑,如果出現SpringBoot的404,則代表啓動成功。
-
新建Student實體並創建數據庫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
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; } }
1 2 3 4
CREATE TABLE student( student_id VARCHAR(32), student_name VARCHAR(32) );
-
編寫StudentDao接口,並進行測試
接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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); }
測試類:
1 2 3 4 5 6 7 8 9 10 11 12 13
@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搭建成功。
-
正式搭建
通過上面的準備工作,我們已經可以實現對數據庫的讀寫,但是並沒有實現讀寫分離,現在纔是開始實現數據庫的讀寫分離。
-
修改application.yml
剛纔我們的配置文件中只有單數據源,而讀寫分離肯定不會是單數據源,所以我們首先要在application.yml中配置多數據源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
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
這個配置主要是對主從數據源進行配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
@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,來給他傳一個數據源的類型(主、從)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
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我們需要用到的數據源類型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
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方法,將數據源類型傳入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
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 } }
-
代碼梳理
通過上文中的程序,我們已經可以實現讀寫分離了,但是這麼看着還是挺亂的。所以在這裏重新梳理一遍上文中的代碼。
其實邏輯並不難:
- 通過@Configuration實現多數據源的配置。
- 通過MyBatis的攔截器,DynamicDataSourceInterceptor來判斷某條SQL語句是讀還是寫,如果是讀,則調用DynamicDataSourceHolder.setDbType(“slave”),否則調用DynamicDataSourceHolder.setDbType(“master”)。
- 通過AbstractRoutingDataSource的determineCurrentLookupKey()方法,返回DynamicDataSourceHolder.getDbType();也就是我們在攔截器中設置的數據源。
- 對注入的數據源執行SQL。
-
測試
1 2 3 4 5 6 7 8 9 10 11 12 13
@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創建一個賬號並賦上權限:
1 2 3 4 5 6
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,但是這裏暫時不介紹分庫分表,所以這個暫且不提。
-
配置文件說明
-
server.xml
打開mycat安裝目錄下的/conf/server.xml文件,這個配置文件比較長,看着比較費腦,但其實對於初學者來說,我們需要配置的地方並不多,所以不用太害怕這種長篇幅的配置文件。(其實在上一篇文章的結尾,我也說過,面對一個新技術的時候首先不能懵逼,一步一步地去分析並接受他,扯遠了)配置文件簡化之後大概是這樣的一個結構。
1 2 3 4 5 6 7 8 9
<?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模塊,我們把它展開,着重講講這部分的配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<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,就是用這裏的用戶名和密碼進行登錄。至於如何配置,我在上面的配置中都寫好啦。跟着做就沒有問題。
-
schema.xml
打開MyCat安裝目錄的conf/schema.xml,這個配置文件是我們需要關注的一個配置文件,因爲我們的讀寫分離、分庫分表、故障轉移、都配置在這個配置文件中。但是這個配置文件並不長,我們可以一點一點慢慢分析。
首先是標籤中的內容。這個標籤主要是爲MyCat虛擬出一個數據庫,我們連接到MyCat上能看到的數據庫就是這裏配置的,而分庫分表也主要在這個標籤中進行配置。這個標籤中的name屬性,就是爲虛擬數據庫指定一個名字,也是我們連接MyCat看到的數據庫的庫名,dataNode是和下文的dataNode標籤中的name相對應的,代表這個虛擬的數據庫和下面的dataNode進行綁定。
1 2 3 4
<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屬性的值相對應。
1
<dataNode name="這裏寫節點名,需要和schema中的dataNode相對應" dataHost="這裏也是一個自定義名字,需要和dataHost中的name相對應" database="這裏填MySQL真實的數據庫名" />
第三個標籤要說的是標籤,這個標籤是和我們真實數據庫的主從、讀寫分離聯繫起來的標籤,什麼意思呢。這個標籤中有這麼兩個子標籤和分別代表我們的寫庫和讀庫,中配置的庫可以用於讀或者寫,而中配置的庫只能用於讀。
可以看到schema.xml的配置是一環扣一環的,每個標籤之間都有相互進行聯繫的屬性。我們最後配置完的schema.xml應該長下面這個樣子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
<?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:
1 2 3 4
<user name="MyCat" defaultAccount="true"> <property name="password">123456</property> <property name="schemas">MyCat</property> </user>
schema.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<?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庫中創建一張表
CREATE TABLE student(student_id VARCHAR(32),student_name VARCHAR(32));
這樣就可以證明我們的mycat連接真實數據庫成功。那麼我們下面就要開始證明讀寫分離,何謂讀寫分離呢?就是讀數據操作從從庫中讀取,而主庫只負責寫操作,下面我們開始進行驗證。
在驗證之前,我們需要將MyCat的日誌設置爲debug模式,因爲在info模式下,是不能在日誌中顯示SQL語句轉發到哪一個數據庫中進行查詢的。
如何設置:
打開conf/log4j2.xml
執行一條插入語句:
INSERT INTO student(student_id,student_name) VALUES('20191130','Object');
查看日誌:
可以看到,INSERT語句是在201中寫入的,201是Master庫,也就是寫庫。
寫在我們來執行一條讀語句:
SELECT * FROM student;
可以看到,SELECT 語句是在202中執行的,202是Slave庫,也就是讀庫。
再執行一次:
這個時候讀語句在203中執行,還是讀庫,這兩個讀庫是基於負載均衡規則來進行讀取的。
這樣就完成了讀寫分離的配置,當我們需要進行INSERT/UPDATE/DELETE時,會直接到Master中進行寫入,然後同步到Slave庫,而要進行SELECT操作時,就改爲去Slave中讀,不影響Master的寫入,這種讀寫分離,拓展了MySQL主從同步的功能,可以在容災備份的同時,提升數據庫的性能。
-
MyCat配置故障轉移
我們在上文中已經完成了MyCat關於讀寫分離的配置,那麼我們大膽假設,假如我們的Master數據庫突然宕機了,那麼是否整個集羣就喪失了寫功能呢?
在沒有故障轉移之前,這個答案是肯定的,當主庫宕機時,從庫作爲讀庫,是不會有寫的功能的,整個集羣也就喪失了寫的功能,這是我們不希望看到的。
我們希望看到的場景是:當主庫宕機,某一個從庫自動變爲主庫,承擔寫的功能,保證整個集羣的可用性。
那麼我們開始進行配置,其實思路很簡單,MyCat的標籤中有一個switchType屬性,其決定了切換的條件。
switchType指的是切換的模式,目前的取值也有4種:
-
switchType=’-1’ 表示不自動切換
-
switchType=’1’ 默認值,表示自動切換
-
switchType=’2’ 基於MySQL主從同步的狀態決定是否切換,心跳語句爲 show slave status
-
switchType=’3’基於MySQL galary cluster的切換機制(適合集羣)(1.4.1),心跳語句爲 show status like ‘wsrep%’。
我們直接將switchType修改爲2,然後將兩個讀庫配置爲第一個寫庫同級的寫庫。
配置文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?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)。如果項目比較大了,數據庫節點有可能進行增減,並且需要主從切換之類的功能,那麼就使用第二種方式吧。這種配置化的實現可以降低第二天洗頭時候下水管堵塞的機率。