SpringMVC + MyBatis分庫分表方案


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

 

 至此,在盡量少冗余代碼的情況下,滿足並發情況下分庫需求。如果有更優方案,歡迎交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM