mybatis做爲流行的ORM框架,項目實際使用過程當中可能會遇到分庫分表的場景。mybatis在分表,甚至是同主機下的分庫均可以說是完美支持的,只須要將表名或者庫名做爲動態參數組裝sql就可以完成。可是多餘分在不一樣主機上的庫,就不太同樣了,組裝sql沒法區分數據庫主機。網上搜索了一下,對於此類狀況,大都採用的動態數據源的概念,也即定義不一樣的數據源鏈接不一樣的主機數據庫,在查詢前經過動態數據源進行數據源切換,但從實現上來看,這個切換並非單sql級別的,而能夠理解爲時間級別的切換,即查詢前切到對應數據源,這種實如今併發場景下並不能知足分庫減壓需求,甚至會致使查錯數據庫的狀況。mysql
這裏給出分庫分表的實現方式,特別在分庫的方案上,採用真正可併發的方案。spring
這裏以銀行卡消費記錄爲例子來看這個問題,銀行有多個用戶,經過Card( id,owner) 來標誌,每一個卡有消費記錄,CostLog(id,time,amount) ,因爲消費記錄數據過多,咱們對數據進行分庫分表存儲。sql
1、基本配置數據庫
首先咱們來看下mybatis結合springmvc的基本配置方式(不進行分庫分表)。apache
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>
上面配置中須要咱們本身定義的 內容有session
1.session工廠中的數據訪問映射文件,這裏須要符合配置中命名規範並放在對應路徑下,以Mapper.xml結尾,能夠叫作 CostLogMapper.xmlmybatis
<?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.上面兩項所依賴的數據對象 CostLogmvc
@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); } }
2、不分主機的分庫表實現
對於上例,咱們只須要在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) { //分兩庫兩表db一、db2,每一個庫中又有兩個表tb一、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); } }
3、分主機的分庫實現
首先經過需求確認幾點:
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) { //分兩庫兩表db一、db2,每一個庫中又有兩個表tb一、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); } }
至此,在儘可能少冗餘代碼的狀況下,知足併發狀況下分庫需求。若是有更優方案,歡迎交流。