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