1 多數據源的典型使用場景
在實際開發中,經常可能遇到在一個應用中可能需要訪問多個數據庫的情況。以下是兩種典型場景:
1 業務復雜
數據分布在不同的數據庫中,筆者見過一個相對比較復雜的業務,一個業務中同時操作了9個庫。
2 讀寫分離
一些規模較小的公司,沒有專門的中間件團隊搭建讀寫分離基礎設施,因此需要業務開發人員自行實現讀寫分離。
這里的架構與上圖類似。不同的是,在讀寫分離中,主庫和從庫的數據庫是一致的(不考慮主從延遲)。數據更新操作(insert、update、delete)都是在主庫上進行,主庫將數據變更信息同步給從庫。在查詢時,可以在從庫上進行,從而分擔主庫的壓力。
2 如何實現多數據源
對于大多數的java應用,都使用了spring框架,spring-jdbc模塊提供了AbstractRoutingDataSource,其內部可以包含了多個DataSource,然后在運行時來動態的訪問哪個數據庫。這種方式訪問數據庫的架構圖如下所示:
應用直接操作的是AbstractRoutingDataSource的實現類,告訴AbstractRoutingDataSource訪問哪個數據庫,然后由AbstractRoutingDataSource從事先配置好的數據源(ds1、ds2)選擇一個,來訪問對應的數據庫。
關于如何利用AbstractRoutingDataSource實現多數據源訪問,各種博客已經很多,基本功能都能實現,但是易用性不夠好,要修改大量代碼,業務侵入性太強。
這也是筆者為什么寫這篇文章的原因,這里提供了一種更加簡單易用的多數據源實現,筆者稱之為RoutingDataSource。在讀者對sprign-aop不是很了解的情況下,也能非常容易上手。而且筆者將這個組件發布到了maven中央倉庫,因此你可以直接pom.xml中進行引用。
<dependency>
<groupId>io.github.tianshouzhi</groupId>
<artifactId>routing-datasource</artifactId>
<version>1.0.0</version>
</dependency>
3 RoutingDataSource的配置
假設我們有2個庫,db1(包含user表),db2(包含user_account表)。我們需要配置2個數據源(類型任意:druid、c3p0、dbcp、tomcat-jdbc任何實現JDBC規范的數據源都可以),將其交給RoutingDataSource進行管理,相關配置如下:
<!--配置ds1,訪問數據庫db1-->
<bean id="ds1" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
<property name="username" value="root"/>
<property name="password" value="your password"/>
<property name="url" value="jdbc:mysql://localhost:3306/db1"/>
<property name="driver" value="com.mysql.jdbc.Driver"/>
</bean>
<!--配置ds2,訪問數據庫db2-->
<bean id="ds2" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
<property name="username" value="root"/>
<property name="password" value="your password"/>
<property name="url" value="jdbc:mysql://localhost:3306/db2"/>
<property name="driver" value="com.mysql.jdbc.Driver"/>
</bean>
<!--配置RoutingDataSource,其管理了ds1和ds2-->
<bean id="routingDataSource" class="io.github.tianshouzhi.routing.RoutingDataSource">
<property name="targetDataSources">
<map>
<!--entry的key,將在稍后將看到的@Routing注解中使用到-->
<entry key="ds1" value-ref="ds1"/>
<entry key="ds2" value-ref="ds2"/>
</map>
</property>
<!--配置默認數據源,在RoutingDataSource無法確定使用哪個數據源時,將會使用默認的數據源-->
<property name="defaultTargetDataSource" value="ds1"/>
</bean>
在實際開發中,我們通常不會直接操作數據源,而是與ORM框架進行整合,這里選用mybatis,因此需要添加mybatis相關配置
<!--配置mybatis的SqlSessionFactoryBean,注入datasource屬性引用的是routingDataSource-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="routingDataSource”/>
<!--注意,如果你sql寫在xml中,需要打開以下配置,本案例寫在映射器接口上-->
<!--<property name="mapperLocations" value="classpath*:config/sqlmap/**/*.xml" />-->
</bean>
<!--配置MapperScannerConfigurer-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!--UserMapper和UserAccountMapper位于此包中-->
<property name="basePackage" value="io.github.tianshouzhi.routing.mapper"/>
</bean>
UserMapper和UserAccountMapper,分別用于訪問db1.user表和db2.user_account表。之后我們就可以通過這兩個接口上添加@Routing注解,來讓其訪問不同的庫。
4 @Routing注解的基本使用
@Routing可以在方法上使用,也可以在類或者接口上使用,以下是Routing注解的使用說明
1 方法上添加@Routing注解
public interface UserMapper {
@Routing("ds1”)
//通過@Routing注解,指定此方法走ds1數據源
public User selectById(@Param("id") int id);
//未添加注解,將走默認的數據源,因為在配置RoutingDataSource時,已經指定了默認的數據源是ds1,所以可以成功訪問user表;
//注意:如果默認的數據源是ds2,此方法將會報錯,因為user表位于db1中,而不是db2中
public int insert(User user);
}
2 接口或者類上使用@Routing注解,內部定義的方法將都會繼承此注解
例如我們在UserAccountMapper接口上定義了@Routing注解,接口中定義的2個方法都會走ds2數據源訪問db2,而user_account位于db2中 :
@Routing("ds2")
public interface UserAccountMapper {
UserAccount selectById(@Param("id") int id);
int insert(UserAccount userAccount);
}
提示:通常我們一個Mapper接口操作的都是某個庫中的表,因此建議直接在接口上添加@Routing注解,而不是每個方法單獨添加
3 接口/方法上同時添加@Routing注解,方法上的@Routing注解優先于接口上的Routing注解
@Routing("ds2")
public interface UserAccountMapper {
//使用接口上@Routing注解指定的ds2數據源
UserAccount selectById(@Param("id") int id);
//使用方法上@Routing注解指定的ds1數據源
//注意:這是一個錯誤的示例,因為user_account表位于db2中
@Routing("ds1")
int insert(UserAccount userAccount);
}
4 使用包級別的@Routing注解
如果項目的目錄結構劃分的比較好,操作不同的庫的Mapper接口,位于不同的包下。如:
此時你可以使用包級別的@Routing注解,在包下面創建一個package-info.java,從而無需在每個接口上都定義@Routing注解。例如:
注:對于@Routing注解而言,優先級滿足以下條件:方法>接口>包
5 在業務層方法調用
Mapper映射器接口屬于dao層,通常dao層的代碼都是在service層進行調用的,業務層的接口也可以添加@Routing注解,如果沒有添加。則由調用的Mapper映射器方法、接口上的@Routing注解決定使用哪個ds,如果都沒有沒有定義,則使用默認的數據源
public void business(int userId,int userAccountId) {
userAccountMapper.selectById(userAccountId);
userMapper.selectById(userId);
}
6 業務層方法添加@Routing注解
業務層方法添加@Routing注解后,將忽略內部調用的Mapper映射器方法、接口上的Routing注解,如:
//指定方法內部調用的映射器接口,都必須使用ds2
@Routing("ds2")
public void business(int userId,int userAccountId) {
//user_account表位于db2中,因此訪問可以成功
System.out.println(userAccountMapper.selectById(userAccountId));
//注意:user表位于db1中,這里強制使用ds2,因此將執行失敗
System.out.println(userMapper.selectById(userId));
}
之所以一個方法使用了@Routing注解后,將會忽略內部調用的其他方法的@Routing注解,主要是為了與事務的語義兼容。
5 事務的支持
@Routing注解可以與spring的事務聯合使用,以下是聲明式事務@Transactional注解的案例,注意要保證事務中的方法必須都訪問的是同一個庫中的表。
以下是事務管理器的配置
<!--事務管理器配置,注意datasource屬性引用的也是routingDataSource-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="routingDataSource"/>
</bean>
<tx:annotation-driven />
1 只使用@Transactional注解,不使用@Routing注解
這將使用我們配置的默認數據源,如果訪問了其他庫中的表,將會報錯,也就是說,使用了@Transactional注解后,也會忽略內部調用的其他方法的@Routing注解。
@Transactional
public void testDefaultTransaction(User user,UserAccount userAcccount) {
//默認數據源是ds1,可以訪問db1中user表,因此插入成功
userMapper.insert(user);
//注意:這個方法將執行失敗,事務將回滾,因為user_account位于db2中
userAccountMapper.insert(userAcccount)
}
2 同時使用@Transactional/@Routing注解
spring事務管理器將會使用@Routing注解中指定的數據源來開啟事務
@Transactional
@Routing("ds2”) //使用ds2開啟事務
public void testRoutingTransaction(User user,UserAccount userAcccount) {
userAccountMapper.insert(userAccount);
//注意:這個方法將執行失敗,事務將回滾,因為user位于db1中
userMapper.insert(user);
}
6 RoutingDataSource的完整使用案例
完整使用案例,可從項目的開源github地址獲取:http://github.com/tianshouzhi/routing-datasource,也可以通過git 克隆項目源碼:
git clone http://github.com/tianshouzhi/routing-datasource.git
相關代碼位于test目下:
以下是相關建表語句:
CREATE DATABASE `db1`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE DATABASE `db2`;
CREATE TABLE `user_account` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`account` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
免費學習視頻歡迎關注云圖智聯:http://e.yuntuzhilian.com/