zoukankan      html  css  js  c++  java
  • spring+mybatis的多源数据库配置实战

    前言:
      关于spring+mybatis的多源数据库配置, 其实是个老生常谈的事情. 网上的方案出奇的一致, 都是借助AbstractRoutingDataSource进行动态数据源的切换.
      这边再无耻地做一回大自然的搬运工, 除了做下笔记, 更多的希望是作为一个切入点, 能探寻下mybatis实现分库分表的解决方案.

    基本原理:
      关于mybatis的配置, 基本遵循如下的概念流:

    DB(数据库对接信息)->数据源(数据库连接池配置)->session工厂(连接管理与数据访问映射关联)->DAO(业务访问封装).

      对于定义的sqlmapper接口类, mybatis会为这些类动态生成一个代理类, 隐藏了连接管理(获取/释放), 参数设置/SQL执行/结果集映射等细节, 大大简化了开发工作.
      而连接管理涉及到具体的DataSource类实现机制, 在具体执行sql前, 其DB源的选定还有操作空间. 这也为DB路由(切换)提供了口子, 而AbstractRoutingDataSource的引入, 一定程度上为DB自由切换提供了便利.

    配置工作:
      先编写jdbc.properties的内容:

    # db1的配置
    db1.jdbc.url=jdbc:mysql://127.0.0.1:3306/db_account_1?useUnicode=true&characterEncoding=utf-8
    db1.jdbc.username=rd
    db1.jdbc.password=rd
    db1.jdbc.driver=com.mysql.jdbc.Driver
    
    # db2的配置
    db2.jdbc.url=jdbc:mysql://127.0.0.1:3306/db_account_2?useUnicode=true&characterEncoding=utf-8
    db2.jdbc.username=rd
    db2.jdbc.password=rd
    db2.jdbc.driver=com.mysql.jdbc.Driver

      编辑mybatis-config.xml(对mybatis做一些基础通用配置)的内容:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <!-- 配置mybatis的缓存,延迟加载等等一系列属性 -->
        <settings>
            <!-- 全局映射器启用缓存 -->
            <setting name="cacheEnabled" value="true" />
            <!-- 查询时,关闭关联对象即时加载以提高性能 -->
            <setting name="lazyLoadingEnabled" value="true" />
            <!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指定),不会加载关联表的所有字段,以提高性能 -->
            <setting name="aggressiveLazyLoading" value="false" />
            <!-- 允许插入 NULL -->
            <setting name="jdbcTypeForNull" value="NULL" />
        </settings>
    </configuration>

      编辑application-context.xml的配置:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context.xsd 
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <aop:aspectj-autoproxy proxy-target-class="true"/>
    
        <context:component-scan base-package="com.springapp.mvc"/>
        <context:annotation-config />
    
        <!-- 加载jdbc.properties配置文件 -->
        <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
            <property name="locations">
                <list>
                    <value>classpath:conf/jdbc.properties</value>
                </list>
            </property>
            <property name="fileEncoding" value="UTF-8"/>
            <property name="ignoreUnresolvablePlaceholders" value="true"/>
        </bean>
    
        <!-- 配置数据源1 -->
        <bean id="dataSource1" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="url" value="${db1.jdbc.url}"/>
            <property name="username" value="${db1.jdbc.username}"/>
            <property name="password" value="${db1.jdbc.password}"/>
            <property name="driverClassName" value="${db1.jdbc.driver}" />
        </bean>
    
    	<!-- 配置数据源2 -->
        <bean id="dataSource2" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="url" value="${db2.jdbc.url}"/>
            <property name="username" value="${db2.jdbc.username}"/>
            <property name="password" value="${db2.jdbc.password}"/>
            <property name="driverClassName" value="${db2.jdbc.driver}" />
        </bean>
    
        <!-- 配置动态数据源 -->
        <bean id="dynamicDatasource" class="com.springapp.mvc.datasource.DynamicDataSource">
            <property name="targetDataSources">
                <map>
                    <entry key="db1" value-ref="dataSource1"/>
                    <entry key="db2" value-ref="dataSource2"/>
                </map>
            </property>
            <property name="defaultTargetDataSource" ref="dataSource1"/>
        </bean>
    
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <property name="dataSource" ref="dynamicDatasource"/>
            <property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
            <property name="mapperLocations">
                <list></list>
            </property>
        </bean>
    
        <!--mybatis的配置-->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
            <property name="basePackage" value="com.springapp.mvc.dal"/>
            <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        </bean>
    
    </beans>

      注: 这里面涉及一些类, 会在下文中定义.

    依赖引入:
      这边使用了alibaba开源的druid作为数据库连接池.

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.11</version>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.10</version>
            </dependency>
    
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.4.6</version>
            </dependency>
    
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
                <version>1.3.2</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>4.1.1.RELEASE</version>
            </dependency>
    

      

    基础代码编写:
      主要是db路由的datasource实现类, 以及辅助的注解工具类.
      定义db来源的枚举类:

    @Getter
    @AllArgsConstructor
    public enum DataSourceKey {
    
        DB1("db1"),
        DB2("db2");
    
        private String dbKey;
    
    }

      定义标示当前激活db的工具类:

    public class DatasourceContextHolder {
    
        private static final ThreadLocal<String> contextHolder
                = new ThreadLocal<String>();
    
        // 设置数据源
        public static void setDataSourceType(DataSourceKey dbKey) {
            contextHolder.set(dbKey.getDbKey());
        }
    
        // 获取当前的数据源
        public static String getDataSourceType() {
            return contextHolder.get();
        }
    
        // 清空数据源
        public static void clearDataSourceType() {
            contextHolder.remove();
        }
    
    }

      注: 利用了ThreadLocal来保存当前选择的db源
      定义AbstractRoutingDataSource的实现类:

    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            return DatasourceContextHolder.getDataSourceType();
        }
    
    }

      注: 只要重载determineCurrentLookupKey()函数即可.
      定义注解:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DataSourceSelector {
        DataSourceKey dataSource() default DataSourceKey.DB1;
    }

      定义切面类:

    @Aspect
    @Component
    @Order(1)
    public class DataSourceSelectorAdvice {
    
        // 定义切点, 用于db源切换
        @Pointcut("@annotation(com.springapp.mvc.datasource.DataSourceSelector)")
        public void selectorDB() {
        }
    
        @Around("selectorDB() && @annotation(dataSourceSelector)")
        public Object aroundSelectDB(ProceedingJoinPoint pjp, DataSourceSelector dataSourceSelector) throws Throwable {
            // 设置具体的数据源
            DatasourceContextHolder.setDataSourceType(dataSourceSelector.dataSource());
            try {
                // 执行拦截的方法本体
                return pjp.proceed();
            } finally {
                // 清空设置的数据源
                DatasourceContextHolder.clearDataSourceType();
            }
        }
    
    }

      这些代码构成了动态切换db源的主干框架.

    业务代码编写:
      编写DO类:

    @Getter
    @Setter
    @ToString
    public class AccountDO {
    
        private String username;
    
        private String password;
    
    }

      编写sqlmapper接口类:

    @Repository
    public interface AccountMapper {
    
        @Select("SELECT username, password FROM tb_account WHERE user_id = #{user_id}")
        @Results({
                @Result(property = "userId",   column = "user_id",  jdbcType = JdbcType.VARCHAR),
                @Result(property = "username", column = "username", jdbcType = JdbcType.VARCHAR),
                @Result(property = "password", column = "password", jdbcType = JdbcType.VARCHAR),
        })
        AccountDO queryByUserId(@Param("user_id") String userId);
    
    }

      编写service类:

    @Service
    public class AccountService {
    
        @Resource
        private AccountMapper accountMapper;
    
        // *) 从db1获取数据
        @DataSourceSelector(dataSource = DataSourceKey.DB1)
        public AccountDO queryByUserId1(String userId) {
            return accountMapper.queryByUserId(userId);
        }
    
        // *) 从db2获取数据
        @DataSourceSelector(dataSource = DataSourceKey.DB2)
        public AccountDO queryByUserId2(String userId) {
            return accountMapper.queryByUserId(userId);
        }
    
    }

      Aspectj对接口(interface)无效, 对具体的实体类才其作用, 因为sqlmapper接口类会被mybatis生成一个动态类, 因此需要加切面(db切换), 需要在service层去实现.

    验证数据准备:
      本地创建了两个db, 都创建相同的表tb_account.

    CREATE TABLE `tb_account` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `user_id` varchar(32) NOT NULL,
      `username` varchar(32) DEFAULT NULL,
      `password` varchar(32) DEFAULT NULL,
      PRIMARY KEY (`id`), UNIQUE KEY (`user_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

      db1的tb_account有账号数据(1001, lilei).
      db2的tb_account有账号数据(2001, hanmeimei).
      

    测试:
      编写单测:

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({"classpath:application-context.xml"})
    public class AccountServiceTest {
    
        @Resource
        private AccountService accountService;
    
        @Test
        public void queryByUserId1() {
            // 用户id:1001, 落在db1中, 不在db2
            String userId = "1001";
    
            AccountDO accountDO1 = accountService.queryByUserId1(userId);
            Assert.assertNotNull(accountDO1);       // 存在断言
    
            AccountDO accountDO2 = accountService.queryByUserId2(userId);
            Assert.assertNull(accountDO2);          // 不存在断言
        }
    
        @Test
        public void queryByUserId2() {
            // 用户id:2001, 不在db1中, 在db2中
            String userId = "2001";
    
            AccountDO accountDO1 = accountService.queryByUserId1(userId);
            Assert.assertNull(accountDO1);          // 不存在断言
    
            AccountDO accountDO2 = accountService.queryByUserId2(userId);
            Assert.assertNotNull(accountDO2);       // 存在断言
        }
    
    }

      运行的结果符合预期.

    后记:
      对于微服务的盛行, 其实多源的数据源(基于业务划分)基本就不存在, 如果存在, 要么业务刚发展起来, 要么就是公司的基础设施太薄弱了^_^. 网上也看到有人用来主从(master/slave)的配置, 其实对于有一定规模的公司而言, mysql的主从分离都由类db proxy的中间件服务承包了.
      那他的意义究竟在哪呢? 其实我感觉还是给mysql的分库分表, 提供了一种可行的思路.

  • 相关阅读:
    剑指 Offer——13. 调整数组顺序使奇数位于偶数前面
    剑指 Offer——3. 从尾到头打印链表
    剑指 Offer——2. 替换空格
    剑指 Offer——1. 二维数组中的查找
    LeetCode 905. Sort Array By Parity 按奇偶校验排列数组
    LeetCode 448. Find All Numbers Disappeared in an Array找到所有数组中消失的元素
    SSH 代码笔记
    anaconda3安装caffe
    opencv多版本安装
    人脸文章与数据库
  • 原文地址:https://www.cnblogs.com/mumuxinfei/p/9366222.html
Copyright © 2011-2022 走看看