mybatis作為流行的ORM框架,項目實際使用過程中可能會遇到分庫分表的場景。mybatis在分表,甚至是同主機下的分庫都可以說是完美支持的,只需要將表名或者庫名作為動態參數組裝sql就能夠完成。但是多余分在不同主機上的庫,就不太一樣了,組裝sql無法區分數據庫主機。網上搜索了一下,對於此類情況,大都采用的動態數據源的概念,也即定義不同的數據源連接不同的主機數據庫,在查詢前通過動態數據源進行數據源切換,但從實現上來看,這個切換並不是單sql級別的,而可以理解為時間級別的切換,即查詢前切到對應數據源,這種實現在並發場景下並不能滿足分庫減壓需求,甚至會導致查錯數據庫的情況。
這里給出分庫分表的實現方式,特別在分庫的方案上,采用真正可並發的方案。
這里以銀行卡消費記錄為例子來看這個問題,銀行有多個用戶,通過Card( id,owner) 來標志,每個卡有消費記錄,CostLog(id,time,amount) ,由於消費記錄數據過多,我們對數據進行分庫分表存儲。
一、基本配置
首先我們來看下mybatis結合springmvc的基本配置方式(不進行分庫分表)。
mybatis的配置鏈路可以有底層到上層解釋為: DB(數據庫對接信息) -》數據源(數據庫連接池配置) -》session工廠(連接管理與數據訪問映射關聯) -》DAO(業務訪問封裝)
<!--定義mysql 數據源,連接數據庫主機的連接信息 --> <bean id="test1-datasource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> <property name="maxActive" value="40"></property> <property name="maxIdle" value="30"></property> <property name="maxWait" value="30000"></property> <property name="minIdle" value="2"/> <property name="timeBetweenEvictionRunsMillis" value="3600000"></property> <property name="minEvictableIdleTimeMillis" value="3600000"></property> <property name="defaultAutoCommit" value="true"></property> <property name="testOnBorrow" value="true"></property> <property name="validationQuery" value="select 1"/> </bean> <!--定義session工廠,指定數據訪問映射文件和使用的數據源--> <bean id="test1-sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="mapperLocations"> <list> <value>classpath*:confMapper/*Mapper.xml</value> </list> </property> <property name="dataSource" ref="test1-datasource"/> </bean> <!--定義session工廠和DAO掃描路徑,自動進行DAO與session工廠的綁定--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.ming.test.po"/> <property name="sqlSessionFactoryBeanName" value="test1-sqlSessionFactory"/> </bean>
上面配置中需要我們自己定義的 內容有
1.session工廠中的數據訪問映射文件,這里需要符合配置中命名規范並放在對應路徑下,以Mapper.xml結尾,可以叫做 CostLogMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="CostDao"> <resultMap id="BaseResultMap" type="CostLog"> <result property="id" column="id"/> <result property="time" column="time"/> <result property="amount" column="amount"/> </resultMap> <select id="queryCostLog" resultMap="BaseResultMap"> SELECT `id`,`time`,`amount` FROM CostLog WHERE `id` = #{id} </select> </mapper>
2.掃描綁定中 basePackage指定的包名下的DAO類
public interface CostDao { CostLog queryCostLog(@Param("id") int id); }
3.上面兩項所依賴的數據對象 CostLog
@Setter @Getter public class CostLog { private Integer id; private Date time; private Integer amount; }
4.對應的數據庫表
這里我們和 CostLog 使用同樣的命名
我們可以使用如下代碼訪問:
@Service public class CostLogService { @Resource CostDao costDao; public CostLog queryCostDao(int id) { return costDao.queryCostLog(id); } }
二、不分主機的分庫表實現
對於上例,我們只需要在DAO中增加庫表名參數,並適當修改SQL即可
數據訪問映射配置寫法:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="CostDao"> <resultMap id="BaseResultMap" type="CostLog"> <result property="id" column="id"/> <result property="time" column="time"/> <result property="amount" column="amount"/> </resultMap> <select id="queryCostLog" resultMap="BaseResultMap"> SELECT `id`,`time`,`amount` FROM ${dbName}.${tbName} WHERE `id` = #{id} </select> </mapper>
DAO類寫法:
public interface CostDao { CostLog queryCostLog(@Param("dbName") String dbName, @Param("tbName") String tbName, @Param("id") int id); }
調用層計算庫表名稱,並傳遞參數:
@Service public class CostLogService { @Resource CostDao costDao; public CostLog queryCostDao(int id) { //分兩庫兩表db1、db2,每個庫中又有兩個表tb1、tb2,我們根據賬戶id模4的取模值來分庫表,0:db1.tb1 ;1:db1.tb2;2:db2.tb1;3:db2.tb2 String dbName = id % 4 < 2 ? "db1" : "db2"; String tbName = id % 2 == 0 ? "tb1" : "tb2"; return costDao.queryCostLog(dbName, tbName, id); } }
三、分主機的分庫實現
首先通過需求確認幾點:
1.我們期望不同的查詢根據id自動到不同的主機上去查詢,也就是db1和db2在不同的主機上
2.我們分庫目的是數據庫減負並且會有並發訪問,因此db1和db2要能夠同時提供服務
鑒於第一點,我們需要定義兩個數據源,同時分別連接不同的數據庫主機。
鑒於第二點,我們需要將數據源的選擇細化到單個請求。
a.一種是將邏輯封裝到DAO中實現,使DAO進行訪問前根據請求參數按照我們定義的邏輯選擇數據源。遺憾的是,DAO的具體實現是又mybatis動態代理生成的,這個功能依賴mybatis的支持,我目前並不知道mybatis有提供這么一個功能。
b.另一種是定義兩個DAO,分別連接不同的數據源,但是兩個DAO的查詢邏輯是完全一樣的。我們采用這種方式。
一種實現是我們定義兩套完全相同的數據映射配置和兩個DAO接口,分別連接不同的數據源,但這種方式實際上會有較多的重復配置,如果分庫不止兩個,而是多個,那么后續維護修改就更加困難。有沒有辦法讓多個DAO使用同一個數據訪問映射文件呢,經過測試,是有的,甚至多個DAO接口可以繼承同一個DAO接口的實現(通過DAO注解直接定義訪問邏輯)。
我們可以定義一個父級DAO接口A,然后為每個分庫定義一個空的DAO接口,每個接口都繼承接口A。如下,我們定義 Db1CostDao 和 Db2CostDao 都繼承 CostDao。

子接口只需掛一個名字,而無需有額外實現
public interface Db1CostDao extends CostDao { }
然后我們在各個數據源的MapperScannerConfigurer配置中,將各個子接口關聯到不同的分庫session工廠上。而在數據訪問映射文件中,我們定義的DAO類型為父級DAO接口A。這樣在spring啟動掃描時,由於每個子DAO都是接口A的子接口,因此每個子DAO都實例化為一個bean,我們可以在數據訪問業務層通過自定義邏輯返回對應的DAO。最終查詢的數據庫為對應的子DAO接口所對應的數據庫。
<!--定義mysql 數據源,連接數據庫主機的連接信息 -->
<bean id="test1-datasource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="maxActive" value="40"></property>
<property name="maxIdle" value="30"></property>
<property name="maxWait" value="30000"></property>
<property name="minIdle" value="2"/>
<property name="timeBetweenEvictionRunsMillis" value="3600000"></property>
<property name="minEvictableIdleTimeMillis" value="3600000"></property>
<property name="defaultAutoCommit" value="true"></property>
<property name="testOnBorrow" value="true"></property>
<property name="validationQuery" value="select 1"/>
</bean>
<!--定義session工廠,指定數據訪問映射文件和使用的數據源-->
<bean id="test1-sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="mapperLocations">
<list>
<value>classpath*:confMapper/*Mapper.xml</value>
</list>
</property>
<property name="dataSource" ref="test1-datasource"/>
</bean>
<!--定義session工廠和DAO掃描路徑,自動進行DAO與session工廠的綁定-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="test.dao.db1"/>
<property name="sqlSessionFactoryBeanName" value="test1-sqlSessionFactory"/>
</bean>
<!--定義mysql 數據源,連接數據庫主機的連接信息 -->
<bean id="test2-datasource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="maxActive" value="40"></property>
<property name="maxIdle" value="30"></property>
<property name="maxWait" value="30000"></property>
<property name="minIdle" value="2"/>
<property name="timeBetweenEvictionRunsMillis" value="3600000"></property>
<property name="minEvictableIdleTimeMillis" value="3600000"></property>
<property name="defaultAutoCommit" value="true"></property>
<property name="testOnBorrow" value="true"></property>
<property name="validationQuery" value="select 1"/>
</bean>
<!--定義session工廠,指定數據訪問映射文件和使用的數據源-->
<bean id="test2-sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="mapperLocations">
<list>
<value>classpath*:confMapper/*Mapper.xml</value>
</list>
</property>
<property name="dataSource" ref="test1-datasource"/>
</bean>
<!--定義session工廠和DAO掃描路徑,自動進行DAO與session工廠的綁定-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="test.dao.db2"/>
<property name="sqlSessionFactoryBeanName" value="test2-sqlSessionFactory"/>
</bean>
映射文件 CostLogMapper.xml則無需做任何修改。
在業務層我們通過自定義邏輯選擇DAO
@Service public class CostLogService { @Resource Db1CostDao costDao1; @Resource Db2CostDao costDao2; CostDao selectDao(int id) { return id % 4 < 2 ? costDao1 : costDao2; } public CostLog queryCostDao(int id) { //分兩庫兩表db1、db2,每個庫中又有兩個表tb1、tb2,我們根據賬戶id模4的取模值來分庫表,0:db1.tb1 ;1:db1.tb2;2:db2.tb1;3:db2.tb2 String dbName = id % 4 < 2 ? "db1" : "db2"; String tbName = id % 2 == 0 ? "tb1" : "tb2"; return selectDao(id).queryCostLog(dbName, tbName, id); } }
至此,在盡量少冗余代碼的情況下,滿足並發情況下分庫需求。如果有更優方案,歡迎交流。
