springboot簡單的分表插件

業務上做了水平分表,公司基礎架構沒有提供分表中間件,開源的中間件用起來門檻高,有較大的學習成本。自己基於springboot+mybatis+jsqlparser實現了一個簡單的分表插件。
籠統來說就是攔截SQL,分析SQL,替換tableName,返回重寫後的SQL給Mybatis支持簡單的CRUD分表,支持join分表
項目裏引入maven依賴:

 		<dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>x.x.x</version>
        </dependency>
		<dependency>
            <groupId>com.github.jsqlparser</groupId>
            <artifactId>jsqlparser</artifactId>
            <version>x.x.x</version>
        </dependency>

1.分表註解

首先需要一個註解,用來標識需要分表的類或方法。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface TableSplit {

    //是否分表
    boolean split() default true;

    //表名 該字段暫未使用。使用註解會對SQL中所有的表名進行替換
    String table() default "";

    //分表字段 
    String field() default "";

    //分表策略
    String strategy() default "";

}

2.分表策略

用策略模式設計一個分表的策略,在後續的mybatis攔截器中使用

2.1策略接口

public interface Strategy {

    String TABLE_NAME = "table_name";
    String SPLIT_FIELD = "split_field";
    String EXECUTE_PARAM_DECLARE = "execute_param_declare";
    String EXECUTE_PARAM_VALUES = "execute_param_values";

    /**
     * convert sql
     * @param params
     * @return
     * @throws Exception
     */
    String convert(String sql, Map<String, Object> params) throws Exception;

    /**
     * 
     * @param params
     * @return
     * @throws Exception
     */
    Integer getTableIndex(Map<String, Object> params) throws Exception;
}

2.2 分表策略管理類

管理類維護一個map,可以通過key獲取不同的分表策略。

@Slf4j
public class StrategyManager {

    private Map<String, Strategy> strategies = new ConcurrentHashMap<>(10);

    public Strategy getStrategy(String key) {
        return strategies.get(key);
    }

    public Map<String, Strategy> getStrategies() {
        return strategies;
    }

    public void setStrategies(Map<String, String> strategies) {
        for (Map.Entry<String, String> entry : strategies.entrySet()) {
            try {
                this.strategies.put(entry.getKey(), (Strategy) Class.forName(entry.getValue()).newInstance());
            } catch (Exception e) {
                log.error("實例化策略出錯", e);
            }
        }
    }
}

2.3 分表策略的實現

分表策略的具體實現可以根據實際場景和業務自行實現,比如下面這實現類就是我在項目中使用的,用userId字段對8取模得到表的下標值。

public class RemainderStrategy implements Strategy {

    /**
     * 取模基數
     */
    private static final int DIVIDER = 8;


    @Override
    public String convert(String sql, Map<String, Object> params) throws Exception {
        MySqlParserFactory sqlParserFactory = new MySqlParserFactory();
        Integer reminder = this.getIndex(params);
        return sqlParserFactory.parser(sql, reminder);
    }

    @Override
    public Integer getTableIndex(Map<String, Object> params) throws Exception {
        Integer reminder = this.getIndex(params);
        return reminder;
    }

    private Integer getIndex(Map<String, Object> params) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        String field = (String) params.get(Strategy.SPLIT_FIELD);
        Object paramValue = params.get(Strategy.EXECUTE_PARAM_VALUES);
        String value;
        if (paramValue instanceof Map) {
            Map map = (Map) paramValue;
            value = String.valueOf(map.get(field));
        } else {
            value = BeanUtils.getProperty(paramValue, field);
        }
        Integer reminder = Integer.valueOf(value) % DIVIDER;
        return reminder;
    }

2.4 把分表策略註冊到策略管理類

我是用xml的方式,當然你也可以@Bean的方式。

@Configuration
@ImportResource(locations = {"classpath:application-bean.xml"})
public class BeanConfig {

}
<?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-3.0.xsd">
    <!-- 配置分表策略 -->
    <bean id="strategyManager" class="xxx.xxx.xxx.StrategyManager">
        <property name="strategies">
            <map>
                <entry key="remainder" value="xxx.xxx.xxx.impl.RemainderStrategy"/>
            </map>
        </property>
    </bean>
