分享

基于Mybatis的分表设计

 WindySky 2017-07-04

    基于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

Java代码  收藏代码
  1. <?xml version='1.0'?>  
  2. <rules>  
  3.     <trouter>  
  4.         <router table="user">  
  5.             <key>name</key>  
  6.             <property>user_group</property>  
  7.             <strategy>hash_mod</strategy>  
  8.             <delimiter>_</delimiter>  
  9.             <size>8</size>  
  10.         </router>  
  11.     </trouter>  
  12. </rules>  

    此XML是有格式限制的,格式scheme由下文的解析器确定。

    1)trouter:父节点,全文应该只能有一个。

    2)每个trouter可以有多个router节点,每个router节点表示一个表组。"table"属性表示表组,一个表组中的所有子表,均以此值开头。

    3)key:即shard key,用于分表计算,注意此key的属性需要在Mybatis的statement中,即所有的statement中必须有“key”参与。

Java代码  收藏代码
  1. 1)select id from ${user_group} where name = #{name}  
  2. 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

    声明分表策略处理器。

Java代码  收藏代码
  1. public class TRouterStrategy {  
  2.   
  3.     static enum Strategy {  
  4.   
  5.         HASH_MOD("hash_mod"),  
  6.         MOD("mod"),  
  7.         RANGE("range");//TODO  
  8.         public String code;  
  9.   
  10.         Strategy(String code) {  
  11.             this.code = code;  
  12.         }  
  13.   
  14.         public static Strategy codeOf(String code) {  
  15.             if (code == null) {  
  16.                 return null;  
  17.             }  
  18.             for (Strategy e : values()) {  
  19.                 if (e.code.equalsIgnoreCase(code)) {  
  20.                     return e;  
  21.                 }  
  22.             }  
  23.             return null;  
  24.         }  
  25.     }  
  26.   
  27.     public static int route(Strategy strategy, Object key, TRouter router) {  
  28.         switch (strategy) {  
  29.             case HASH_MOD:  
  30.                 return hashMod(key, router);  
  31.             case MOD:  
  32.                 return mod((Number) key, router);  
  33.             default:  
  34.                 throw new RuntimeException("Strategy is not supported:" + strategy.code);  
  35.         }  
  36.     }  
  37.   
  38.     public static int hashMod(Object key, TRouter router) {  
  39.         int size = router.getSize();  
  40.         return Math.abs(key.hashCode() % size);  
  41.     }  
  42.   
  43.     public static int mod(Number key, TRouter router) {  
  44.         int size = router.getSize();  
  45.         return Math.abs(key.intValue() % size);  
  46.     }  
  47.   
  48.     public static int range(Number key, TRouter router) {  
  49.         throw new RuntimeException("Range strategy is not supported now!");  
  50.     }  
  51. }  

 

    3、TRouterStrategryEnum.java

    声明分表策略的常量。

Java代码  收藏代码
  1. /** 
  2.  * Created by liuguanqing on 17/5/3. 
  3.  * 数据库表路由策略,支持两种 
  4.  * 1)hash:根据key的hash值进行取模计算,得到所在的表名后缀 
  5.  * 2)range:根据key的值域区间,进行计算表名后缀 
  6.  * <p> 
  7.  * 在hash计算时,如果key为数字类型,则直接取模,如果是其他类型,将根据其hashcode进行取模, 
  8.  * 这要求key的hashcode方法是严格的。建议key是原始类型的值,而不是pojo类的对象。 
  9.  */  
  10. public enum TRouterStrategyEnum {  
  11.   
  12.     HASH("hash"),  
  13.     RANGE("range");//  
  14.     public String code;  
  15.   
  16.     TRouterStrategyEnum(String code) {  
  17.         this.code = code;  
  18.     }  
  19.   
  20.     public TRouterStrategyEnum codeOf(String code) {  
  21.         if (code == null) {  
  22.             return null;  
  23.         }  
  24.         for (TRouterStrategyEnum e : values()) {  
  25.             if (e.code.equalsIgnoreCase(code)) {  
  26.                 return e;  
  27.             }  
  28.         }  
  29.         return null;  
  30.     }  
  31. }  

 

    4、TRouter.java

    对应XML中的<trouter>,用于保存每个节点的数据。

