分库分表是常见的数据库水平扩展方案之一,Mybatis实现分表插件,可以对数据库进行动态分表,方便进行扩展和管理。下面我将为您详细介绍如何实现Mybatis分表插件,并提供两条示例。
什么是Mybatis分表插件?
Mybatis分表插件是一种Mybatis的插件机制,可以应对分表的需求。通常情况下,将业务数据切分到多个表中,可以极大地提高多线程并发执行时的效率,降低业务处理系统的锁冲突率,从而提高系统的吞吐量和响应速度。
Mybatis分表插件的实现步骤
1. 自定义分表插件类
创建自定义的分表插件类MybatisPlugin,需要继承自Interceptor,并实现其intercept()方法:
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Properties;
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
@Component
@Data
public class MybatisPlugin implements Interceptor {
public static final String TABLE_SUFFIX = "_$actualTableSuffix"; // 字段名称为指定数据源所对应的真实表名的后缀
private HashMap<String, String> tableSuffix = new HashMap<>(); // 存储数据源所对应的真实表名
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取参数
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
// 重组sql
String newSql = rebuildSql(sql, boundSql, ms.getConfiguration());
// 通过反射修改实际操作的数据表名称
Field field = ms.getClass().getDeclaredField("mappedStatement");
field.setAccessible(true);
MappedStatement mappedStatement = (MappedStatement) field.get(ms);
Field sqlSourceField = mappedStatement.getClass().getDeclaredField("sqlSource");
sqlSourceField.setAccessible(true);
updateParameter(mappedStatement.getConfiguration(), newSql, (String) tableSuffix.get(mappedStatement.getId()));
// 执行SQL
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
private String rebuildSql(String sql, BoundSql boundSql, org.apache.ibatis.session.Configuration configuration) {
// 获取对应的实际表名
String actualTableName = (String) tableSuffix.get(configuration.getEnvironment().getId());
if (StringUtils.isEmpty(actualTableName)) {
actualTableName = "table1"; // 使用默认的表名
}
String newSql = sql.replaceAll("tableName", actualTableName);
return newSql;
}
private void updateParameter(org.apache.ibatis.session.Configuration configuration, String sql, String table) throws NoSuchFieldException, IllegalAccessException {
// 通过BoundSql重置缓存的sql
Field field = BoundSql.class.getDeclaredField("sql");
field.setAccessible(true);
field.set(configuration, sql);
// 通过MappedStatement重置缓存的BoundSql
Field field2 = MappedStatement.class.getDeclaredField("sqlSource");
field2.setAccessible(true);
field2.set(configuration.getMappedStatements().get(0), buildSqlSource(configuration, table, sql));
}
private Object buildSqlSource(org.apache.ibatis.session.Configuration configuration, String table, String sql) {
SqlSource sqlSource = new StaticSqlSource(configuration, sql, null);
return new RawSqlSource(configuration, sqlSource, table);
}
}
上述代码是自定义的分表插件类的具体实现方式,主要通过intercept()方法实现对$sql替换为真实表名。
2. 对应Mapper.xml创建拦截器
创建对应的Mapper.xml,使用拦截器来处理Mybatis分表插件,在对应的operationType触发时通过plugin拦截器进行切入:
<update id="save" parameterType="com.spring.mybatis.domain.User">
insert into tableName(id,name,password) values(#{id},#{name},#{password})
</update>
<update id="save" parameterType="com.spring.mybatis.domain.User" >
<plugin interceptor="com.spring.mybatis.plugin.MybatisPlugin" />
</update>
3. 测试分表插件
测试分表插件。对于下面这两条示例SQL:
INSERT INTO `tableName` (userId, name, age, sex)values(2,'Tom',19,'M')
INSERT INTO `tableName` (userId, name, age, sex)values(3,'Jim',20,'F')
当分表插件生效时,通过指定数据源,便可对不同的表进项插入操作。
示例一:实现Mybatis分表插件
假设有 A、B 两个表,在service层中,指定分表插件并设置表名。 当使用 insert into A 表时,自动向 B 表中插入值。
首先自定义插件注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InsertClient {
String name() default "";
}
针对不同的业务场景,创建相应的注解。下面是自定义分表插件类实现代码:
@Component
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class MybatisSplitTableInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(MybatisSplitTableInterceptor.class);
private static final String TABLE_NAME = "_A";
@Override
public Object intercept(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
final MappedStatement ms = (MappedStatement) args[0];
final Object parameter = args[1];
final MapperMethod mapperMethod = new MapperMethod(ms.getConfiguration().getMapperRegistry(), ms, parameter);
final InsertClient insertClient = mapperMethod.getMethod().getAnnotation(InsertClient.class);
if (insertClient == null) {
return invocation.proceed();
}
final BoundSql boundSql = ms.getBoundSql(parameter);
final String originalSql = boundSql.getSql();
final String mappedSql = getMappedSql(originalSql, TABLE_NAME);
for (int i = 0; i < boundSql.getParameterMappings().size(); i++) {
final ParameterMapping parameterMapping = boundSql.getParameterMappings().get(i);
final String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
logger.info("跳过多余的参数:{}={}", propertyName, boundSql.getAdditionalParameter(propertyName));
} else {
final Object parameterObject = boundSql.getParameterObject();
final MetaObject metaObject = parameterObject instanceof Map ? null : metaObjectFor(parameterObject);
final Object value = metaObject == null ? null : metaObject.getValue(propertyName);
logger.info("参数:{}={}", propertyName, value);
boundSql.setAdditionalParameter(propertyName, value);
}
}
logger.info("新SQL:{}", mappedSql);
final BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), mappedSql, boundSql.getParameterMappings(), boundSql.getParameterObject());
final MappedStatement newMs = copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
args[0] = newMs;
return invocation.proceed();
}
private static String getMappedSql(String originalSql, String tableName) {
final StringBuilder sb = new StringBuilder(originalSql);
if (originalSql.contains("values")) {
final int index = sb.indexOf("(") + 1;
sb.insert(index, "id,");
final int endIndex = sb.indexOf(")");
sb.insert(endIndex, ",'" + tableName + "'");
}
sb.insert(sb.indexOf(tableName), "_");
return sb.toString().replaceFirst(tableName, tableName.toLowerCase());
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties props) {
logger.info(props.toString());
}
private static class BoundSqlSqlSource implements SqlSource {
private final BoundSql boundSql;
public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
}
@Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}
完成自定义分表插件的代码,即可在使用A表的时候,直接插入到B表中。
示例二:另类分表插件的实现
现有几个表,分别是(weibo_201701,weibo_201702,weibo_210703,weibo_201704)等等。如果系统中需要查询某段时间范围内的数据(如201702、201703)时,直接操作查询对应的表会显得非常麻烦。
先创建一个扩展Mybatis的拦截器。在拦截器中,针对操作表名进行统一处理,将表名转换为类似“weibo_2017”的格式,只需要在mapper.xml中操作该格式的表名即可。插件同时会自动查找当前默认情况下所要操作的表,自动进行路由。
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class DynamicTableInterceptor implements Interceptor
{
public static final String SUFFIX_CUR = "_cur";
public static final String SUFFIX_S1 = "_s1";
public static final String SUFFIX_S2 = "_s2";
...
}
在使用的时候,只需要设置查询时间即可,插件会帮你路由到正确的数据。
<select id="listActiveUserId" parameterType="int"
resultType="long" useCache="true">
SELECT uid FROM weibo_${YYYYMM} WHERE active=1
</select>
实现了该插件,则执行该sql时自动切换到正确的数据源上。
总结
Mybatis分表插件可以动态分表插入,方便数据库扩展和管理,其实现步骤为:
- 自定义插件类
- 对应Mapper.xml创建插件拦截器
- 测试分表插件
同时,该插件支持多种业务场景,如直接插入到其他表中,及时间范围分表等,具备高度的兼容性和扩展性。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Mybatis实现分表插件 - Python技术站