</beans>

4.定義一個ContextHelper類

實現ApplicationContextAware接口,主要用來獲取策略管理類的bean,因爲我這邊沒有用IOC。

@Component
public class ContextHelper implements ApplicationContextAware {
    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext context)
            throws BeansException {
        ContextHelper.context = context;
    }

    public static ApplicationContext getApplicationContext() {
        return ContextHelper.context;
    }

    public static <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }

    public static Object getBean(String name) {
        return context.getBean(name);
    }

    public static <T> T getBean(String name, Class<T> clazz) {
        return context.getBean(name, clazz);
    }
}

4.基於jsqlparser重寫SQL

分表策略實現類的作用就是返回重寫後的SQL,這塊功能主要是基於jsqlparser實現的。

4.1 MySqlParserFactory 工廠類

編寫一個工廠類,用來封裝解析SQL的職責。

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.CCJSqlParserManager;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;

import java.io.StringReader;
public class MySqlParserFactory {

    public String parser(String sql, Integer reminder) throws Exception {
        CCJSqlParserManager parser = new CCJSqlParserManager();
        Statement stmt = parser.parse(new StringReader(sql));
        if (stmt instanceof Select) {
            return selectSqlParser(sql, reminder);
        }
        if (stmt instanceof Update) {
            return updateSqlParser(stmt, reminder);
        }
        if (stmt instanceof Insert) {
            return insertSqlParser(stmt, reminder);
        }
        if (stmt instanceof Delete) {
            return deleteSqlParser(stmt, reminder);
        }
        return StringUtils.EMPTY;
    }

    private String deleteSqlParser(Statement stmt, Integer reminder) {
        Delete statement = (Delete) stmt;
        Table t = statement.getTable();
        t.setName(t.getName() + reminder);
        statement.setTable(t);
        return statement.toString();
    }

    private String insertSqlParser(Statement stmt, Integer reminder) {
        Insert statement = (Insert) stmt;
        Table t = statement.getTable();
        t.setName(t.getName() + reminder);
        statement.setTable(t);
        return statement.toString();
    }


    private String updateSqlParser(Statement stmt, Integer reminder) {
        Update statement = (Update) stmt;
        Table t = statement.getTable();
        t.setName(t.getName() + reminder);
        statement.setTable(t);
        return statement.toString();
    }

    private String selectSqlParser(String sql, Integer reminder) throws JSQLParserException {
        Select select = (Select) CCJSqlParserUtil.parse(sql);
        //Start of value modification
        StringBuilder buffer = new StringBuilder();
        ExpressionDeParser expressionDeParser = new ExpressionDeParser();
        SelectDeParser deParser = new MySelectDeParser(reminder, expressionDeParser, buffer);
        expressionDeParser.setSelectVisitor(deParser);
        expressionDeParser.setBuffer(buffer);
        select.getSelectBody().accept(deParser);
        return buffer.toString();
    }

4.2 MySelectDeParser

select 操作比較複雜,使用下面的類做處理。

import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.ExpressionVisitor;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.Pivot;
import net.sf.jsqlparser.util.deparser.SelectDeParser;

public class MySelectDeParser extends SelectDeParser {

    private Integer reminder;

    public MySelectDeParser(Integer reminder, ExpressionVisitor expressionVisitor, StringBuilder buffer) {
        super(expressionVisitor, buffer);
        this.reminder = reminder;
    }

