上一篇介绍了读写分离,这一篇来说一下分库分表,废话不多说直接上代码。
1.首先配置文件,这里配置三个库
driver=com.mysql.jdbc.Driver
#定义初始连接数
initialSize=0
#定义最大连接数
maxActive=20
#定义最大空闲
maxIdle=20
#定义最小空闲
minIdle=1
#定义最长等待时间
maxWait=60000
jdbc.mysql.url0=jdbc:mysql://localhost:3306/test_02?createDatabaseIfNotExist=true&characterEncoding=utf-8&useUnicode=true
jdbc.mysql.username0=root
jdbc.mysql.password0=root
jdbc.mysql.url1=jdbc:mysql://localhost:3306/test_00?createDatabaseIfNotExist=true&characterEncoding=utf-8&useUnicode=true
jdbc.mysql.username1=root
jdbc.mysql.password1=root
jdbc.mysql.url2=jdbc:mysql://localhost:3306/test_01?createDatabaseIfNotExist=true&characterEncoding=utf-8&useUnicode=true
jdbc.mysql.username2=root
jdbc.mysql.password2=root
在每个库下边建立三个表如下图
2.配置文件写好之后接下来配置数据源
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 引入配置文件 -->
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:props/jdbc.properties"/>
</bean>
<bean id="dataSource0" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${jdbc.mysql.url0}"/>
<property name="username" value="${jdbc.mysql.username0}"/>
<property name="password" value="${jdbc.mysql.password0}"/>
<!-- 初始化连接大小 -->
<property name="initialSize" value="${initialSize}"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="${maxActive}"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="${maxIdle}"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="${minIdle}"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="${maxWait}"></property>
</bean>
<bean id="dataSource1" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${jdbc.mysql.url1}"/>
<property name="username" value="${jdbc.mysql.username1}"/>
<property name="password" value="${jdbc.mysql.password1}"/>
<!-- 初始化连接大小 -->
<property name="initialSize" value="${initialSize}"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="${maxActive}"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="${maxIdle}"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="${minIdle}"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="${maxWait}"></property>
</bean>
<bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${jdbc.mysql.url2}"/>
<property name="username" value="${jdbc.mysql.username2}"/>
<property name="password" value="${jdbc.mysql.password2}"/>
<!-- 初始化连接大小 -->
<property name="initialSize" value="${initialSize}"></property>
<!-- 连接池最大数量 -->
<property name="maxActive" value="${maxActive}"></property>
<!-- 连接池最大空闲 -->
<property name="maxIdle" value="${maxIdle}"></property>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="${minIdle}"></property>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="${maxWait}"></property>
</bean>
<!-- 动态获取数据源 -->
<bean id="mysqlDynamicDataSource" class="com.wz.dbRouting.db.DynamicDataSource">
<property name="targetDataSources">
<!-- 标识符类型 -->
<map>
<entry key="db0" value-ref="dataSource0"/>
<entry key="db1" value-ref="dataSource1"/>
<entry key="db2" value-ref="dataSource2"/>
</map>
</property>
</bean>
<bean id="dbRuleSet" class="com.wz.dbRouting.bean.RouterSet">
<property name="routeFieldStart" value="0"></property>
<property name="routeFieldEnd" value="9200000000000000000"></property>
<property name="dbNumber" value="3"></property>
<property name="routeType" value="2"></property>
<property name="ruleType" value="3"></property>
<property name="tableNumber" value="5"></property>
<property name="dbKeyArray">
<list>
<value>db0</value>
<value>db1</value>
<value>db2</value>
</list>
</property>
</bean>
<!--事务-->
<bean id="baiTiaoTransactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="mysqlDynamicDataSource"></property>
</bean>
<bean id="btTransactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="baiTiaoTransactionManager"></property>
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"></property>
</bean>
<!-- spring和MyBatis完美整合,不需要mybatis的配置映射文件 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="mysqlDynamicDataSource"/>
<!-- 自动扫描mapping.xml文件 -->
<property name="mapperLocations" value="classpath:com/wz/mapping/*.xml"></property>
</bean>
<!-- DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.wz.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
</bean>
</beans>
动态获取数据源,这个和上篇读写分离一样,也是用treadLocal保证线程安全
package com.wz.dbRouting;
/**
* 动态数据源实现中KEY的存放工具类
* 动态数据源实现中KEY的存放工具类:使用treadLocal的方式来保证线程安全
*/
public class DbContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
private static final ThreadLocal<String> tableIndexHolder= new ThreadLocal<String>();
public static void setDbKey(String dbKey) {
contextHolder.set(dbKey);
}
public static String getDbKey() {
return (String) contextHolder.get();
}
public static void clearDbKey() {
contextHolder.remove();
}
public static void setTableIndex(String tableIndex){
tableIndexHolder.set(tableIndex);
}
public static String getTableIndex(){
return (String) tableIndexHolder.get();
}
public static void clearTableIndex(){
tableIndexHolder.remove();
}
}
package com.wz.dbRouting.db;
import com.wz.dbRouting.DbContextHolder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.logging.Logger;
/**
* @Description Spring 的动态数据源的实现
* @Autohr wz
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
public static final Logger logger = Logger.getLogger(DynamicDataSource.class.toString());
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbKey();//获取当前数据源
}
}
设置一些分库分表需要的参数
package com.wz.dbRouting.bean;
import java.util.List;
/**
* @Description
* @Autohr wz
*/
public class RouterSet {
/**根据字符串*/
public final static int RULE_TYPE_STR=3;
public final static int ROUTER_TYPE_DB=0;
public final static int ROUTER_TYPE_TABLE =1;
public final static int ROUTER_TYPE_DBANDTABLE=2;
/**数据库表的逻辑KEY,与数据源MAP配置中的key一致*/
private List<String> dbKeyArray;
/**数据库数量*/
private int dbNumber;
/**数据表数量*/
private int tableNumber;
/**数据表index样式*/
private String tableIndexStyle;
/**Id开始*/
private String routeFieldStart;
/**Id结束*/
private String routeFieldEnd;
/**规则类型*/
private int ruleType;
/**路由类型类型*/
private int routeType;
public static int getRULE_TYPE_STR() {
return RULE_TYPE_STR;
}
public static int getROUTER_TYPE_DB() {
return ROUTER_TYPE_DB;
}
public static int getROUTER_TYPE_TABLE() {
return ROUTER_TYPE_TABLE;
}
public static int getROUTER_TYPE_DBANDTABLE() {
return ROUTER_TYPE_DBANDTABLE;
}
public List<String> getDbKeyArray() {
return dbKeyArray;
}
public void setDbKeyArray(List<String> dbKeyArray) {
this.dbKeyArray = dbKeyArray;
}
public int getDbNumber() {
return dbNumber;
}
public void setDbNumber(int dbNumber) {
this.dbNumber = dbNumber;
}
public int getTableNumber() {
return tableNumber;
}
public void setTableNumber(int tableNumber) {
this.tableNumber = tableNumber;
}
public String getTableIndexStyle() {
return tableIndexStyle;
}
public void setTableIndexStyle(String tableIndexStyle) {
this.tableIndexStyle = tableIndexStyle;
}
public String getRouteFieldStart() {
return routeFieldStart;
}
public void setRouteFieldStart(String routeFieldStart) {
this.routeFieldStart = routeFieldStart;
}
public String getRouteFieldEnd() {
return routeFieldEnd;
}
public void setRouteFieldEnd(String routeFieldEnd) {
this.routeFieldEnd = routeFieldEnd;
}
public int getRuleType() {
return ruleType;
}
public void setRuleType(int ruleType) {
this.ruleType = ruleType;
}
public int getRouteType() {
return routeType;
}
public void setRouteType(int routeType) {
this.routeType = routeType;
}
}
3.下面开始是自定义注解
设置一个路由常量按照userNum来分表
package com.wz.dbRouting.annotation;
/**
* @Description
*/
public class RouterConstants {
public static final String ROUTER_FIELD_DEFAULT = "userNum";
public static final String ROUTER_TABLE_SUFFIX_DEFAULT = "_0000";
}
package com.wz.dbRouting.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description
* @Autohr wz
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Router {
String routerField() default RouterConstants.ROUTER_FIELD_DEFAULT;
String tableStyle() default RouterConstants.ROUTER_TABLE_SUFFIX_DEFAULT;
}
4.接下来是拦截器以及分库分表的规则。
主要思路是:通过拦截器来拦截方法名称以及方法参数,根据参数中的userNum字段,对该字段进行hashcode求余的方式来判断这条记录是存在几库几表。具体规则见代码
拦截器代码
package com.wz.dbRouting;
import com.wz.dbRouting.annotation.Router;
import com.wz.dbRouting.annotation.RouterConstants;
import com.wz.dbRouting.router.RouterUtils;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* @Description 切面切点 在Router注解的方法执行前执行 切点织入
* @Autohr wz
*/
@Aspect
@Component
public class DBRouterInterceptor {
private static final Logger log = LoggerFactory.getLogger(DBRouterInterceptor.class);
@Autowired
private DBRouter dBRouter;
@Pointcut("@annotation( com.wz.dbRouting.annotation.Router)")
public void aopPoint() {
}
@Before("aopPoint()")
public Object doRoute(JoinPoint jp) throws Throwable {
long t1 = System.currentTimeMillis();
boolean result = true;
//根据JoinPoint jp 获取方法名称和参数
Method method = getMethod(jp);
Router router = method.getAnnotation(Router.class);
String routeField = router.routerField();
Object[] args = jp.getArgs();
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
long t2 = System.currentTimeMillis();
//通过反射得到对象args[i] 的 routeField 字段的值
String routeFieldValue = BeanUtils.getProperty(args[i],
routeField);
log.debug("routeFieldValue{}" + (System.currentTimeMillis() - t2));
if (StringUtils.isNotEmpty(routeFieldValue)) {
//看这个值是否为默认的分库分表字段,如果是设置库和表的名称
if (RouterConstants.ROUTER_FIELD_DEFAULT.equals(routeField)) {
//根据hashcode取%
dBRouter.doRouteByResource("" + RouterUtils.getResourceCode(routeFieldValue));
break;
}
}
}
}
log.debug("doRouteTime{}" + (System.currentTimeMillis() - t1));
return result;
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature msig = (MethodSignature) sig;
return getClass(jp).getMethod(msig.getName(), msig.getParameterTypes());
}
private Class<? extends Object> getClass(JoinPoint jp)
throws NoSuchMethodException {
return jp.getTarget().getClass();
}
}
具体规则代码
package com.wz.dbRouting;
/**
* @Description DB路由接口 DB路由器接口,通过调用该接口来自动判断数据位于哪个服务器
* @Autohr wz
*/
public interface DBRouter {
/**
* 进行路由
* @param fieldId
* @return
* @throws
*/
public String doRoute(String fieldId);
public String doRouteByResource(String resourceCode);
}
package com.wz.dbRouting.router;
import com.wz.dbRouting.DBRouter;
import com.wz.dbRouting.DbContextHolder;
import com.wz.dbRouting.annotation.RouterConstants;
import com.wz.dbRouting.bean.RouterSet;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.DecimalFormat;
import java.util.List;
/**
* @Description 根据指定变量动态切 库和表
* @Autohr supers【weChat:13031016567】
*/
@Service("dBRouter")
public class DBRouterImpl implements DBRouter {
private static final Logger log = LoggerFactory.getLogger(DBRouterImpl.class);
/**
* 配置列表
*/
@Autowired
private List<RouterSet> routerSetList;
public String doRoute(String fieldId) {
if (StringUtils.isEmpty(fieldId)) {
throw new IllegalArgumentException("dbsCount and tablesCount must be both positive!");
}
int routeFieldInt = RouterUtils.getResourceCode(fieldId);
String dbKey = getDbKey(routerSetList, routeFieldInt);
return dbKey;
}
public String doRouteByResource(String resourceCode) {
if (StringUtils.isEmpty(resourceCode)) {
throw new IllegalArgumentException("dbsCount and tablesCount must be both positive!");
}
int routeFieldInt = Integer.valueOf(resourceCode);
String dbKey = getDbKey(routerSetList, routeFieldInt);
return dbKey;
}
/**
* @Description 根据数据字段来判断属于哪个段的规则,获得数据库key
* @Autohr wz
*/
private String getDbKey(List<RouterSet> routerSets, int routeFieldInt) {
RouterSet routerSet = null;
if (routerSets == null || routerSets.size() <= 0) {
throw new IllegalArgumentException("dbsCount and tablesCount must be both positive!");
}
String dbKey = null;
for (RouterSet item : routerSets) {
if (item.getRuleType() == routerSet.RULE_TYPE_STR) {
routerSet = item;
if (routerSet.getDbKeyArray() != null && routerSet.getDbNumber() != 0) {
long dbIndex = 0;
long tbIndex = 0;
//默认按照分库进行计算
long mode = routerSet.getDbNumber();
//如果是按照分库分表的话,计算
if (item.getRouteType() == RouterSet.ROUTER_TYPE_DBANDTABLE && item.getTableNumber() != 0) {
mode = routerSet.getDbNumber() * item.getTableNumber();
dbIndex = routeFieldInt % mode / item.getTableNumber();
tbIndex = routeFieldInt % item.getTableNumber();
String tableIndex = getFormateTableIndex(item.getTableIndexStyle(), tbIndex);
DbContextHolder.setTableIndex(tableIndex);
} else if (item.getRouteType() == RouterSet.ROUTER_TYPE_DB) {
mode = routerSet.getDbNumber();
dbIndex = routeFieldInt % mode;
} else if (item.getRouteType() == RouterSet.ROUTER_TYPE_TABLE) {
tbIndex = routeFieldInt % item.getTableNumber();
String tableIndex = getFormateTableIndex(item.getTableIndexStyle(), tbIndex);
DbContextHolder.setTableIndex(tableIndex);
}
dbKey = routerSet.getDbKeyArray().get(Long.valueOf(dbIndex).intValue());
log.debug("getDbKey resource:{}------->dbkey:{},tableIndex:{},", new Object[]{routeFieldInt, dbKey, tbIndex});
DbContextHolder.setDbKey(dbKey);
}
break;
}
}
return dbKey;
}
/**
* @Description 此方法是将例如+++0000根式的字符串替换成传参数字例如44 变成+++0044
* @Autohr supers【weChat:13031016567】
*/
private static String getFormateTableIndex(String style, long tbIndex) {
String tableIndex = null;
DecimalFormat df = new DecimalFormat();
if (StringUtils.isEmpty(style)) {
style = RouterConstants.ROUTER_TABLE_SUFFIX_DEFAULT;//在格式后添加诸如单位等字符
}
df.applyPattern(style);
tableIndex = df.format(tbIndex);
return tableIndex;
}
public List<RouterSet> getRouterSetList() {
return routerSetList;
}
public void setRouterSetList(List<RouterSet> routerSetList) {
this.routerSetList = routerSetList;
}
}
现在分库分表的规则就告一段落了,接下来进行单元测试。
5.控制台打印:
log4j:WARN No appenders could be found for logger (org.springframework.test.context.junit4.SpringJUnit4ClassRunner).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
db2库 _0003表 的插入结果:1
分库分表成功!