SpringBoot 读写分离实现(AbstractRoutingDataSource)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/believer123/article/details/78224798

读写分离一直都是项目的标配,之前项目的做法非常简单,直接配置两个数据源,一个只读,一个只写,只读的放到xxx.read,只写的放到xxx.write包下。Service层调用的时候根据操作选择对应的数据源。主要配置:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        略...
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:config/db/mybatis-configuration.xml" />
        <property name="mapperLocations">
            <array>
                <value>classpath*:xxx/write/resource/*.sql.xml</value>

            </array>
        </property>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="xxx.write" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    </bean>


    <!-- Transaction manager for a single JDBC DataSource -->
    <bean id="txManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
    </bean>

以上实现了为只写的数据源配置事务。Mybatis自动扫描对应包下的xml文件。
这样做优点还是很明显的,简单易懂。以后只要有新功能按照读写分离原则放到指定包下即可。
缺点就是在Service层涉及到读写同时进行的时候,需要调用对应的Mapper,比如:xxxReadMapper,xxxWriteMapper 的方法。
如果以后读写分离改成的数据库层处理,那么这里的代码就需要合并到一起,增加工作量。

那有没有更好的方法呢?是否可以做到自动读写分离呢?
当然是有的,而且还有很多种方式,比如通过数据库代理的方式,而不是通过代码来实现。或者还有其他开源框架。这里介绍下我的实现方式,基于AbstractRoutingDataSource。
该类通过代理的方式实现了数据源的动态分配,在使用时通过自定义的key来选择对应的数据源。它的注释是这么说明的:

Abstract javax.sql.DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.

步骤1:执行db目录下的springboot.sql文件来初始化db,这里需要配置两个db,一个只读(springboot_r)一个写(springboot)。
步骤2:继承自AbstractRoutingDataSource,初始化结束时自动扫描容器内的数据源,实现自动代理

@Component("dynamicDataSource")
    @Primary
    @ConfigurationProperties(prefix = "dynamicDatasource")
    public static class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware {

        public static final Map<String, String> DATASOURCE_STRATEGY = new HashMap<>();

        private Map<String, String> strategy = new HashMap<>();
        private ApplicationContext applicationContext;
        private String defaultDataSource;

        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicDataSourceHolder.getDataSource();
        }

        @Override
        protected Object resolveSpecifiedLookupKey(Object lookupKey) {
            return super.resolveSpecifiedLookupKey(lookupKey);
        }

        @Override
        public void afterPropertiesSet() {
            Map<String, DataSource> dataSources = applicationContext.getBeansOfType(DataSource.class);
            if (dataSources.size() == 0) {
                throw new IllegalStateException("Datasource can not found!!!");
            }

            // exclude current datasource
            Map<Object, Object> targetDataSource = excludeCurrentDataSource(dataSources);
            setTargetDataSources(targetDataSource);

            // 多数据源方法设置
            Iterator<String> it = strategy.keySet().iterator();
            while (it.hasNext()) {
                String key = it.next();
                String[] values = strategy.get(key).split(",");
                for (String v : values) {
                    if (StringUtils.isNotBlank(v)) {
                        DATASOURCE_STRATEGY.put(v, key);
                    }
                }
            }

            // 默认数据源设置
            setDefaultTargetDataSource(targetDataSource.get(getDefaultDataSource()));

            super.afterPropertiesSet();
        }

        /***
         * exclude current Datasource
         * 
         * @param dataSources
         * @return
         */
        private Map<Object, Object> excludeCurrentDataSource(Map<String, DataSource> dataSources) {
            Map<Object, Object> targetDataSource = new HashMap<>();
            Iterator<String> keys = dataSources.keySet().iterator();
            while (keys.hasNext()) {
                String key = keys.next();
                if (!(dataSources.get(key) instanceof DynamicDataSource)) {
                    targetDataSource.put(key, dataSources.get(key));
                }
            }
            return targetDataSource;
        }

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }

        public Map<String, String> getStrategy() {
            return strategy;
        }

        public void setStrategy(Map<String, String> strategy) {
            this.strategy = strategy;
        }

        public String getDefaultDataSource() {
            return defaultDataSource;
        }

        public void setDefaultDataSource(String defaultDataSource) {
            this.defaultDataSource = defaultDataSource;
        }

    }

步骤3:配置读和写的数据源

@ConfigurationProperties(prefix = "db.mybatis.jdbc")
    @Bean(destroyMethod = "close", name = "write")
    public DataSource dataSourceWrite() {

        log.info("*************************dataSource***********************");

        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setRemoveAbandoned(true);
        dataSource.setTestWhileIdle(true);
        dataSource.setTimeBetweenEvictionRunsMillis(30000);
        dataSource.setNumTestsPerEvictionRun(30);
        dataSource.setMinEvictableIdleTimeMillis(1800000);
        return dataSource;
    }

    @ConfigurationProperties(prefix = "db.mybatis2.jdbc")
    @Bean(destroyMethod = "close", name = "read")
    public DataSource dataSourceRead() {

        log.info("*************************dataSource***********************");

        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setRemoveAbandoned(true);
        dataSource.setTestWhileIdle(true);
        dataSource.setTimeBetweenEvictionRunsMillis(30000);
        dataSource.setNumTestsPerEvictionRun(30);
        dataSource.setMinEvictableIdleTimeMillis(1800000);
        return dataSource;
    }

步骤4:为动态数据源配置读写分离策略,这里使用的是最简单的前缀规则,如果有需要可以自行改成正则表达式的方式,以下配置定义了get,find,select开头的方法都使用read数据源

dynamicDatasource.strategy.read=get,find,select
dynamicDatasource.strategy.write=insert,update,delete,login
dynamicDatasource.defaultDataSource=write

步骤5:单元测试,在test包下DynamicDataSourceTest类中有两个方法,一个测试只读一个测试写:

@Test
    public void testLogin() throws Exception {
        User user = new User();
        user.setUsername("11111111111");
        user.setPassword("123456");
        User loginUser = userService.login(user);
        System.out.println("登录结果:" + loginUser);
    }

    @Test
    public void testFindUser() throws Exception {
        User loginUser = userService.findUserByToken("xxx");
        System.out.println("查询用户结果:" + loginUser);
    }

执行testLogin单元测试可以看出这里的操作用的是写的数据源

 ooo Using Connection [jdbc:mysql://localhost/springboot?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull, UserName=root@localhost, MySQL Connector Java]

执行testFindUser可以看出这里用的是读的数据源

ooo Using Connection [jdbc:mysql://localhost/springboot_r?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull, UserName=root@localhost, MySQL Connector Java]

这种方式优点:不需要再像之前一样对读写操作分离了,都可以统一到一个Mapper上,代码可以统一到一个包下。程序员甚至都不需要意识到数据库的读写分离。以后替换成db层处理也是非常方便的。

注意点:
因为事务和动态数据源切换都是基于AOP的,所以顺序非常重要。动态切换要在事务之前,如果发现无法动态切换数据源那么可以看下他们之间的顺序。

以上代码已提交至SpringBootLearning的DynamicDataSource工程。


说明:
com.cml.springboot.framework.db 动态数据源配置包
com.cml.springboot.framework.mybatis mybatis配置包,配置了mybatis规则和读写数据源

SpringBootLearning是对springboot学习与研究项目,是根据实际项目的形式对进行配置与处理,欢迎star与fork。
[oschina 地址]
http://git.oschina.net/cmlbeliever/SpringBootLearning
[github 地址]
https://github.com/cmlbeliever/SpringBootLearning

猜你喜欢

转载自blog.csdn.net/believer123/article/details/78224798