spring+mybatis的多源數據庫配置實戰

 

前言:
  關於spring+mybatis的多源數據庫配置, 實際上是個老生常談的事情. 網上的方案出奇的一致, 都是藉助AbstractRoutingDataSource進行動態數據源的切換.
  這邊再無恥地作一回大天然的搬運工, 除了作下筆記, 更多的但願是做爲一個切入點, 能探尋下mybatis實現分庫分表的解決方案.html

 

基本原理:
  關於mybatis的配置, 基本遵循以下的概念流:java

DB(數據庫對接信息)->數據源(數據庫鏈接池配置)->session工廠(鏈接管理與數據訪問映射關聯)->DAO(業務訪問封裝).

  對於定義的sqlmapper接口類, mybatis會爲這些類動態生成一個代理類, 隱藏了鏈接管理(獲取/釋放), 參數設置/SQL執行/結果集映射等細節, 大大簡化了開發工做.
  而鏈接管理涉及到具體的DataSource類實現機制, 在具體執行sql前, 其DB源的選定還有操做空間. 這也爲DB路由(切換)提供了口子, 而AbstractRoutingDataSource的引入, 必定程度上爲DB自由切換提供了便利.mysql

 

配置工做:
  先編寫jdbc.properties的內容:spring

# 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作一些基礎通用配置)的內容:sql

<?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做爲數據庫鏈接池.session

        <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來源的枚舉類:mybatis

@Getter
@AllArgsConstructor
public enum DataSourceKey {

    DB1("db1"),
    DB2("db2");

    private String dbKey;

}

  定義標示當前激活db的工具類:mvc

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的分庫分表, 提供了一種可行的思路.

相關文章
相關標籤/搜索