SpringMVC + MyBatis分庫分表方案

  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);
    }
}

 

 至此,在儘可能少冗餘代碼的狀況下,知足併發狀況下分庫需求。若是有更優方案,歡迎交流。

相關文章
相關標籤/搜索