Java代码  收藏代码
  1. public class TRouter implements Serializable {  
  2.   
  3.     /** 
  4.      * 
  5.      */  
  6.     protected static final String DEFAULT_DELIMITER = "_";//  
  7.     protected static final String DEFAULT_STRATEGY = TRouterStrategyEnum.HASH.code;  
  8.     private String strategy;  
  9.     private String property;  
  10.     private TRouterStrategy.Strategy _strategy;  
  11.     private String table;  
  12.     private String delimiter;//  
  13.     private Integer size = 0;//the num of  subtables  
  14.     private String key;  
  15.     private Map<Integer, String> indexes = new HashMap<Integer, String>();  
  16.   
  17.     public TRouter() {  
  18.     }  
  19.   
  20.     public String getProperty() {  
  21.         return property;  
  22.     }  
  23.   
  24.     public void setProperty(String property) {  
  25.         this.property = property;  
  26.     }  
  27.   
  28.     public String getTable() {  
  29.         return table;  
  30.     }  
  31.   
  32.     public void setTable(String table) {  
  33.         this.table = table;  
  34.     }  
  35.   
  36.     public String getDelimiter() {  
  37.         return delimiter;  
  38.     }  
  39.   
  40.     public void setDelimiter(String delimiter) {  
  41.         this.delimiter = delimiter;  
  42.     }  
  43.   
  44.     public Integer getSize() {  
  45.         return size;  
  46.     }  
  47.   
  48.     public void setSize(Integer size) {  
  49.         this.size = size;  
  50.         //初始化索引  
  51.         indexes.clear();  
  52.         for (int i = 0; i < size; i++) {  
  53.             indexes.put(i, table + delimiter + i);  
  54.         }  
  55.     }  
  56.   
  57.     public String getStrategy() {  
  58.         return strategy;  
  59.     }  
  60.   
  61.     public void setStrategy(String strategy) {  
  62.         _strategy = TRouterStrategy.Strategy.codeOf(strategy);  
  63.         if (_strategy == null) {  
  64.             throw new RuntimeException("Strategy:" + strategy + " is not valid!");  
  65.         }  
  66.         this.strategy = strategy;  
  67.     }  
  68.   
  69.     public String getKey() {  
  70.         return key;  
  71.     }  
  72.   
  73.     public void setKey(String key) {  
  74.         this.key = key;  
  75.     }  
  76.   
  77.     /** 
  78.      * @param target 根据此target值进行table路由 
  79.      * @return 
  80.      */  
  81.     public String getTable(Object target) {  
  82.         if (target == null) {  
  83.             throw new NullPointerException("route key cant be null1");  
  84.         }  
  85.         int index = TRouterStrategy.route(_strategy, target, this);  
  86.         return indexes.get(index);  
  87.     }  
  88.   
  89.     //check and populate  
  90.     public String populate(Object target) {  
  91.         if(target == null) {  
  92.             return null;  
  93.         }  
  94.         try {  
  95.             int index = TRouterStrategy.route(_strategy,target,this);  
  96.             return indexes.get(index);  
  97.         } catch (Exception e) {  
  98.             //  
  99.         }  
  100.         return null;  
  101.     }  
  102.   
  103.     @Override  
  104.     public String toString() {  
  105.         StringBuilder sb = new StringBuilder();  
  106.         sb.append("table:").append(table)  
  107.                 .append(",delimiter:").append(delimiter)  
  108.                 .append(",size:").append(size)  
  109.                 .append(",strategy:").append(strategy)  
  110.                 .append(",key:").append(key);  
  111.         return sb.toString();  
  112.     }  
  113. }  

 

    5、TRouterPool.java

    用于保存全局的分表策略,提供一些简单的策略操作。

Java代码  收藏代码
  1. public class TRouterPool {  
  2.   
  3.     private final Map<String, TRouter> ruleMap = new HashMap<String, TRouter>(32);  
  4.   
  5.     public TRouterPool() {  
  6.     }  
  7.   
  8.     ;  
  9.   
  10.     public void put(TRouter router) {  
  11.         if (router.getTable() == null) {  
  12.             throw new NullPointerException("Table of rule must be defined!");  
  13.         }  
  14.         if (router.getKey() == null) {  
  15.             throw new IllegalArgumentException("Shard key of rule must be defined");  
  16.         }  
  17.         if (router.getDelimiter() == null) {  
  18.             router.setDelimiter(TRouter.DEFAULT_DELIMITER);  
  19.         }  
  20.         if (router.getStrategy() == null) {  
  21.             router.setStrategy(TRouter.DEFAULT_STRATEGY);  
  22.         }  
  23.         String property = router.getProperty();  
  24.         if (property == null) {  
  25.             throw new NullPointerException("Property of rule must be defined!");  
  26.         }  
  27.         if (ruleMap.containsKey(property)) {  
  28.             throw new IllegalArgumentException("Property '" + property + "' has been be defined,it's must be unique!");  
  29.         }  
  30.         ruleMap.put(router.getProperty(), router);  
  31.     }  
  32.   
  33.     public TRouter getRule(String property) {  
  34.         return ruleMap.get(property);  
  35.     }  
  36.   
  37.   
  38.     public List<TRouter> getAllRules() {  
  39.         return new ArrayList<TRouter>(ruleMap.values());  
  40.     }  
  41.   
  42.     /** 
  43.      * 从指定的parameters中找到shard key,并计算出对应table名称 
  44.      * @param parameters 
  45.      * @return 
  46.      */  
  47.     public Map<String,String> populate(Map<String,Object> parameters) {  
  48.         if(parameters == null || ruleMap.isEmpty()) {  
  49.             return null;  
  50.         }  
  51.         Map<String,String> tables = new HashMap<String, String>(12);  
  52.         for(Map.Entry<String,TRouter> entry : ruleMap.entrySet()) {  
  53.             TRouter router = entry.getValue();  
  54.             //如果包含shard key,则计算  
  55.             Object target = parameters.get(router.getKey());  
  56.             if(target != null) {  
  57.                 tables.put(entry.getKey(), router.populate(target));  
  58.             }  
  59.         }  
  60.         return tables;  
  61.     }  
  62.   
  63. }  

 

    6、TRouterParser.java

     支持Spring配置,用于解析指定的XML,并生成TRouterPool对象,此后TRooterPool对象即可被外部组件使用。

