当然,分库分表有很多的方法论,比如垂直拆分、水平拆分;也有很多的中间件产品,比如MyCat、ShardingJDBC。
根据业务场景选择得当的拆分方法,再选择一个熟习的开源框架,就能帮助我们完成项目中所涉及到的数据拆分事情。
本文并不打算就这些方法论和开源框架展开深入的磋商,笔者想谈论其余一个场景:
如果系统中须要拆分的表并不多,只是1个或者少量的几个,我们是否值得引入一些相对繁芜的中间件产品;特殊是,如果我们对它们的事理不甚理解,是否有信心驾驭它们 ?
基于此,如果你的系统中有少量的表须要拆分,也没有专门的资源去研究开源组件,那么我们可以自己来实现一个大略的分库分表插件;当然,如果你的系统比较繁芜,业务量较大,还是采取开源组件或者团队自研组件来办理这事较为稳妥。
一、事理
分库分表这事说大略也大略,说繁芜那也挺繁芜...
大略是由于它的核心流程比较明确。便是解析SQL语句,然后根据预先配置的规则,重写或路由到真实的数据库表中去;
繁芜在于,SQL语句繁芜且灵巧,比如分页、去重、排序、分组、聚合、关联查询等操作,如何精确的解析它们。
以是就算是ShardingJDBC,在官网中也明确了支持项和不支持项。
二、表明式配置相对付繁芜的配置文件,我们采取较为轻便的表明式配置,它的定义如下:
@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Sharding { String tableName(); //逻辑表名 String field(); //分片键 String mode(); //算法模式 int length() default 0; //分表数量}
那么,在哪里利用它呢 ? 比如我们的用户表须要分表,那就在User这个实体工具上标注。
@Data@Sharding(tableName = "user",field = "id",mode = "hash",length = 16)public class User { private Long id; private String name; private String address; private String tel; private String email;}
这就解释了,我一共有 16 张用户表,根据用户ID,利用Hash算法来打算它的位置。
当然,我们不止有Hash算法,还可以根据日期范围来定义。
@Data@Sharding(tableName = "car",field = "creatTime",mode = "range")public class Car { private long id; private String number; private String brand; private String creatTime; private long userId;}
三、分片算法
在这里,笔者实现了两种分片办法,便是HashAlgorithm和RangeAlgorithm 。
1、范围分片如果你的系统中有利用冷热数据分离,我们可以按照日期将不同月的数据分散到不同的表中。
比如车辆的创建韶光是2019-12-10 15:30:00,这条数据将会被分配到car_201912这张表中去。
我们通过截取韶光的年月部分,然后再加上逻辑表名即可。
public class RangeAlgorithm implements Algorithm { @Override public String doSharding(String tableName, Object value,int length) { if (value!=null){ try{ DateUtil.parseDateTime(value.toString()); String replace = value.toString().substring(0, 7).replace("-", ""); String newName = tableName+"_"+replace; return newName; }catch (DateException ex){ logger.error("韶光格式不符合哀求!传入参数:{},精确格式:{}",value.toString(),"yyyy-MM-dd HH:mm:ss"); return tableName; } } return tableName; }}
2、Hash分片
在Hash分片算法中,我们可以先判断表的数量,是不是2的幂次方。如果不是,就通过算数办法获取下标,如果是呢,就通过位运算的办法获取下标。当然了,这是在HashMap源码中学到的哦。
public class HashAlgorithm implements Algorithm { @Override public String doSharding(String tableName, Object value,int length) { if (this.isEmpty(value)){ return tableName; }else{ int h; int hash = (h = value.hashCode()) ^ (h >>> 16); int index; if (is2Power(length)){ index = (length - 1) & hash; }else { index = Math.floorMod(hash, length); } return tableName+"_"+index; } }}
四、拦截器
配置和分片算法都有了,接下来便是重头戏了。在这里,我们利用Mybatis拦截器将它们派上用场。
常年CRUD的我们,都知道一条业务SQL肯定逃不出它们的范围。个中,在业务上我们的删除功能一样平常都是逻辑删除,以是,基本上不会有DELETE操作。
相较而言,新增和修正SQL都比较大略且格式固定,查询SQL每每比较灵巧且繁芜。以是,在这里笔者定义了两个拦截器。
不过,在先容拦截器之前,我们有情由要理解其余两个东西:SQL语法解析器和分片算法处理器。
1、JSqlParserJSqlParser卖力解析SQL语句,并转化为Java类的层次构造。我们可以先看个大略的例子来认识它。
public static void main(String[] args) throws JSQLParserException {String insertSql = "insert into user (id,name,age) value(1001,'范闲',20)";Statement parse = CCJSqlParserUtil.parse(insertSql);Insert insert = (Insert) parse;String tableName = insert.getTable().getName();List<Column> columns = insert.getColumns();ItemsList itemsList = insert.getItemsList();System.out.println("表名:"+tableName+" 列名:"+columns+" 属性:"+itemsList);}输出: 表名:user 列名:[id, name, age] 属性:(1001, '范闲', 20)
我们可以看到,JSqlParser可以解析出SQL的语法信息。相应的,我们也可以变动工具内容,从而达到修正SQL语句的目的。
2、算法处理器我们的分片算法有多个,详细该当调用哪一个是在程序运行期来决定的。以是,我们利用一个Map先将算法注册起来,然后根据分片模式来调用它。这也是策略模式的表示。
@Componentpublic class AlgorithmHandler { private Map<String, Algorithm> algorithm = new HashMap<>(); @PostConstruct public void init(){ algorithm.put("range",new RangeAlgorithm()); algorithm.put("hash",new HashAlgorithm()); } public String handler(String mode,String name,Object value,int length){ return algorithm.get(mode).doSharding(name, value,length); }}
3、拦截器
我们知道,MyBatis许可你在已映射语句实行过程中的某一点进行拦截调用。
如果你对它的事理还不熟习,那么可以先看看笔者的文章:Mybatis拦截器的事理。
整体来看,它的流程如下:
通过Mybatis拦截待实行的SQL;通过JSqlParser解析SQL,获取逻辑表名等;调用分片算法获取真实表名;修正SQL,并修正BoundSql;Mybatis实行修正后的SQL,达成目的。比如,对付insert语句和update语句,它的核心代码如下:
五、查询及分页
事实上,新增和修正都比较大略,较为繁芜的是查询语句。
但是,我们的插件并不在于要知足所有的查询语句,而是可以根据真实的业务场景来扩展修正。
不过分页功能基本上是逃不开的。拿PageHelper为例,它的事理也是通过Mybatis拦截器来实现的。如果它和我们的分表插件在一起,可能会产生冲突。
以是在分表插件中,笔者也集成了分页功能,基本上和PageHelper一样,但并未直策应用它。其余,对付查询来说,在查询条件中是否带有分片键,也是很关键的地方。
1、查询在范围算法中,在业务上我们哀求只查询特定某一个月或者近几个月的数据即可;在Hash算法中,我们则哀求每次都带有主键。
但第二个条件每每不能成立,业务方也知足不了每次都必须带有主键。
针对这种情形,我们只能遍历所有的表,查询符合条件的数据,然后再汇总返回;
这种办法的缺陷显而易见,性能较差。还有一种办法便是可以将常用的查询条件与分片键建立映射关系,在查询时先根据查询条件找到分片键的字段值,然后再根据分片键查询。
2、分页如上所言,插件中集成了分页功能,实现流程与PageHelper一样,但考虑到冲突,并未直策应用。
作者:清幽之地原文链接:https://juejin.im/post/5dfc6cc0518825126f3735d7