需求场景
由于业务体量较大,数据增长较快,所以需要把用户数据拆分到不同的库表中去,减轻数据库压力。
分库分表操作主要有垂直拆分和水平拆分:
- 垂直拆分:指按照业务将表进行分类,分布到不同的数据库上,这样也就将数据的压力分担到不同的库上面。最终一个数据库由很多表的构成,每个表对应着不同的业务,也就是专库专用。
- 水平拆分:如果垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而水平拆分是把同一个表拆到不同的数据库中。如:user_001、user_002
项目地址
https://gitee.com/charles_ruan/db-router
快速入门
- 下载项目,maven clean install
- 引入依赖
<dependency>
<groupId>com.charles</groupId>
<artifactId>db-router-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
- 配置文件
mini-db-router:
jdbc:
datasource:
dbCount: 2
tbCount: 4
default: db00
routerKey: uId
list: db01,db02
db00:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/chalres?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC&useSSL=true
username: root
password: root
db01:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db1?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC&useSSL=true
username: root
password: root
db02:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db2?useUnicode=true&characterEncoding=utf8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC&useSSL=true
username: root
password: root
- 使用注解
Mapper
@DBRouterStrategy(splitTable = true)
public interface UserMapper extends BaseMapper<UserInfo> {
List<UserInfo> getList();
@DBRouter(key = "userName")
void insertUser(UserInfo userInfo);
}
Mapper.xml
<insert id="insertUser" parameterType="com.charles.entity.UserInfo">
insert into cls_user (user_name, password) values(#{userName},#{password})
</insert>
源码解析
DBRouterJoinPoint#dbRouter
。切面类。根据注解上的指定key从参数上获取值,根据dbKey进行路由,决定使用的表名和库名。
@Around("aopPoint() && @annotation(dbRouter)")
public Object dbRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
String dbKey = dbRouter.key();
if (StringUtils.isBlank(dbKey) && StringUtils.isBlank(dbRouterConfig.getRouterKey())) {
throw new RuntimeException("annotation DBRouter key is null!");
}
dbKey = StringUtils.isNotBlank(dbKey)? dbKey: dbRouterConfig.getRouterKey();
// 路由属性
String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
// 路由策略
dbRouterStrategy.doRouter(dbKeyAttr);
// 返回结果
try {
return jp.proceed();
} finally {
dbRouterStrategy.clear();
}
}
DBRouterStrategyHashCode#doRouter
,进行路由切换。
@Override
public void doRouter(String dbKeyAttr) {
int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
// 扰动函数,hashmap也采用此函数进行散列
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
// 库表索引
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
// 设置到ThreadLocal
setDBKey(dbIdx);
setTBKey(tbIdx);
logger.info("数据库路由 dbIdx: {} tbIdx: {}", dbIdx, tbIdx);
}
DynamicMybatisPlugin#intercept
,判断是否存在DBRouterStrategy
注解,获取sql,用正则表达式进行修改sql。修改数据表的表名。
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})})
public class DynamicMybatisPlugin implements Interceptor {
private Pattern pattern = Pattern.compile("(from|into|update)[\\s]{1,}(\\w{1,})", Pattern.CASE_INSENSITIVE);
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 获取自定义注解判断是否进行分表操作
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
Class<?> clazz = Class.forName(className);
DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()) {
return invocation.proceed();
}
// 获取SQL
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
// 替换SQL表名 USER 为 USER_(动态获取)
Matcher matcher = pattern.matcher(sql);
String tableName = null;
if (matcher.find()) {
tableName = matcher.group().trim();
}
assert null != tableName;
String replaceSql = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());
// 通过反射修改SQL语句
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, replaceSql);
field.setAccessible(false);
return invocation.proceed();
}
}
DynamicDataSource
进行数据源的切换
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return "db" + DBContextHolder.getDBKey();
}
}