Java代码  收藏代码
  1. public class TRouterParser {  
  2.   
  3.     public static TRouterPool parse(String location) throws Exception {  
  4.         ClassLoader loader = Thread.currentThread().getContextClassLoader();  
  5.         Digester digester = new Digester();  
  6.         digester.setClassLoader(loader);  
  7.         final TRouterPool rulePool = new TRouterPool();  
  8.         digester.push(rulePool);  
  9.         digester.addRuleSet(new ConfigRuleSet());  
  10.         digester.parse(loader.getResource(location));  
  11.         return rulePool;  
  12.     }  
  13.   
  14.     static class ConfigRuleSet extends RuleSetBase {  
  15.   
  16.         @Override  
  17.         public void addRuleInstances(Digester digester) {  
  18.             //digester.push(TableRulePool.getInstance());  
  19.             digester.addObjectCreate("*/trouter/router", TRouter.class.getName());  
  20.             digester.addSetProperties("*/trouter/router");  
  21.             digester.addSetNext("*/trouter/router", "put");  
  22.             digester.addCallMethod("*/trouter/router/property", "setProperty", 0, new String[]{"java.lang.String"});  
  23.             digester.addCallMethod("*/trouter/router/size", "setSize", 0, new String[]{"java.lang.Integer"});  
  24.             digester.addCallMethod("*/trouter/router/delimiter", "setDelimiter", 0, new String[]{"java.lang.String"});  
  25.             digester.addCallMethod("*/trouter/router/strategy", "setStrategy", 0, new String[]{"java.lang.String"});  
  26.             digester.addCallMethod("*/trouter/router/key", "setKey", 0, new String[]{"java.lang.String"});  
  27.         }  
  28.     }  
  29.   
  30.   
  31.     public static void main(String[] args) throws Exception {  
  32.         TRouterPool routerPool = parse("trouter-rules-sample.xml");  
  33.         List<TRouter> rules = routerPool.getAllRules();  
  34.         for (TRouter item : rules) {  
  35.             System.out.println(item);  
  36.         }  
  37.     }  
  38. }  

 

三、Mybatis源码修改

    不违背我们的设计初衷,我们希望在使用Mybatis时尽可能的透明,不让开发者感知太多分表的内部实现,即接入分表组件后,原来的代码尽可能少的修改,同时那些不分表的操作也应该能够平滑支持。

    1)$变量

Java代码  收藏代码
  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文件。

Java代码  收藏代码
  1. <?xml version="1.0" encoding="UTF-8" ?>  
  2. <!DOCTYPE configuration PUBLIC "-////DTD Config 3.0//EN"  
  3. "http:///dtd/mybatis-3-config.dtd">  
  4. <configuration>  
  5.     <properties>  
  6.         <property name="routerLocation" value="trouter-rules.xml"/>  
  7.     </properties>  
  8.     <settings>  
  9.         <setting name="lazyLoadingEnabled" value="false" />  
  10.         <setting name="logImpl" value="SLF4J"/>  
  11.                 <setting name="routerLocation" value="trouter-rules.xml"/>  
  12.     </settings>  
  13.   
  14. </configuration>  

 

    声明一个自定义的属性“routerLocation”,值为“trouter-rules.xml”的位置,尽可能与sqlmap-config.xml临近。

 

    3、Configuration.java源码修改(org.apache.ibatis.session.Configuration)

    因为增加了自定的选项,那么需要增加解析的逻辑,这部分工作由Configuration类负责,此外Configuration实例中所有的属性都可以在Mybatis运行期间获取,因为configuration实例的引用将会传递给Statement等;这也是我们为什么需要在Configuration中增加解析“分表策略”的原因。(此外,在DynamicContext中也是可以访问configuration引用)

 

