基于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 "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/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);
相关推荐
总结起来,这个项目是关于如何使用SpringMVC和MyBatis框架,结合策略设计模式和拦截器技术,实现一个动态的按年分表策略。这样的设计不仅可以提升系统的性能,还能灵活适应业务需求的变化,是一个典型的面向现代Web...
本项目基于Java、SpringBoot、MyBatis以及ShardingJDBC实现了一个分库分表的解决方案,旨在帮助开发者理解并掌握这一技术。以下是关于这些技术的详细介绍: **Java**: Java是一种广泛使用的面向对象的编程语言,...
Mybatis的插件机制基于拦截器(Interceptor)设计模式,允许在执行SQL之前或之后插入自定义逻辑。分库分表插件主要工作在SQL路由阶段,根据特定的规则(如哈希、范围等)对原始SQL进行修改,添加分片信息,确保数据...
总结来说,基于MyBatis的数据库拆分和读写分离实现,涉及到对MyBatis框架的深入理解和扩展,包括对SqlSessionTemplate的改造、数据库拆分策略的设计(如垂直拆分和水平拆分的一致性哈希)、以及读写分离的路由机制。...
该源码项目是一个基于mybatis-mate的企业级模块设计,涵盖了207个文件,其中包含110个Java源文件、28个SQL脚本、20个XML配置、17个Gradle构建文件、15个YAML配置文件、5个PDF文档、2个Markdown文件、2个JAR包文件、1...
"spring动态数据源+mybatis分库分表"是一个针对大型数据库场景的解决方案,它利用Spring框架的动态数据源功能和MyBatis的SQL映射能力,实现数据库的透明化分片。以下是这个主题的详细知识点: 1. **Spring动态数据...
在Java开发领域,数据库的扩展性和性能优化是一个重要的...在实际项目中,需要根据业务需求和数据规模,合理设计分片策略,并充分利用Sharding-JDBC和Mybatis-Plus提供的工具和特性,以实现高效、稳定的数据库操作。
本项目是基于Mybatis-Plus、Sharding-JDBC以及MySQL实现的一个分库分表示例,旨在帮助开发者理解和实践数据库水平扩展。 **Mybatis-Plus** Mybatis-Plus(简称MP)是一个Mybatis的增强工具,在Mybatis的基础上只做...
而Shardbatis则是一个专门针对Spring和MyBatis设计的分库分表插件,用于简化数据库分片的实现过程。 首先,我们需要理解Spring的核心概念。Spring是一个开源的Java平台,它提供了全面的软件基础设施服务,用于构建...
MyBatis-Sharding 是一种基于 MyBatis 的轻量级分库分表解决方案,它可以帮助开发者有效地解决亿级数据量下的 MySQL 存储问题。下面将详细介绍 MyBatis-Sharding 的核心概念、实现原理以及如何在实际项目中进行应用...
本项目为MyBatis Generator(MBG)定制扩展,旨在处理MySQL大小写敏感配置及分表时的动态表名替换。包含69个文件,涵盖38个Java源文件、16个XML配置文件、3个Markdown文档、3个JAR包、2个PNG图片、1个Git忽略文件、1...
1、基于yml 配置方式 ,实现springBoot+sharding-jdbc+mybatis-plus 实现分库分表,读写分离,以及全局表,子表的配置。 2、实现mybatis-plus 整合到springboot 详细使用请看 测试用例
本案例基于Spring、MyBatis和Sharding-JDBC 1.3.1版本,提供了一个可以直接运行的分库分表实现,帮助开发者快速理解和实践这一技术。 首先,我们要理解什么是分库分表。分库是指将一个大型数据库拆分为多个小型...
【标题】"基于mybatis,springboot开箱即用的读写分离插件.zip" 提供了一个集成mybatis和springboot的解决方案,旨在简化数据库读写分离的实现过程。这个插件使得开发者能够快速地在自己的应用中部署读写分离功能,...
标题"sharding-jdbc开源分表框架整合mybatis-demo"表明这是一个示例项目,展示了如何将`sharding-jdbc`这个开源的分库分表框架与`MyBatis`持久层框架集成在一起。这通常涉及到数据库水平扩展、数据分布以及事务管理...
源码分析方面,Mybatis-Sharding的实现基于AOP(面向切面编程)和动态代理技术,通过拦截器链来拦截SQL执行,然后插入分片逻辑。这种方式使得插件可以透明地介入到Mybatis的执行流程中,同时保持了代码的清晰和可...
基于Mybatis Plus实现代码生成器CodeGenerator 基于Mybatis Plus实现代码生成器CodeGenerator是指使用Mybatis Plus框架来实现代码生成器的功能。Mybatis Plus是一个基于Mybatis的增强型ORM框架,提供了许多实用的...
另一种策略是范围分表,基于时间或者其他连续的字段,如年份或月份,将数据分配到不同的表中,这种方式可以更好地保证数据的均衡分布。 在具体实现时,还需要注意以下几点: 1. 事务管理:多数据源环境下,事务的...
### SpringBoot+Mybatis-Plus ...至此,我们已经成功实现了基于SpringBoot、Mybatis-Plus及Sharding-JDBC 5.1.1的单库分表方案。通过上述步骤,可以有效地提升系统的性能和扩展性,为处理高并发场景打下坚实的基础。