• mysql读写分离


    动手实现MySQL读写分离and故障转移

    MySQL的读写分离两种实现方式

    第一种方式:我们手动在代码层实现逻辑,来解析读请求或者写请求,分别分发到不同的数据库中,实现读写分离;

    第二种方式就是基于中间件做一层proxy,比如MyCat来实现读写分离的效果。

    手写实现读写分离

    MyBatis+Spring

    手写MyBatis拦截器来判断SQL是读或者写,从而选择数据源,最后交给Spring注入数据源,来实现读写分离。

    ------修改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
    View Code

    ------DataSource的配置

    首先要先创建两个ConfigurationProperties类,这一步不是非必须的,直接配置DataSource也是可以的,但是我还是比较习惯去写这个Properties。

    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;
        }
        
    }
    View Code

    SlaveProperties类似。

    ------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;
        }
    }
    View Code

    ------动态数据源的切换

    这里使用到的主要是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();
        }
    }
    View Code

    ------DynamicDataSourceHolder的实现

    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();
        }
    }
    View Code

    ------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
    
        }
    
    }
    View Code

    ------总结梳理

    通过上文中的程序,我们已经可以实现读写分离了,但是这么看着还是挺乱的。所以在这里重新梳理一遍上文中的代码。

    其实逻辑并不难:

    1. 通过@Configuration实现多数据源的配置。
    2. 通过MyBatis的拦截器,DynamicDataSourceInterceptor来判断某条SQL语句是读还是写,如果是读,则调用DynamicDataSourceHolder.setDbType("slave"),否则调用DynamicDataSourceHolder.setDbType("master")。
    3. 通过AbstractRoutingDataSource的determineCurrentLookupKey()方法,返回DynamicDataSourceHolder.getDbType();也就是我们在拦截器中设置的数据源。
    4. 对注入的数据源执行SQL。

    AOP+Spring

    spring动态数据源设置默认的master数据源和从数据源,aop针对读写的方法名来是读或者写,然后选择数据源去请求。

    基于MyCat中间件实现读写分离、故障转移

    MyCat配置文件

    ------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>
    View Code

    其实对于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,就是用这里的用户名和密码进行登录。至于如何配置,我在上面的配置中都写好啦。跟着做就没有问题。

    当然可以配置多个用户<user>,比如配置root用户有所有权限。普通用户只有读权限。

    ------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>
    View Code
    第二个标签是标签,这个标签是和我们真实数据库中的database联系起来的,name属性是我们对这个dataNode自定义的一个名字,要注意的是,这个名字需要和schema标签中的dataNode内容一致,database属性写的是我们真实数据库中的真实database的名字。而dataHost的内容需要和之后标签中的name属性的值相对应。
    <dataNode name="这里写节点名,需要和schema中的dataNode相对应" dataHost="这里也是一个自定义名字,需要和dataHost中的name相对应" database="这里填MySQL真实的数据库名" />
    View Code

    第三个标签要说的是标签,这个标签是和我们真实数据库的主从、读写分离联系起来的标签,什么意思呢。这个标签中有这么两个子标签和分别代表我们的写库读库,中配置的库可以用于读或者写,而中配置的库只能用于读。

    可以看到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>

    ------rules.xml

    分库分表相关。

    配置故障转移

    假如我们的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%'。
    View Code

    我们直接将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>
    View Code

    此时停掉master库即可测试。但是此时我们MySQL的主从架构已经被破坏,如果需要恢复主从结构,就需要手动地重新去恢复我们的主从架构。我们需要将201和203作为Slave,202作为Master,因为Master拥有最完整的数据。

    两者的优劣分析

    代码层实现读写分离,主要的优点就是灵活,可以自己根据不同的需求对读写分离的规则进行定制化开发,但其缺点也十分明显,就是当我们动态增减主从库数量的时候,都需要对代码进行一个或多或少的修改。并且当主库宕机了,如果我们没有实现相应的容灾逻辑,那么整个数据库集群将丧失对外的写功能。

    使用MyCat中间件实现读写分离,优点十分明显,我们只需要进行配置就可以享受读写分离带来的效率的提升,不用写一行代码,并且当主库宕机时,我们还可以通过配置的方式进行主从库的自动切换,这样即使主库宕机我们的整个集群也不会丧失写的功能。其缺点可能就是我们得多付出一台服务器作为虚拟节点了吧,毕竟服务器也是需要成本的。

    两种方式如何抉择:如果你目前的项目比较小,或者干脆是一个毕业设计、课程设计之类的,不会有动态增减数据库的需求,那么自己动手实现一个数据库的读写分离会比较适合你,毕竟答辩的时候,可以一行一行代码跟你的导师和同学解(zhuang)释(bi)。如果项目比较大了,数据库节点有可能进行增减,并且需要主从切换之类的功能,那么就使用第二种方式吧。这种配置化的实现可以降低第二天洗头时候下水管堵塞的几率。

  • 相关阅读:
    IDEA mybatis no data source 警告
    Flex 本地化(多语言) 语言文件夹设置
    Flex中 将字符串转化为Datetime类
    Disabling Clang Compiler warnings
    didEndEditingRowAtIndexPath with nil indexPath
    万达的商业模式有什么独到之处
    微信公众平台入门到精通合集
    BeautifulSoup解析非标准HTML的问题
    《人件》与软件研发项目管理
    足球3v3心得
  • 原文地址:https://www.cnblogs.com/fanguangdexiaoyuer/p/12342132.html
Copyright © 2020-2023  润新知