Java代码  收藏代码
  1. public class Configuration {  
  2.     //...增加两个属性  
  3.     protected String routerLocation;  
  4.     protected TRouterPool routerPool;  
  5.       
  6.     public TRouterPool getRouterPool() {  
  7.         return routerPool;  
  8.     }  
  9.   
  10.     public void setRouterPool(TRouterPool routerPool) {  
  11.         this.routerPool = routerPool;  
  12.     }  
  13.   
  14.     public void setRouterLocation(String routerLocation) {  
  15.         this.routerLocation = routerLocation;  
  16.     }  
  17.   
  18.     //  
  19. }  

 

    4、XMLConfigBuilder.java(org.apache.ibatis.builder.xml)

    此类主要是解析sqlmap-config.xml,所以我们还需要修改此类,以支持对“trouter-rules.xml”解析,在Configuration类中,将使用它解析XML并完成configuration实例的初始化。

Java代码  收藏代码
  1. public class XMLConfigBuilder extends BaseBuilder {  
  2.   
  3.     private void settingsElement(Properties props) throws Exception {  
  4.         //..其他操作保留,在此方法的结尾之前,增加:  
  5.         configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));  
  6.   
  7.         //解析分表的router  
  8.         String routerLocation = props.getProperty("routerLocation","");  
  9.         if(!routerLocation.isEmpty()) {  
  10.             configuration.setRouterLocation(routerLocation);  
  11.             configuration.setRouterPool(parserTRouter(routerLocation));  
  12.         }  
  13.     }  
  14. }  

 

    5、DynamicContext.java

    在Configuration实例初始化完毕之后,每次Mybatis的SQL操作(session),都会通过DynamicContext进行参数解析和渲染。对于$参数,Mybatis通过正则表达式匹配和渲染,所以,我们需要修改它的默认行为,在“渲染表名”参数之前,我们就应该从Statement中传递的parameter中,找到“shard key”,并计算表名的值。

Java代码  收藏代码
  1. public class DynamicContext {  
  2.   
  3.     //修改构造方法  
  4.     public DynamicContext(Configuration configuration, Object parameterObject) {  
  5.         if (parameterObject != null && !(parameterObject instanceof Map)) {  
  6.             MetaObject metaObject = configuration.newMetaObject(parameterObject);  
  7.             bindings = new ContextMap(metaObject);  
  8.         } else {  
  9.             bindings = new ContextMap(null);  
  10.         }  
  11.   
  12.         bindings.put(PARAMETER_OBJECT_KEY, parameterObject);  
  13.         bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());  
  14.         if(parameterObject == null) {  
  15.             return;  
  16.         }  
  17.   
  18.         TRouterPool routerPool = configuration.getRouterPool();  
  19.         if (routerPool == null) {  
  20.             return;  
  21.         }  
  22.         //我们根据shard key,对所有“分表”策略进行应用  
  23.         //因为我无法知道当前Statement中使用的哪个“表”,所以  
  24.         //我们使用shard key,计算所有分表策略。最终“Statement”中  
  25.         //声明的“表名参数”肯定会渲染。  
  26.         MetaObject metaObject = configuration.newMetaObject(parameterObject);  
  27.         ContextMap current = new ContextMap(metaObject);  
  28.   
  29.         Map<String, String> tableMapper = routerPool.populate(current);  
  30.         if (tableMapper != null) {  
  31.             bindings.putAll(tableMapper);  
  32.         }  
  33.     }  
  34.     //...  
  35. }  

 

    在DynamicContext类中,我们使用shard key对所有的分表策略计算,假如你配置了三个“router”,那么将会计算出三个分表值,那么当前Statement的表名也在其中,所以渲染SQL的结果没有问题;之所以这么做,原因是,通过DynamicContext,我无法知道Statemement究竟使用了哪个特定的router,为了简化代码设计,我不如全部router都计算一遍。

 

四、使用

    我们在使用Mybatis进行分表时需要注意几个问题

    1、Statement

Java代码  收藏代码
  1. 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。

Java代码  收藏代码
  1. 1、基于map  
  2. Map<String,Object> params = new HashMap<>();  
  3. params.put("name","zhangsan");  
  4. this.sqlsession.selectOne("UserMapper.getByName",params);  
  5.   
  6. 2、基于对象  
  7. UserDo user = new UserDo();  
  8. user.setName("zhangsan");  
  9. this.sqlsession.selectOne("UserMapper.getByName",user);  

 

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多