zoukankan      html  css  js  c++  java
  • 读写分离注解

      以前写过读写分离,今天完善成文档。

     一:概述

    1.结构文档

      

    2.思路

      组装好各个数据源,然后通过注解选择使用读或者写的数据源,将其使用AbstractRoutingDataSource中的方法determineCurrentLookuoKey进行选择datasource的key。

      然后,通过key,就找到了要使用的数据源。

      在数据源的这里,最后配置上连接池。

    3.说明

      本配置直接使用一个公共的连接池配置,如果需要,自己进行配置

    二:程序说明

    1.连接池配置

    package com.jun.webpro.common.config.dataSource.properties;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    import java.util.Map;
    
    /**
     * 数据库连接池配置
     */
    @ConfigurationProperties(prefix = "spring.druid")
    @Component
    @Data
    public class DruidProperties {
        /**
         * 初始化大小
         */
        private int initialSize;
        /**
         * 最小
         */
        private int minIdle;
        /**
         * 最大
         */
        private int maxActive;
        /**
         * 获取连接等待超时的时间
         */
        private int maxWait;
        /**
         * 间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
         */
        private int timeBetweenEvictionRunsMillis;
        /**
         * 一个连接在池中最小生存的时间,单位是毫秒
         */
        private int minEvictableIdleTimeMillis;
    
        private String validationQuery;
    
        private boolean testWhileIdle;
    
        private boolean testOnBorrow;
    
        private boolean testOnReturn;
        /**
         * 打开PSCache
         */
        private boolean poolPreparedStatements;
        /**
         * 并且指定每个连接上PSCache的大小
         */
        private int maxPoolPreparedStatementPerConnectionSize;
        /**
         * 监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
         */
        private String filters;
        /**
         * 物理连接初始化的时候执行的sql
         */
        private List<String> connectionInitSqls;
        /**
         * connectProperties属性来打开mergeSql功能;慢SQL记录
         */
        private Map<String, String> connectionProperties;
    }
    

      

    2.主从库的配置项接口

      因为通过这个接口,进行下面的注入

    package com.jun.webpro.common.config.dataSource.properties;
    
    
    public interface DataSourceProperties {
    
        String getDriverClassName();
    
        String getUrl();
    
        String getUsername();
    
        String getPassword();
    }
    

     

    3.从库配置

    package com.jun.webpro.common.config.dataSource.properties;
    
    import lombok.Data;
    import lombok.Getter;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    @Component
    @Data
    @Getter
    @ConfigurationProperties("mysql.datasource.slave")
    public class SlaveDataSourceProperties implements DataSourceProperties{
        /**
         * url
         */
        private String url;
    
        /**
         * driverClassName
         */
        private String driverClassName;
    
        /**
         * username
         */
        private String username;
    
        /**
         * password
         */
        private String password;
    }
    

      

    4.主库配置

    package com.jun.webpro.common.config.dataSource.properties;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    @Component
    @Data
    @ConfigurationProperties("mysql.datasource.master")
    public class MasterDataSourceProperties implements DataSourceProperties{
        /**
         * url
         */
        private String url;
    
        /**
         * driverClassName
         */
        private String driverClassName;
    
        /**
         * username
         */
        private String username;
    
        /**
         * password
         */
        private String password;
    }
    

      

    5.数据池配置项

    package com.jun.webpro.common.config.dataSource.config;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import com.jun.webpro.common.config.dataSource.properties.DataSourceProperties;
    import com.jun.webpro.common.config.dataSource.properties.DruidProperties;
    import org.springframework.context.annotation.Configuration;
    
    import javax.annotation.Resource;
    import java.sql.SQLException;
    import java.util.Properties;
    
    @Configuration
    public class DataSourceConfig {
        // 导入数据库连接池配置
        @Resource
        private DruidProperties druidProperties;
    
        /**
         * @param dataSourceProperties 其他地方传参
         */
        DruidDataSource initDruidDataSource(DataSourceProperties dataSourceProperties) throws SQLException {
            try (DruidDataSource dataSource = new DruidDataSource()) {
                dataSource.setInitialSize(druidProperties.getInitialSize());
                dataSource.setMinIdle(druidProperties.getMinIdle());
                dataSource.setMaxActive(druidProperties.getMaxActive());
                dataSource.setMaxWait(druidProperties.getMaxWait());
                dataSource.setTimeBetweenEvictionRunsMillis(druidProperties.getTimeBetweenEvictionRunsMillis());
                dataSource.setMinEvictableIdleTimeMillis(druidProperties.getMinEvictableIdleTimeMillis());
                dataSource.setValidationQuery(druidProperties.getValidationQuery());
                dataSource.setTestWhileIdle(druidProperties.isTestWhileIdle());
                dataSource.setTestOnBorrow(druidProperties.isTestOnBorrow());
                dataSource.setTestOnReturn(druidProperties.isTestOnReturn());
                dataSource.setPoolPreparedStatements(druidProperties.isPoolPreparedStatements());
                dataSource.setMaxPoolPreparedStatementPerConnectionSize(druidProperties.getMaxPoolPreparedStatementPerConnectionSize());
                dataSource.setConnectionInitSqls(druidProperties.getConnectionInitSqls());
                dataSource.setFilters(druidProperties.getFilters());
                Properties properties = new Properties();
                for (String key : druidProperties.getConnectionProperties().keySet()) {
                    properties.setProperty(key, druidProperties.getConnectionProperties().get(key));
                }
                dataSource.setConnectProperties(properties);
                dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
                dataSource.setUrl(dataSourceProperties.getUrl());
                dataSource.setUsername(dataSourceProperties.getUsername());
                dataSource.setPassword(dataSourceProperties.getPassword());
                return dataSource;
            }
        }
    }
    

      

    6.主数据库配置项

    package com.jun.webpro.common.config.dataSource.config;
    
    import com.jun.webpro.common.config.dataSource.properties.MasterDataSourceProperties;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    import java.sql.SQLException;
    
    
    @Slf4j
    @Configuration
    public class MasterDataSourceConfig extends DataSourceConfig{
        @Resource
        private MasterDataSourceProperties dataSourceProperties;
    
        /**
         * 数据源
         */
        @Bean(name = "masterDataSource")
        @Primary
        public DataSource masterDataSource() throws SQLException {
            return initDruidDataSource(dataSourceProperties);
        }
    
    }
    

      

    7.从数据库配置

    package com.jun.webpro.common.config.dataSource.config;
    
    import com.jun.webpro.common.config.dataSource.properties.MasterDataSourceProperties;
    import com.jun.webpro.common.config.dataSource.properties.SlaveDataSourceProperties;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    import java.sql.SQLException;
    
    
    @Slf4j
    @Configuration
    public class SlaveDataSourceConfig extends DataSourceConfig{
        @Resource
        private SlaveDataSourceProperties dataSourceProperties;
    
        /**
         * 数据源
         */
        @Bean(name = "slaveDataSource")
        public DataSource masterDataSource() throws SQLException {
            return initDruidDataSource(dataSourceProperties);
        }
    }
    

      

    8.mybatis配置项

      这里用于mysql事务

    package com.jun.webpro.common.config.dataSource.config;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class MybatisConfig {
        @Bean("mybatis-config")
        @ConfigurationProperties(prefix = "mybatis.configuration")
        public org.apache.ibatis.session.Configuration globalConfiguration() {
            return new org.apache.ibatis.session.Configuration();
        }
    }
    

      

    9.将数据源的key设置进ThreadLocal中

      这里主要是注解中,将值添加进来,然后后面使用。

    package com.jun.webpro.common.config.dataSource.route;
    
    import lombok.extern.slf4j.Slf4j;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * Description 这里切换读/写模式
     * 原理是利用ThreadLocal保存当前线程是否处于读模式(通过开始READ_ONLY注解在开始操作前设置模式为读模式,
     * 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式
     */
    @Slf4j
    public class DbContextHolder {
    
        public static final String MASTER = "masterDataSource";
        public static final String SLAVE = "slaveDataSource";
    
        private static ThreadLocal<String> contextHolder= new ThreadLocal<>();
    
        public static void setDbType(String dbType) {
            if (dbType == null) {
                log.error("dbType为空");
                throw new NullPointerException();
            }
            log.info("设置dbType为:{}",dbType);
            contextHolder.set(dbType);
        }
    
        public static String getDbType() {
            return contextHolder.get();
        }
    
        public static void clearDbType() {
            contextHolder.remove();
        }
    }
    

      

    10.去ThreadLocal中获取荣国注解加入的key

      这个key可以决定走哪个库。

    package com.jun.webpro.common.config.dataSource.route;
    
    import com.jun.webpro.common.config.dataSource.route.DbContextHolder;
    import com.jun.webpro.common.units.NumberUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    import java.util.Objects;
    
    /**
     * Description
     *
     */
    @Slf4j
    public class RoutingDataSource extends AbstractRoutingDataSource {
    
        @Value("${mysql.datasource.num}")
        private int num;
    
        @Override
        protected Object determineCurrentLookupKey() {
            String typeKey = DbContextHolder.getDbType();
            if (Objects.equals(DbContextHolder.MASTER, typeKey)) {
                log.info("使用了写库");
                return typeKey;
            }else {
                int sum = NumberUtils.getRandom(1, num);
                log.info("使用了读库{}", sum);
                return DbContextHolder.SLAVE + sum;
            }
        }
    }
    

      

    11.读写配置,主要点是重写routingDataSource

    package com.jun.webpro.common.config.dataSource.route;
    
    import com.jun.webpro.common.config.dataSource.config.DataSourceConfig;
    import com.jun.webpro.common.config.dataSource.route.DbContextHolder;
    import com.jun.webpro.common.config.dataSource.route.RoutingDataSource;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.mybatis.spring.SqlSessionFactoryBean;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.jdbc.datasource.DataSourceTransactionManager;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import javax.annotation.Resource;
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * Description
     *
     */
    @Configuration
    @MapperScan(basePackages = "com.jun.webpro.common.domain.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
    public class WriteOrReadDatabaseConfig extends DataSourceConfig {
        @Resource
        private DataSource masterDataSource;
    
        @Resource
        private DataSource slaveDataSource;
    
        /**
         * 事务
         */
        @Bean
        public DataSourceTransactionManager masterTransactionManager() {
            return new DataSourceTransactionManager(routingDataSource());
        }
    
        @Bean
        public SqlSessionFactory sqlSessionFactory(@Qualifier("mybatis-config") org.apache.ibatis.session.Configuration configuration) throws Exception {
            final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            sessionFactory.setDataSource(routingDataSource());
            sessionFactory.setConfiguration(configuration);
            sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
            return sessionFactory.getObject();
        }
    
        /**
         * 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
         * 工厂模式
         */
        @Bean
        public AbstractRoutingDataSource routingDataSource() {
            RoutingDataSource proxy = new RoutingDataSource();
            Map<Object, Object> targetDataSources = new HashMap<>(2);
            targetDataSources.put(DbContextHolder.MASTER, masterDataSource);
            targetDataSources.put(DbContextHolder.SLAVE+"1", slaveDataSource);
            proxy.setDefaultTargetDataSource(slaveDataSource);
            proxy.setTargetDataSources(targetDataSources);
            return proxy;
        }
    }
    

      

    12.注解

    package com.jun.webpro.common.aspect;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * Description 通过该接口注释的service使用读模式,其他使用写模式
     *
     * 接口注释只是一种办法,如果项目已经有代码了,通过注释可以不修改任何业务代码加持读写分离
     * 也可以通过切面根据方法开头来设置读写模式,例如getXXX()使用读模式,其他使用写模式
     *
     */
    @Target({ElementType.METHOD,ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ReadOnly {
    }
    

      

     13,注解实现

      将要使用的库写进去

    package com.jun.webpro.common.aspect.impl;
    
    import com.jun.webpro.common.aspect.ReadOnly;
    import com.jun.webpro.common.config.dataSource.route.DbContextHolder;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.Ordered;
    import org.springframework.stereotype.Component;
    
    /**
     * Description
     *
     */
    @Aspect
    @Component
    public class ReadOnlyInterceptor implements Ordered {
        private static final Logger log= LoggerFactory.getLogger(ReadOnlyInterceptor.class);
    
        @Around("@annotation(readOnly)")
        public Object setRead(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable{
            try{
                // 通过注解,将值注册进去
                DbContextHolder.setDbType(DbContextHolder.SLAVE);
                return joinPoint.proceed();
            }finally {
                DbContextHolder.clearDbType();
                log.info("清除threadLocal");
            }
        }
    
        @Override
        public int getOrder() {
            return 0;
        }
    }
    

      

    三:配置项

    1.

    mysql:
      datasource:
        #读库数目
        num: 1
        master:
          url: jdbc:mysql://47.103.25.1:3306/center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave:
          url: jdbc:mysql://47.103.25.1:3306/center?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
    
    spring:
      # Redis配置, 使用了连接池
      redis:
        database: 0
        host: 106.14.25.1
        port: 6379
        password:
        jedis:
          pool:
            max-active: 20
            max-wait: -1
            max-idle: 20
            min-idle: 10
        timeout: 1000
      #druid数据库连接池配置
      druid:
        initialSize: 5
        minIdle: 5
        maxActive: 8
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        connectionInitSqls: set names utf8mb4
        filters: stat
        connectionProperties:
          druid:
            stat:
              mergeSql: true
              slowSqlMillis: 1000
    

      

     四:关于事务

    1.DataSourceTranceManager

      AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。

      spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。

      将事务管理在数据持久 (Dao层) 开启,切换数据源的操作放在业务层进行操作,就可在事务开启之前顺利进行数据源切换,不会再出现切换失败了。

      

    五:缺点

    1.不能动态的增加数据源

    2.主从延迟问题

      如果往主库增加数据,马上到从库读取,一般没有问题

      但是数据量大的时候,因为有延迟,可能去查询的时候,查询不到

     

  • 相关阅读:
    【转】c#基础系列1---深入理解值类型和引用类型
    【转】Aspnet Core为什么支持跨平台
    [翻译svg教程]svg学习系列 开篇
    使用docker 解决一个小问题,你也可能用的到
    增加软链接
    漫长的表白
    被社会抽了一巴掌
    杂乱五章的2015年终总结
    [资源分享]yslow 与firebug 修复版本Firefox35【绿色版本下载】
    Web前端性能测试-性能测试知多少---深入分析前端站点的性能
  • 原文地址:https://www.cnblogs.com/juncaoit/p/12809798.html
Copyright © 2011-2022 走看看