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); } }
至此,在尽可能少冗余代码的状况下,知足并发状况下分库需求。若是有更优方案,欢迎交流。