    @Override
    public void visit(Table table) {
        String tableName = table.getName();
        table.setName(tableName + reminder.toString());
        StringBuilder buffer = getBuffer();
        buffer.append(table.getFullyQualifiedName());
        Pivot pivot = table.getPivot();
        if (pivot != null) {
            pivot.accept(this);
        }
        Alias alias = table.getAlias();
        if (alias != null) {
            buffer.append(alias);
        }
    }
}

該工廠類只是簡單的使用jsqlparser對CRUD的SQL做了替換。更多jsqlparser的功能需要自己探索了。

5.mybatis攔截器

編寫一個類實現mybatis的Interceptor接口,用來攔截mybatis發送給jdbc的sql。

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMap;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class TableSplitInterceptor 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);

        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        Object parameterObject = metaStatementHandler.getValue("delegate.boundSql.parameterObject");
        doSplitTable(metaStatementHandler, parameterObject);
        // 傳遞給下一個攔截器處理
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 當目標類是StatementHandler類型時,才包裝目標類,否者直接返回目標本身,減少目標被代理的次數
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private void doSplitTable(MetaObject metaStatementHandler, Object param) throws Exception {
        String originalSql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
        if (originalSql != null && !originalSql.equals("")) {
            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
            String id = mappedStatement.getId();
            String className = id.substring(0, id.lastIndexOf("."));
            String methodName = id.substring(id.lastIndexOf(".") + 1);
            Class<?> clazz = Class.forName(className);
            ParameterMap paramMap = mappedStatement.getParameterMap();
            Method method = findMethod(clazz.getDeclaredMethods(), methodName);
            // 根據配置自動生成分表SQL
            TableSplit tableSplit = null;
            if (method != null) {
                tableSplit = method.getAnnotation(TableSplit.class);
            }
            if (tableSplit == null) {
                tableSplit = clazz.getAnnotation(TableSplit.class);
            }
            if (tableSplit != null && tableSplit.split() && StringUtils.isNotBlank(tableSplit.strategy())) {
                StrategyManager strategyManager = ContextHelper.getBean(StrategyManager.class);
                String convertedSql = "";
                Strategy strategy = strategyManager.getStrategy(tableSplit.strategy());
                Map<String, Object> params = getStringObjectMap(param, paramMap, tableSplit, tableSplit.table());
                convertedSql = strategy.convert(originalSql, params);
                metaStatementHandler.setValue("delegate.boundSql.sql", convertedSql);
            }
        }
    }

    private Map<String, Object> getStringObjectMap(Object param, ParameterMap paramMap, TableSplit tableSplit, String table) {
        Map<String, Object> params = new HashMap<>();
        params.put(Strategy.TABLE_NAME, table);
        params.put(Strategy.SPLIT_FIELD, tableSplit.field());
        params.put(Strategy.EXECUTE_PARAM_DECLARE, paramMap);
        params.put(Strategy.EXECUTE_PARAM_VALUES, param);
        return params;
    }

    private Method findMethod(Method[] methods, String methodName) {
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                return method;
            }
        }
        return null;
    }
}

6.使用

代碼就是上面的這些。最後別忘了把自定義的攔截器註冊到mybatis的SqlSessionFactory中。

/**
     * sql會話工廠類
     *
     * @return 會話工廠
     * @throws Exception 異常
     */
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        factoryBean.setConfiguration(configuration);
        factoryBean.setPlugins(new Interceptor[]{new TableSplitInterceptor()});
        return factoryBean.getObject();
    }

6.1使用示例

在mapper中添加註解,指定分表策略的key和分表的分表字段即可。
如果你項目中使用統一分表策略和分表字段,那麼直接在TableSplit註解類中default指定默認值就好了。
TableSplit可以標識類和方法。方法上粒度更細。
攔截器優先獲取method上的註解。然後獲取Class上的註解,

@TableSplit(strategy = "key的名稱",field = "userId")
public interface TestMapper {
    int countByExample(@Param("userId") Integer userId, @Param("example") countByExample);
}    
<select id="countByExample" parameterType="xxx.xxx.xxx"
            resultType="java.lang.Integer">
        select count(*) from test
    </select>

業務中直接使用Mapper即可,SQL會被TableSplitInterceptor 攔截器攔截,完成表名的替換。比如上面的SQL中 test 表名會被替換成int index = (userId%8);
test_index

以上就是自己搭建的一套簡單分表插件。業務中耦合還是比較小的,唯一耦合的地方就是需要指定分表字段,一定要在MyBatis的Mapper接口的方法上添加正確的分表參數。比如目前使用userId取模,則需要在接口方法額外添加
@Param(“userId”) Integer userId,或者 @Param(“example”) countByExample中一定需要有uesrId的字段。不然分表策略無法得到分表的下標值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章