美文网首页db
Mysql 主从复制配置和程序读写分离配置

Mysql 主从复制配置和程序读写分离配置

作者: habit_learning | 来源:发表于2018-07-04 16:50 被阅读31次

    一、主从复制配置

    首先,准备两台服务器,master:192.168.174.10,slave:192.168.174.11。
    然后两台服务器上安装Mysql 5.5.5以上版本,因为其默认的执行引擎是支持事务的Innodb。
    Centos上安装Mysql

    配置主服务器:

    进入 /etc/my.cnf,在[mysqld]标识符下新增以下配置:
    server-id =10 # 主服务器标识,唯一即可
    log_bin=/usr/local/mysql/data/sql_log/mysql-bin #二进制日志存放路径
    binlog-do-db=miaosha # 需要主从复制的数据库,不写默认为全部库
    binlog-ignore-db=mysql # 不需要复制的数据库
    max_binlog_size = 1000M # 二进制日志文件的最大容量
    binlog_format = row # 它不记录sql语句上下文相关信息,仅保存哪条记录被修改
    expire_logs_days = 7 # 日志文件保存天数
    sync_binlog = 1

    bin-log文件:

    基本定义:二进制日志,也称为二进制日志,记录对数据发生或潜在发生更改的SQL语句,并以二进制的形式保存在磁盘中;
    作用:可以用来查看数据库的变更历史(具体的时间点所有的SQL操作)、数据库增量备份和恢复(增量备份和基于时间点的恢复)、MySQL的复制(主主数据库的复制、主从数据库的复制)。
    文件位置:默认存放位置为数据库文件所在目录下,也可以自定义,但是必须是mysql用户的包下。
    文件的命名方式: 名称为hostname-bin.xxxxx (重启mysql一次将会自动生成一个新的binlog)

    sync_binlog:

    sync_binlog=0为默认情况,表示MySQL不控制binlog的刷新,由文件系统自己控制它的缓存的刷新。这时候的性能是最好的,但是风险也是最大的。因为一旦系统Crash,在binlog_cache中的所有binlog信息都会被丢失。
    如果sync_binlog>0,表示每sync_binlog次事务提交,MySQL调用文件系统的刷新操作将缓存刷下去。最安全的就是sync_binlog=1了,表示每次事务提交,MySQL都会把binlog刷下去,是最安全但是性能损耗最大的设置。

    配置从服务器:

    同样进入 /etc/my.cnf,在[mysqld]标识符下新增以下配置:
    server-id = 11 # 从服务器标识,唯一即可
    relay_log=/usr/local/mysql/data/sql_log/mysqld-relay-log #relay_log 存放位置,必须是mysql用户的包下
    master_info_repository = TABLE
    relay_log_info_repository =TABLE
    read_only=on # 只读
    relay_log_recovery = on # 开启

    relay log 文件:

    由IO thread线程从主库读取的二进制日志事件组成,该日志被Slave上的SQL thread线程执行,从而实现数据的复制。

    master_info_repository :

    该文件保存slave连接master的状态以及配置信息,如用户名,密码,日志执行的位置。master_info_repository 配置为TABLE,这些信息会被写入mysql.slave_master_info 表中,代替原来的master.info文件了。使用表来代替原来的文件,主要为了crash-safe replication,从而大大提高从库的可靠性。

    relay_log_info_repository :

    该文件保存slave上relay log的执行位置。设置为TABLE可以避免relay.info更新不及时,SLAVE 重启后导致的主从复制出错。

    relay_log_recovery :

    当slave从库宕机后,假如relay-log损坏了,导致一部分中继日志没有处理,则自动放弃所有未执行的relay-log,并且重新从master上获取日志,这样就保证了relay-log的完整性。

    数据备份与传输:

    使用mysqldump 从主库中备份数据保存到all.sql文件,然后将all.sql文件传输给从数据库,最后从数据库执行该all.sql,从而实现数据传输。
    mysqldump -u用户名-p密码 数据库 > sql脚本文件路径全名,
    上述命令将指定数据库备份到某dump文件(转储文件)中,比如:
    mysqldump -uroot -p123456 miaosha > all.sql;
    然后将all.sql传输给从服务器:scp all.sql root@192.168.174.11:/home;
    最后从服务器执行all.sql文件,实现数据传输:mysql –u用户名 –p密码 –D数据库 < sql脚本文件路径全名,
    示例:mysql -uroot -p123456 -Dmiaosha < /home/all.sql

    在主服务器上创建一个用户user,并且赋予REPLICATION SLAVE 权限:

    登录主服务器的Mysql数据库,执行以下语句:
    mysql> CREATE USER user@'192.168.174.%' IDENTIFIED BY '123456';
    mysql> GRANT REPLICATION SLAVE ON * . * TO user@'192.168.174.%';
    192.168.174.%:表示192.168.174.0~192.168.174.255范围内的服务器都可以使用user用户。
    这样子,从服务器就可以通过user用户访问主服务器的二进制日志文件,从而实现数据的复制。
    同时,使用 show master status命令,查看主服务器日志文件信息,后续从服务器上创建复制链路需要这些参数。


    主服务器日志文件信息

    从服务器上创建复制链路:

    登录从服务器的Mysql数据库,执行以下语句:
    CHANGE MASTER TO
    -> MASTER_HOST='192.168.174.10', # 主服务器ip
    -> MASTER_USER='user', # 主服务器上创建的user用户
    -> MASTER_PASSWORD='123456', # user用户密码
    -> MASTER_LOG_FILE='mysql-bin.000001', # 为master中的二进制日志文件,与上面show master status结果的File一致
    -> MASTER_LOG_POS=501; # master中二进制日志文件的起始复制位置,与上面show master status结果的Position一致
    然后,开启从服务器的链路,执行 start slave 命令;至此,我们就实现了主从复制,我们可以通过执行 show slave status\G 命令来查询是否开启成功。


    从服务器连接状态

    二、程序读写分离配置

    本文主从数据库的切换需要用到AOP,所以直接引用jar包;

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    

    配置文件application.yml的数据库配置:

    spring:
      datasource:
        master:
          driverClassName: com.mysql.jdbc.Driver
          url: jdbc:mysql://192.168.174.10:3306/miaosha?useUnicode=true&characterEncoding=utf8&useSSL=true
          username: root
          password: 123456
          type: com.alibaba.druid.pool.DruidDataSource
          max-active: 1000
          initial-size: 100
          min-idle: 500
          max-wait: 60000
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: select 1
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          max-open-prepared-statements: 20
          #对于长时间不使用的连接强制关闭
          remove-abandoned: true
          #超过5分钟开始关闭空闲连接
          remove-abandoned-timeout: 300
        slave:
          driverClassName: com.mysql.jdbc.Driver
          url: jdbc:mysql://192.168.174.11:3306/miaosha?useUnicode=true&characterEncoding=utf8&useSSL=true
          username: root
          password: 123456
          type: com.alibaba.druid.pool.DruidDataSource
          max-active: 1000
          initial-size: 100
          min-idle: 500
          max-wait: 60000
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: select 1
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          max-open-prepared-statements: 20
          #对于长时间不使用的连接强制关闭
          remove-abandoned: true
          #超过5分钟开始关闭空闲连接
          remove-abandoned-timeout: 300
    

    数据库配置类 DataSourceConfiguration:

    @Configuration
    @Slf4j
    public class DataSourceConfiguration {
    
        /**
         * 主数据源
         * @return
         */
        @Bean(name = "writeDataSource")
        @Primary
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource writeDataSource(){
            log.info("---------------writeDataSource init -----------------");
            return DataSourceBuilder.create().type(DruidDataSource.class).build();
        }
    
        /**
         * 从数据源
         * @return
         */
        @Bean(name = "readDataSource")
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource readDataSource(){
            log.info("---------------readDataSource init -----------------");
            return DataSourceBuilder.create().type(DruidDataSource.class).build();
        }
    }
    

    自定义一个数据库注解,用于标识读写数据库 @DataSource。

    package com.imooc.miaosha.config;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    @Retention(RUNTIME)
    @Target(METHOD)
    public @interface DataSource {
        String value() default "";
    }
    

    @DataSource的value有两种,一种是“write”写库,另一种是"read"读库,所以我们写一个枚举类。

    package com.imooc.miaosha.enums;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @Getter
    @AllArgsConstructor
    public enum DataSourceType {
        READ("read"),
        WRITE("write");
    
        private String type;
    }
    

    使用ThreadLocal来存放当前线程的读写类型(write或者read),后续会采用AOP获取@DataSource的value值,往ThreadLocal中set进去。

    package com.imooc.miaosha.config;
    
    import com.imooc.miaosha.enums.DataSourceType;
    
    /**
     * 本地线程全部变量-数据源
     */
    public class DataSourceContextHolder {
    
        private static final ThreadLocal<String> dataSourceContext = new ThreadLocal<>();
    
        public static void read(){
            dataSourceContext.set(DataSourceType.READ.getType());
        }
    
        public static void write(){
            dataSourceContext.set(DataSourceType.WRITE.getType());
        }
    
        public static String getJdbcType(){
            return dataSourceContext.get();
        }
    }
    

    创建MyAbstractRoutingDataSource,继承AbstractRoutingDataSource,并重写determineCurrentLookupKey()方法。每次访问数据库,都会调用getConnection()方法,去获取数据库连接,该方法里面调用了determineTargetDataSource()方法,然后在determineTargetDataSource()方法里面调用了AbstractRoutingDataSource类里的抽象方法determineCurrentLookupKey()。这时候我们需要重写该抽象方法来通过ThreadLocal获取当前的数据库类型的标识(write或者read),从而决定采用哪种数据库。

    package com.imooc.miaosha.config;
    
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    /**
     * 重写determineCurrentLookupKey方法,因为每次获取数据库连接时,会调用getConnection()方法,
     * 该方法里面调用了determineTargetDataSource()方法,然后determineTargetDataSource()方法里面调用了
     * AbstractRoutingDataSource类里的抽象方法determineCurrentLookupKey()。
     */
    public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getJdbcType();
        }
    }
    

    Mybatis配置类 MyBatisConfiguration :

    package com.imooc.miaosha.config;
    
    import com.google.common.collect.Maps;
    import com.imooc.miaosha.enums.DataSourceType;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    import java.util.Map;
    
    @Configuration
    public class MyBatisConfiguration {
    
        @Resource(name = "writeDataSource")
        private DataSource writeDataSource;
        @Resource(name = "readDataSource")
        private DataSource readDataSource;
    
        @Bean
        public SqlSessionFactory sqlSessionFactory() throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(roundRobinDataSourceProxy());
            org.apache.ibatis.session.Configuration configuration = sqlSessionFactoryBean.getObject().getConfiguration();
            configuration.setMapUnderscoreToCamelCase(true);
            configuration.setUseGeneratedKeys(true);
            return sqlSessionFactoryBean.getObject();
        }
    
        @Bean
        public AbstractRoutingDataSource roundRobinDataSourceProxy(){
            MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
            Map<Object, Object> targetDataSources = Maps.newHashMap();
            targetDataSources.put(DataSourceType.READ.getType(), readDataSource);
            targetDataSources.put(DataSourceType.WRITE.getType(), writeDataSource);
            proxy.setDefaultTargetDataSource(writeDataSource);
            proxy.setTargetDataSources(targetDataSources);
            return proxy;
        }
    
    }
    

    这里将读写数据源的bean放入AbstractRoutingDataSource 中,其中key为数据库标识write或者read,value为对应的读写数据源bean。这样就可以通过ThreadLocal获取到的当前数据库标识,去取对应的数据源bean了,从而实现读写分离。
    添加事务管理配置 DataSourceTransactionManager ,因为只有写库涉及到事务,所以只需要将写数据源放入事务管理即可:

    package com.imooc.miaosha.config;
    
    import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    
    @Configuration
    @EnableTransactionManagement
    public class DataSourceTransactionManager extends DataSourceTransactionManagerAutoConfiguration {
    
        @Resource(name = "writeDataSource")
        private DataSource writeDataSource;
    
        @Bean
        public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManager(){
            return new org.springframework.jdbc.datasource.DataSourceTransactionManager(writeDataSource);
        }
    }
    

    使用AOP,读取指定包下@DataSource的值:

    package com.imooc.miaosha.aop;
    
    import com.imooc.miaosha.config.DataSource;
    import com.imooc.miaosha.config.DataSourceContextHolder;
    import com.imooc.miaosha.enums.DataSourceType;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    @Aspect
    @Slf4j
    @Component
    public class DataSourceAop {
        /**
         * @annotation(com.imooc.miaosha.config.DataSource)
         * 所有含有@DataSource 注解的方法都将匹配到
         */
        @Before("@annotation(com.imooc.miaosha.config.DataSource)")
        public void setDataSourceType(JoinPoint joinPoint) {
            Object target = joinPoint.getTarget();
            // 获取方法名称
            String methodName = joinPoint.getSignature().getName();
            // 获取方法参数类型
            Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
            try {
                Method method = target.getClass().getMethod(methodName, parameterTypes);
                DataSource dataSource = method.getAnnotation(DataSource.class);
                // 为空什么都不做,因为一开始我们设置了master为默认数据库
                if (dataSource == null) return;
                String value = dataSource.value();
                if (DataSourceType.READ.getType().equals(value)){
                    DataSourceContextHolder.read();
                }else if (DataSourceType.WRITE.getType().equals(value)){
                    DataSourceContextHolder.write();
                }
                log.info("dataSource切换到:{}", value);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 方法走完要清空ThreadLocal,否则下次如果没有指定@DataSource注解,
         * 则不会使用默认的master数据源(即写数据源),而是上一次的数据源
         * @param joinPoint
         */
        @After("@annotation(com.imooc.miaosha.config.DataSource)")
        public void afterSetDataSourceType(JoinPoint joinPoint) {
            DataSourceContextHolder.clearDB();
        }
    
    }
    

    以上配置完成之后,就实现了数据库的读写分离,是不是很给力!

    package com.imooc.miaosha.service.impl;
    
    import java.util.List;
    
    import com.imooc.miaosha.config.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import com.imooc.miaosha.mapper.GoodsMapper;
    import com.imooc.miaosha.model.Goods;
    import com.imooc.miaosha.model.MiaoshaGoods;
    import com.imooc.miaosha.service.GoodsService;
    import com.imooc.miaosha.vo.GoodsVO;
    
    @Service
    public class GoodsServiceImpl implements GoodsService {
    
        @Autowired
        private GoodsMapper goodsMapper;
    
        @Override
        @DataSource("read")
        public List<GoodsVO> getAllGoodsInfo() {
            return goodsMapper.findAllGoodsInfo();
        }
    
        @Override
        public Goods findOne(Long goodsId) {
            return goodsMapper.findOne(goodsId);
        }
    
        @Override
        @DataSource("write")
        public boolean reduceMiaoshaStock(Long goodsId) {
            int i = goodsMapper.decreaseStock(goodsId);
            return i == 1;
        }
    
    

    小结:当一个客户端请求过来,会调用impl包下的service实现类,aop通过扫描实现类中方法上的@DataSource注解,如果没有该注解,则采用默认的写数据源;如果有该注解,则获取注解中的value值,并且set进去ThreadLocal。接着去获取数据源,即调用getConnection()方法,会调用我们重写的determineCurrentLookupKey()方法从ThreadLocal中获取当前的数据源类型,AbstractRoutingDataSource会根据当前的数据源类型,取出对应的数据源,从而执行SQL语句。

    相关文章

      网友评论

        本文标题:Mysql 主从复制配置和程序读写分离配置

        本文链接:https://www.haomeiwen.com/subject/kvzjyftx.html