工作中需要用到MyBatis进行分表操作,简单记录实现过程

MyBatis Interceptor

MyBatis 允许你在已映射语句执行过程中的某一点使用插件来拦截,包括

Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)[拦截执行器的方法]
ParameterHandler (getParameterObject, setParameters)[拦截参数的处理]
ResultSetHandler (handleResultSets, handleOutputParameters)[拦截结果集的处理]
StatementHandler (prepare, parameterize, batch, update, query)[拦截Sql语法构建的处理]

Interceptor接口

public interface Interceptor {
	  //进行拦截的时候要执行的方法
	  Object intercept(Invocation invocation) throws Throwable;
	  //决定是否要进行拦截进而决定要返回一个什么样的目标对象
	  Object plugin(Object target);
	  //Mybatis配置文件中指定一些属性
	  void setProperties(Properties properties);
	
	}

@Intercepts

@Intercepts用于表明当前的对象是一个Interceptor@Signature则表明要拦截的接口方法以及对应的参数类型
	
	@Intercepts( {  
       @Signature(method = "query", type = Executor.class, args = {  
              MappedStatement.class, Object.class, RowBounds.class,  
              ResultHandler.class }),  
       @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })  

image

利用Interceptor实现自定义分表原理

利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前也会产生一个包含Sql语句的Statement对象,Sql语句生成的时机是在Statement之前的,利用Interceptor可以在Sql语句成为Statement之前对Sql语句进行修改。

Mybatis中Statement语句是通过RoutingStatementHandler对象的 prepare方法生成的。使用Interceptor对StatementHandler的prepare方法进行拦截即可获得原始Sql。

获取原始Sql后,可以解析出根表名,再通过一系列的策略获取到实际的分表表名,对其原始Sql进行表名替换。

示例

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class CustomerIdShardInterceptor implements Interceptor {
	    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
	    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
	    private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
	
	
	    @Override
	    public Object intercept(Invocation invocation) throws Throwable {
	        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
	        // 保存会话信息
	        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler,
	                DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
	        // 获取原sql
	        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
			//修改原有Sql数据	        
			String modifiedSql = modifySql(boundSql);
			//将修改后的Sql设置到metaStatementHandler中
	        metaStatementHandler.setValue("delegate.boundSql.sql", modifiedSql);
	        return invocation.proceed();
	    }
	
	    @Override
	    public Object plugin(Object o) {
			//对应类型进行封装
	        if (o instanceof StatementHandler) {
	            return Plugin.wrap(o, this);
	        } else {
	            return o;
	        }
	    }
	
	    @Override
	    public void setProperties(Properties properties) {
	
	    }
	
	    private String modifySql(BoundSql boundSql) {
	        String targetSql = boundSql.getSql().trim().toLowerCase();
	        String tableName = getTableName(targetSql);
			// 对targetSql进行表名修改
			...
	        return targetSql;
	
	    }
	
	    /**
	     * 根据sql获取表名
	     *
	     * @param sql
	     * @return
	     */
	    private String getTableName(String sql) {
	        String[] sqls = sql.split("\\s+");
	        switch (sqls[0]) {
	            case "select": {
	                // select tableName
	                for (int i = 0; i < sqls.length; i++) {
	                    if (sqls[i].equals("from")) {
	                        return sqls[i + 1];
	                    }
	                }
	                break;
	            }
	            case "update": {
	                // update tableName
	                return sqls[1];
	            }
	            case "insert": {
	                // insert into tableName
	                return sqls[2];
	            }
	            case "delete": {
	                // delete tableName
	                return sqls[1];
	            }
	        }
	        return StringUtils.EMPTY;
	    }

    }

关于分表策略

取模

  1. 可以维护一个根symbols表用来存储插入表的自增的id代表固定业务id,使用id取模进行分表(存储分表table_suffix = table_(id%i),i的大小取决于数据量的大小),由此策略可分table_0~table_i-1个分表进行数据存储。
  2. 插入时根据symbols表的自增业务id进行取模对对应表进行插入。
  3. 查询时从symbols对查询业务id进行取模,决定去哪个分表查询

参考