美文网首页
MySQL读写分离实现

MySQL读写分离实现

作者: wolloo | 来源:发表于2019-03-08 17:42 被阅读0次

    数据库写入效率要低于读取效率,一般系统中数据读取频率高于写入频率,单个数据库实例在写入的时候会影响读取性能,这是做读写分离的原因。
    实现方式主要基于mysql的主从复制,通过路由的方式使应用对数据库的写请求只在master上进行,读请求在slave上进行。
    mysql主从复制:https://www.jianshu.com/p/a68551347d7d
    路由的方式主要有两种:
    1.代理
    在应用和数据库之间增加代理层,代理层接收应用对数据库的请求,根据不同请求类型转发到不同的实例,在实现读写分离的同时可以实现负载均衡。

    MySQL proxy.jpg
    目前常用的mysql的读写分离中间件有amoeba,MySQL-Proxy
    2.应用内路由
    在应用程序中实现,针对不同的请求类型去不同的实例执行sql
    client.jpg
    本文主要介绍第二种方式。基于springboot、 mybatis实现。
    思路:之前在做项目的时候实现过mybatis数据源的动态切换。基于原来的方案,用aop来拦截dao层方法,根据方法名称就可以判断要执行的sql类型,动态切换主从数据源。
    1.mybatis和数据源配置
    image.png
    2.数据源切换
    切换数据源需要用到类AbstractRoutingDataSource
    image.png
    targetDataSources用一个map来存储配置的数据源,defaultTargetDataSource默认的数据源 image.png

    项目启动时targetDataSources中的值会放到resolvedDataSources,key默认为targetDataSources中的key,可以实现resolveSpecifiedLookupKey()方法处理。
    resolvedDefaultDataSource会被赋值给defaultTargetDataSource,因此如果defaultTargetDataSource没有配启动会报错 。


    image.png

    在需要与mysql交互时检索resolvedDataSources中的数据源,通过抽象determineCurrentLookupKey()获取当前数据源的key,因此实现这个方法可以实现数据源的切换。

    数据源加载:

    /**
     * Title:MybatisConfiguration
     *
     * @author angla
     **/
    @Configuration
    public class MybatisConfiguration {
        @Autowired
        private Environment env;
        /**
         * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称)
         */
        @Bean
        public DataSource masterDataSource() throws Exception {
            Properties props = new Properties();
            props.put("driverClassName", env.getProperty("spring.mastersource.driver-class-name"));
            props.put("url", env.getProperty("spring.mastersource.url"));
            props.put("username", env.getProperty("spring.mastersource.username"));
            props.put("password", env.getProperty("spring.mastersource.password"));
            return DruidDataSourceFactory.createDataSource(props);
        }
    
        @Bean
        public DataSource slaveDataSource() throws Exception {
            Properties props = new Properties();
            props.put("driverClassName", env.getProperty("spring.slavesource1.driver-class-name"));
            props.put("url", env.getProperty("spring.slavesource1.url"));
            props.put("username", env.getProperty("spring.slavesource1.username"));
            props.put("password", env.getProperty("spring.slavesource1.password"));
            return DruidDataSourceFactory.createDataSource(props);
        }
        /**
         * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
         * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
         */
        @Bean
        @Primary
        @DependsOn({"masterDataSource","slaveDataSource"})
        public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_MASTER.getName(), masterDataSource);
            targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_SLAVE.getName(),slaveDataSource);
    
            DynamicDataSource dataSource = new DynamicDataSource();
            dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
            dataSource.setDefaultTargetDataSource(slaveDataSource);// 默认的datasource设置为myTestDbDataSource
    
            return dataSource;
        }
    
        /**
         * 根据数据源创建SqlSessionFactory
         */
        @Bean
        public SqlSessionFactory sqlSessionFactory(DynamicDataSource ds) throws Exception {
            SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
            fb.setDataSource(ds);// 指定数据源
            fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
            fb.setMapperLocations(
                    new PathMatchingResourcePatternResolver().getResources(Objects.requireNonNull(env.getProperty(
                            "mybatis.mapperLocations"))));
            return fb.getObject();
        }
    
        /**
         * 配置事务管理器
         */
        @Bean
        public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
            return new DataSourceTransactionManager(dataSource);
        }
    
    }
    

    数据源枚举:

    /**
     * Title:DataSourceTypeEnum
     *
     * @author angla
     **/
    
    public enum  DataSourceTypeEnum {
    
        DATA_SOURCE_MASTER(1,"master"),
        DATA_SOURCE_SLAVE(2,"slave");
    
        DataSourceTypeEnum(Integer code, String name) {
            this.code = code;
            this.name = name;
        }
    
        private Integer code;
    
        private String name;
    
        public Integer getCode() {
            return code;
        }
    
        public String getName() {
            return name;
        }
    
    }
    

    定义ThreadLocal存储.png

    /**
     * Title:DataSourceContextHolder
     *
     * @author angla
     **/
    public class DataSourceContextHolder {
    
        private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new ThreadLocal<>();
    
        public static void setDatabaseType(DataSourceTypeEnum databaseType) {
            contextHolder.set(databaseType);
        }
    
        public static DataSourceTypeEnum getDatabaseType() {
            return contextHolder.get();
        }
    
    }
    

    实现determineCurrentLookupKey方法

    /**
     * Title:DynamicDataSource
     *
     * @author angla
     **/
    public class DynamicDataSource extends AbstractRoutingDataSource {
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getDatabaseType();
        }
    }
    

    定义aop拦截dao层方法:

    @Component
    @Aspect
    @Slf4j
    public class DataSourceAspect {
    
        private static final String[] queryStrs = {"query", "select", "get"};
    
        /**
         * 定义切入点,切入点为com.angla.demo.dao下的所有方法
         */
        @Pointcut("execution(* com.angla.demo.dao.*.*(..))")
        public void executeSql() {
        }
    
        /**
         * 前置通知:在连接点之前执行的通知
         *
         * @param joinPoint
         * @throws Throwable
         */
        @Before("executeSql()")
        public void doBefore(JoinPoint joinPoint) throws Throwable {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            String mName = methodSignature.getMethod().getName();
            log.info("拦截sql方法:{}", mName);
            DataSourceContextHolder.setDatabaseType(DataSourceTypeEnum.DATA_SOURCE_MASTER);
            for (String name : queryStrs) {
                if (mName.startsWith(name)) {
                    log.info("查询语句,设置数据源为slave");
                    DataSourceContextHolder.setDatabaseType(DataSourceTypeEnum.DATA_SOURCE_SLAVE);
                    break;
                }
            }
            log.info("当前数据源:{}",DataSourceContextHolder.getDatabaseType().getName());
        }
    
    }
    

    至此,一个简单的读写分离实现就完成了,测试下结果:


    image.png

    停掉master实例,写数据报错,可以正常读取数据,停掉slave实例可以正常写数据,不能读取数据,结果是没问题的。但是这样还不够,现在加载数据源只能加载一主一从,不能适用一主多从或者多主多从的情况,后面需要改下数据源加载和获取方式。

    多主多从配置:


    多主多从.png

    加载数据源配置:

    
    @Data
    @Component
    @ConfigurationProperties(prefix = "spring")
    public class DataSourceProperties {
    
        private List<Map<String,String>> mastersources;
    
        private List<Map<String,String>> slavesources;
        
    }
    
    
        @Autowired
        private DataSourceProperties dataSourceProperties;
    
      /**
         * 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称)
         */
        @Bean
        public List<DataSource> masterDataSources() throws Exception {
    
            List<Map<String, String>> mastersources = dataSourceProperties.getMastersources();
            if (CollectionUtils.isEmpty(mastersources)) {
                throw new IllegalArgumentException("需要至少一个主数据源");
            }
            List<DataSource> dataSources = new ArrayList<>();
            for (Map map : mastersources) {
                dataSources.add(DruidDataSourceFactory.createDataSource(map));
            }
            return dataSources;
        }
    
        @Bean
        public List<DataSource> slaveDataSources() throws Exception {
            List<Map<String, String>> slavesources = dataSourceProperties.getSlavesources();
            if (CollectionUtils.isEmpty(slavesources)) {
                throw new IllegalArgumentException("需要至少一个从数据源");
            }
            List<DataSource> dataSources = new ArrayList<>();
            for (Map map : slavesources) {
                dataSources.add(DruidDataSourceFactory.createDataSource(map));
            }
            return dataSources;
        }
    
        /**
         * @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
         * @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
         */
        @Bean
        @Primary
        @DependsOn({"masterDataSources", "slaveDataSources"})
        public DynamicDataSource dataSource(List<DataSource> masterDataSources, List<DataSource> slaveDataSources) {
            Map<Object, Object> targetDataSources = new HashMap<>();
            for (int i = 0; i < masterDataSources.size(); i++) {
                targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_MASTER.getName() + i, masterDataSources.get(i));
            }
            for (int i = 0; i < slaveDataSources.size(); i++) {
                targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_SLAVE.getName() + i, slaveDataSources.get(i));
            }
    
            DynamicDataSource dataSource = new DynamicDataSource();
            dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
            dataSource.setDefaultTargetDataSource(slaveDataSources.get(0));// 默认的datasource设置为myTestDbDataSource
    
            return dataSource;
        }
    

    用随机的方式获取数据源:

    @Slf4j
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        @Autowired
        private DataSourceProperties dataSourceProperties;
    
        protected Object determineCurrentLookupKey() {
            DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDatabaseType();
            int i;
            List masterSources = dataSourceProperties.getMastersources();
            List slaveSources = dataSourceProperties.getSlavesources();
            if (dataSourceType.equals(DataSourceTypeEnum.DATA_SOURCE_MASTER)) {
                i = ThreadLocalRandom.current().nextInt(masterSources.size()) % masterSources.size();
            } else {
                i = ThreadLocalRandom.current().nextInt(slaveSources.size()) % slaveSources.size();
            }
            return dataSourceType.getName() + i;
        }
    }
    

    当然数据源加载完成后也可以用其他方式来做多数据源的负载均衡,只需要重写determineCurrentLookupKey()方法就行。

    相关文章

      网友评论

          本文标题:MySQL读写分离实现

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