基于ORM层面的分表实现,目前已经很多了,本人也尝试进行了开发。本人的设计目的,是希望基于Mybatis框架、通过规则引擎,实现分表策略,在接入层,更少的DAO层代码调整。
1)分表规则,通过配置驱动,本人基于Apache Commons Digester组件实现,我们在XML中声明“分表”的策略和子表的规模等。
2)为了对Mybatis层透明,即对于开发者而言,尽可能无需过度关注数据库层面的分表情况,也唔需要在代码层面调整太多;所以,我通过修改Mybatis的源码,实现“表名”参数的渲染。
难点:
1)规则引擎相关的开发。
2)Mybatis源码调整,将变量“表名”根据“分表键”进行计算,并将表名变量在sql层面替换。

一、设计思路
1、分表组、子表:比如我们表组为“user”,那么它有256张子表,那么子表的名称为:user_0、user_1、user_2....user_255,基于区间表示user_[0,256)。(约定)
2、分表键:即shard key,我们基于shard key的值,使用指定的“分表策略”,来计算表的索引号。
3、在Mybatis Statement中表名,通过变量替代,比如${user_group},在渲染SQL之前,需要通过shard key,计算出表的具体名字,然对表名的变量进行赋值操作。
二、规则与配置
1、trouter-rules.xml
- <?xml version='1.0'?>
- <rules>
- <trouter>
- <router table="user">
- <key>name</key>
- <property>user_group</property>
- <strategy>hash_mod</strategy>
- <delimiter>_</delimiter>
- <size>8</size>
- </router>
- </trouter>
- </rules>
此XML是有格式限制的,格式scheme由下文的解析器确定。
1)trouter:父节点,全文应该只能有一个。
2)每个trouter可以有多个router节点,每个router节点表示一个表组。"table"属性表示表组,一个表组中的所有子表,均以此值开头。
3)key:即shard key,用于分表计算,注意此key的属性需要在Mybatis的statement中,即所有的statement中必须有“key”参与。
- 1)select id from ${user_group} where name = #{name}
- 2) insert into ${user_group}(name) values(#{name})
4)property:即在statement中,哪个属性表示表名,因为表名是个变量,但是变量的名称还是需要指定的。
5)strategry:分表策略,对shard key使用何种策略计算表名,本实例支持“hash_mod”、“mod”两种,即根据字符串的hashcode取模、或者根据数字类型直接取模。后期可以增加“range”策略。
6)delimiter:辅助,表示“table”与“分表索引号”组合时,使用的分隔符,假如“table”为“user”、通过shard key计算出的子表索引号为“10”,那么最终的表名为“user_10”。
7)size:表组中子表的个数,假如有256个子表,那么size为256,那么子表的区域为user_[0,255)。
2、TRouterStrategry.java
声明分表策略处理器。
- public class TRouterStrategy {
-
- static enum Strategy {
-
- HASH_MOD("hash_mod"),
- MOD("mod"),
- RANGE("range");//TODO
- public String code;
-
- Strategy(String code) {
- this.code = code;
- }
-
- public static Strategy codeOf(String code) {
- if (code == null) {
- return null;
- }
- for (Strategy e : values()) {
- if (e.code.equalsIgnoreCase(code)) {
- return e;
- }
- }
- return null;
- }
- }
-
- public static int route(Strategy strategy, Object key, TRouter router) {
- switch (strategy) {
- case HASH_MOD:
- return hashMod(key, router);
- case MOD:
- return mod((Number) key, router);
- default:
- throw new RuntimeException("Strategy is not supported:" + strategy.code);
- }
- }
-
- public static int hashMod(Object key, TRouter router) {
- int size = router.getSize();
- return Math.abs(key.hashCode() % size);
- }
-
- public static int mod(Number key, TRouter router) {
- int size = router.getSize();
- return Math.abs(key.intValue() % size);
- }
-
- public static int range(Number key, TRouter router) {
- throw new RuntimeException("Range strategy is not supported now!");
- }
- }
3、TRouterStrategryEnum.java
声明分表策略的常量。
- /**
- * Created by liuguanqing on 17/5/3.
- * 数据库表路由策略,支持两种
- * 1)hash:根据key的hash值进行取模计算,得到所在的表名后缀
- * 2)range:根据key的值域区间,进行计算表名后缀
- * <p>
- * 在hash计算时,如果key为数字类型,则直接取模,如果是其他类型,将根据其hashcode进行取模,
- * 这要求key的hashcode方法是严格的。建议key是原始类型的值,而不是pojo类的对象。
- */
- public enum TRouterStrategyEnum {
-
- HASH("hash"),
- RANGE("range");//
- public String code;
-
- TRouterStrategyEnum(String code) {
- this.code = code;
- }
-
- public TRouterStrategyEnum codeOf(String code) {
- if (code == null) {
- return null;
- }
- for (TRouterStrategyEnum e : values()) {
- if (e.code.equalsIgnoreCase(code)) {
- return e;
- }
- }
- return null;
- }
- }
4、TRouter.java
对应XML中的<trouter>,用于保存每个节点的数据。
- public class TRouter implements Serializable {
-
- /**
- *
- */
- protected static final String DEFAULT_DELIMITER = "_";//
- protected static final String DEFAULT_STRATEGY = TRouterStrategyEnum.HASH.code;
- private String strategy;
- private String property;
- private TRouterStrategy.Strategy _strategy;
- private String table;
- private String delimiter;//
- private Integer size = 0;//the num of subtables
- private String key;
- private Map<Integer, String> indexes = new HashMap<Integer, String>();
-
- public TRouter() {
- }
-
- public String getProperty() {
- return property;
- }
-
- public void setProperty(String property) {
- this.property = property;
- }
-
- public String getTable() {
- return table;
- }
-
- public void setTable(String table) {
- this.table = table;
- }
-
- public String getDelimiter() {
- return delimiter;
- }
-
- public void setDelimiter(String delimiter) {
- this.delimiter = delimiter;
- }
-
- public Integer getSize() {
- return size;
- }
-
- public void setSize(Integer size) {
- this.size = size;
- //初始化索引
- indexes.clear();
- for (int i = 0; i < size; i++) {
- indexes.put(i, table + delimiter + i);
- }
- }
-
- public String getStrategy() {
- return strategy;
- }
-
- public void setStrategy(String strategy) {
- _strategy = TRouterStrategy.Strategy.codeOf(strategy);
- if (_strategy == null) {
- throw new RuntimeException("Strategy:" + strategy + " is not valid!");
- }
- this.strategy = strategy;
- }
-
- public String getKey() {
- return key;
- }
-
- public void setKey(String key) {
- this.key = key;
- }
-
- /**
- * @param target 根据此target值进行table路由
- * @return
- */
- public String getTable(Object target) {
- if (target == null) {
- throw new NullPointerException("route key cant be null1");
- }
- int index = TRouterStrategy.route(_strategy, target, this);
- return indexes.get(index);
- }
-
- //check and populate
- public String populate(Object target) {
- if(target == null) {
- return null;
- }
- try {
- int index = TRouterStrategy.route(_strategy,target,this);
- return indexes.get(index);
- } catch (Exception e) {
- //
- }
- return null;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("table:").append(table)
- .append(",delimiter:").append(delimiter)
- .append(",size:").append(size)
- .append(",strategy:").append(strategy)
- .append(",key:").append(key);
- return sb.toString();
- }
- }
5、TRouterPool.java
用于保存全局的分表策略,提供一些简单的策略操作。
- public class TRouterPool {
-
- private final Map<String, TRouter> ruleMap = new HashMap<String, TRouter>(32);
-
- public TRouterPool() {
- }
-
- ;
-
- public void put(TRouter router) {
- if (router.getTable() == null) {
- throw new NullPointerException("Table of rule must be defined!");
- }
- if (router.getKey() == null) {
- throw new IllegalArgumentException("Shard key of rule must be defined");
- }
- if (router.getDelimiter() == null) {
- router.setDelimiter(TRouter.DEFAULT_DELIMITER);
- }
- if (router.getStrategy() == null) {
- router.setStrategy(TRouter.DEFAULT_STRATEGY);
- }
- String property = router.getProperty();
- if (property == null) {
- throw new NullPointerException("Property of rule must be defined!");
- }
- if (ruleMap.containsKey(property)) {
- throw new IllegalArgumentException("Property '" + property + "' has been be defined,it's must be unique!");
- }
- ruleMap.put(router.getProperty(), router);
- }
-
- public TRouter getRule(String property) {
- return ruleMap.get(property);
- }
-
-
- public List<TRouter> getAllRules() {
- return new ArrayList<TRouter>(ruleMap.values());
- }
-
- /**
- * 从指定的parameters中找到shard key,并计算出对应table名称
- * @param parameters
- * @return
- */
- public Map<String,String> populate(Map<String,Object> parameters) {
- if(parameters == null || ruleMap.isEmpty()) {
- return null;
- }
- Map<String,String> tables = new HashMap<String, String>(12);
- for(Map.Entry<String,TRouter> entry : ruleMap.entrySet()) {
- TRouter router = entry.getValue();
- //如果包含shard key,则计算
- Object target = parameters.get(router.getKey());
- if(target != null) {
- tables.put(entry.getKey(), router.populate(target));
- }
- }
- return tables;
- }
-
- }
6、TRouterParser.java
支持Spring配置,用于解析指定的XML,并生成TRouterPool对象,此后TRooterPool对象即可被外部组件使用。
- public class TRouterParser {
-
- public static TRouterPool parse(String location) throws Exception {
- ClassLoader loader = Thread.currentThread().getContextClassLoader();
- Digester digester = new Digester();
- digester.setClassLoader(loader);
- final TRouterPool rulePool = new TRouterPool();
- digester.push(rulePool);
- digester.addRuleSet(new ConfigRuleSet());
- digester.parse(loader.getResource(location));
- return rulePool;
- }
-
- static class ConfigRuleSet extends RuleSetBase {
-
- @Override
- public void addRuleInstances(Digester digester) {
- //digester.push(TableRulePool.getInstance());
- digester.addObjectCreate("*/trouter/router", TRouter.class.getName());
- digester.addSetProperties("*/trouter/router");
- digester.addSetNext("*/trouter/router", "put");
- digester.addCallMethod("*/trouter/router/property", "setProperty", 0, new String[]{"java.lang.String"});
- digester.addCallMethod("*/trouter/router/size", "setSize", 0, new String[]{"java.lang.Integer"});
- digester.addCallMethod("*/trouter/router/delimiter", "setDelimiter", 0, new String[]{"java.lang.String"});
- digester.addCallMethod("*/trouter/router/strategy", "setStrategy", 0, new String[]{"java.lang.String"});
- digester.addCallMethod("*/trouter/router/key", "setKey", 0, new String[]{"java.lang.String"});
- }
- }
-
-
- public static void main(String[] args) throws Exception {
- TRouterPool routerPool = parse("trouter-rules-sample.xml");
- List<TRouter> rules = routerPool.getAllRules();
- for (TRouter item : rules) {
- System.out.println(item);
- }
- }
- }
三、Mybatis源码修改
不违背我们的设计初衷,我们希望在使用Mybatis时尽可能的透明,不让开发者感知太多分表的内部实现,即接入分表组件后,原来的代码尽可能少的修改,同时那些不分表的操作也应该能够平滑支持。
1)$变量
- select name from ${user_group}
根据Mybatis的设计原理,$修饰的变量,在渲染SQL时直接“填充”,变量值不会使用''进行包括。注意这种类型的变量的渲染时机,是在Mybatis解析Statement时渲染。此前,本人曾经尝试使用Mybatis Interceptor来添加parameter的方式渲染,其实这是无法做到的;因为进入interceptor之前,这类变量已经整理和渲染完毕,将不能再次更改。
2)#变量
这类变量,是基于JDBC PrepareStatement进行参数设定,完全基于JDBC,所以这部分变量渲染SQL之后,将会使用''进行包含。所以对于“表名”参数,是不能使用#变量的,否则渲染的SQL将会语法错误。
最终我们经过多次尝试,那么分表的表明,必须使用$变量声明,且需要在MyBatis Statement解析时就应该确定“表名”参数的值。所以我们只能修改Mybatis的源码来支持。
我们修改Mybatis源码以提供如下支持:
1)可以让Mybatis解析“trouter-rules.xml”,并将解析结果保存在Configuration中。注意“Configuration”类是Mybatis的顶级类,用于保存“sql-config.xml”所有的配置项。
2)我们需要修改Mybatis Statement渲染的代码,即在渲染$变量时,应该提前将表名的参数进行赋值,即进行分表策略的计算。$变量的解析,是Mybatis根据正则表达式进行。我们通过检查源码,找到了渲染的核心类:DynamicContext.java(org.apache.ibatis.scripting.xmltags)。
1、下载Mybatis源码
我们通过gitlab,下载Mybatis源码,基于3.4.4版本,并将此源码的核心部分抽离出来,全部复制到我们自己的project中,package的名称不要改变。
2、sqlmap-config.xml(Mybatis配置的声明文件),在文件中增加如下,指定“分表策略”的XML文件。
- <?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE configuration PUBLIC "-////DTD Config 3.0//EN"
- "http:///dtd/mybatis-3-config.dtd">
- <configuration>
- <properties>
- <property name="routerLocation" value="trouter-rules.xml"/>
- </properties>
- <settings>
- <setting name="lazyLoadingEnabled" value="false" />
- <setting name="logImpl" value="SLF4J"/>
- <setting name="routerLocation" value="trouter-rules.xml"/>
- </settings>
-
- </configuration>
声明一个自定义的属性“routerLocation”,值为“trouter-rules.xml”的位置,尽可能与sqlmap-config.xml临近。
3、Configuration.java源码修改(org.apache.ibatis.session.Configuration)
因为增加了自定的选项,那么需要增加解析的逻辑,这部分工作由Configuration类负责,此外Configuration实例中所有的属性都可以在Mybatis运行期间获取,因为configuration实例的引用将会传递给Statement等;这也是我们为什么需要在Configuration中增加解析“分表策略”的原因。(此外,在DynamicContext中也是可以访问configuration引用)
- public class Configuration {
- //...增加两个属性
- protected String routerLocation;
- protected TRouterPool routerPool;
-
- public TRouterPool getRouterPool() {
- return routerPool;
- }
-
- public void setRouterPool(TRouterPool routerPool) {
- this.routerPool = routerPool;
- }
-
- public void setRouterLocation(String routerLocation) {
- this.routerLocation = routerLocation;
- }
-
- //
- }
4、XMLConfigBuilder.java(org.apache.ibatis.builder.xml)
此类主要是解析sqlmap-config.xml,所以我们还需要修改此类,以支持对“trouter-rules.xml”解析,在Configuration类中,将使用它解析XML并完成configuration实例的初始化。
- public class XMLConfigBuilder extends BaseBuilder {
-
- private void settingsElement(Properties props) throws Exception {
- //..其他操作保留,在此方法的结尾之前,增加:
- configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
-
- //解析分表的router
- String routerLocation = props.getProperty("routerLocation","");
- if(!routerLocation.isEmpty()) {
- configuration.setRouterLocation(routerLocation);
- configuration.setRouterPool(parserTRouter(routerLocation));
- }
- }
- }
5、DynamicContext.java
在Configuration实例初始化完毕之后,每次Mybatis的SQL操作(session),都会通过DynamicContext进行参数解析和渲染。对于$参数,Mybatis通过正则表达式匹配和渲染,所以,我们需要修改它的默认行为,在“渲染表名”参数之前,我们就应该从Statement中传递的parameter中,找到“shard key”,并计算表名的值。
- public class DynamicContext {
-
- //修改构造方法
- public DynamicContext(Configuration configuration, Object parameterObject) {
- if (parameterObject != null && !(parameterObject instanceof Map)) {
- MetaObject metaObject = configuration.newMetaObject(parameterObject);
- bindings = new ContextMap(metaObject);
- } else {
- bindings = new ContextMap(null);
- }
-
- bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
- bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
- if(parameterObject == null) {
- return;
- }
-
- TRouterPool routerPool = configuration.getRouterPool();
- if (routerPool == null) {
- return;
- }
- //我们根据shard key,对所有“分表”策略进行应用
- //因为我无法知道当前Statement中使用的哪个“表”,所以
- //我们使用shard key,计算所有分表策略。最终“Statement”中
- //声明的“表名参数”肯定会渲染。
- MetaObject metaObject = configuration.newMetaObject(parameterObject);
- ContextMap current = new ContextMap(metaObject);
-
- Map<String, String> tableMapper = routerPool.populate(current);
- if (tableMapper != null) {
- bindings.putAll(tableMapper);
- }
- }
- //...
- }
在DynamicContext类中,我们使用shard key对所有的分表策略计算,假如你配置了三个“router”,那么将会计算出三个分表值,那么当前Statement的表名也在其中,所以渲染SQL的结果没有问题;之所以这么做,原因是,通过DynamicContext,我无法知道Statemement究竟使用了哪个特定的router,为了简化代码设计,我不如全部router都计算一遍。
四、使用
我们在使用Mybatis进行分表时需要注意几个问题
1、Statement
- select name from ${user_group} where name = #{name}
1)shard key必须指定,比如name作为“key”,那么name参数必须在parameter中指定。
2)表名使用$变量,变量的名称需要与trouter-rules.xml中指定的table对应,比如你在<router><property>user_group</property></router>,那么在Statement中,表名的变量必须为“user_group”。
3)如果操作的表,没有分表,可以直接使用表名,这并不会带来影响。
4)参与操作的分表,只能有一个,如果有关联查询,那么分表应该作为主表。通常,我们尽可能避免关联子查询。
2、Mybatis DAO设计:原则上,不需要调整太多代码,只需要注意传递的parameter中必须包含shard key。
- 1、基于map
- Map<String,Object> params = new HashMap<>();
- params.put("name","zhangsan");
- this.sqlsession.selectOne("UserMapper.getByName",params);
-
- 2、基于对象
- UserDo user = new UserDo();
- user.setName("zhangsan");
- this.sqlsession.selectOne("UserMapper.getByName",user);
|