如何定制分表中间件

前言java

通常来讲,影响数据库最大的性能问题有两个,一个是对数据库的操做,一个是数据库中的数据太大。对于前者咱们能够借助缓存来减小一部分读操做,针对一些复杂的报表分析和搜索能够交给 HadoopElasticSearch 。对于后者,咱们就只能分库分表,读写分离。redis

互联网行业随着业务的复杂化,大多数应用都会经历数据的垂直分区。一个复杂的流程会按照领域拆分红不一样的服务,每一个服务中心都拥有本身独立的数据库。拆分后服务共享,业务更清晰,系统也更容易扩展,同时减小了单库数据库链接数的压力,也在必定程度上提升了单表大数据量下索引查询的效率。固然业务隔离,也能够避免一个业务把数据库拖死致使全部业务都死掉,咱们将这种按照业务维度,把一个库拆分为多个不一样的库的方式叫作垂直拆分算法

垂直拆分也包含针对长表(属性不少)作冷热分离的拆分。例如,在商品系统设计中,一个商品的生产商、供销商以及特有属性,这些字段变化频率低,查询次数多,叫作冷数据;而商品的份额,关注量等相似的统计信息变化频率较高,叫作活跃数据或者热数据。在 MySQL 中,冷数据查询多更新少,适合用 MyISAM 存储引擎,而热数据更新比较频繁适合用 InnoDB,这也是垂直拆分的一种。spring

当单表数据量随着业务发展继续膨胀,在 MySQL 中当数据量达到千万级时,就须要考虑进行水平拆分了,这样数据就分散到不一样的表上,单表的索引大小获得控制,能够提高查询性能。当数据库的实例吞吐量达到性能瓶颈后,咱们须要水平扩展数据库的实例,让多个数据库实例分担请求,这种根据分片算法,将一个库拆分红多个同样结构的库,将多个表拆分红多个结构相同的表就叫作水平拆分sql


数据拆分也有不少缺点,数据分散,数据库的 Join 操做变得更加复杂,分片后数据的事务一致性很难保证,同时数据的扩容和维护难度增长,拆分规则也可能致使某个业务须要同时查询全部的表而后进行聚合。若是须要排序和函数计算则更加复杂,因此不到万不得已能够先没必要拆分。
数据库

根据分库分表方案中实施切片逻辑的层次不一样,咱们将分库分表的实现方案分红如下4种:缓存

1. 在应用层直接分片bash

这种方式将分片规则直接放在应用层,虽然侵入了业务,开发人员不只既须要实现业务逻辑也须要实现分库分表的配置的开发,可是实现起来简单,适合快速上线,经过编码方式也更容易实现跨表遍历的状况。后期故障也更容易定位,大多数公司都会在业务早期采用此种方式过渡,后期分表需求增多,则会寻求中间件来解决,如下代码为铜板街早期订单表在 DAO 层将分片信息以参数形式传到 mybatis 的 mapper 文件中的实现方案。服务器

1 @Override
 2 public OrderDO findByPrimaryKey(String orderNo) {
 3
 4    Assert.hasLength(orderNo, "订单号不能为空");
 5
 6    Map<String, Object> map = new HashMap<String, Object>(3);
 7    map.put("tableSuffix", orderRouter.routeTableByOrderNo(orderNo));
 8    map.put("dbSuffix", orderRouter.routeDbByOrderNo(orderNo));
 9    map.put("orderNo", orderNo);
10
11    Object obj = getSqlSession().selectOne("NEW_ORDER.FIND_BY_PRIMARYKEY", map);
12    if (obj != null && obj instanceof OrderDO) {
13        return (OrderDO) obj;
14    }
15    return null;
16 }
复制代码

2. 在ORM层直接分片微信

这种方式经过扩展第三方 ORM 框架,将分片规则和路由机制嵌入到 ORM 框架中,如hibernate 和 mybatis,也能够基于 spring jdbctemplate 来实现,目前实现方案较少。

3. 客户端定制 JDBC 协议

这种方式比较常见对业务侵入低。经过定制 JDBC 协议,针对业务逻辑层提供与 JDBC一致的接口,让开发人员没必要要关心分库分表的具体实现,分库分表在 JDBC 内部搞定,对业务层透明。目前流行的 ShardingJDBC、TDDL 便采用了这种方案。这种方案须要开发人员熟悉 JDBC 协议,研发成本较低,适合大多数中型企业。

4. 代理分片

此种分片方式,是在应用层和数据库层增长一个代理,把分片的路由规则配置在代理层,代理层提供与 JDBC 兼容的接口给应用层,开发人员不用关心分片逻辑实现,只须要在代理层配置便可。增长代理服务器,须要解决代理的单点问题增长硬件成本,同时全部的数据库请求增长了一层网络传输影响性能,固然维护也须要更资深的专家,目前采用这种方式的框架有 cobar 和 mycat。


切片算法

选取分片字段

分片后,若是查询的标准是根据分片的字段,则根据切片算法,能够路由到对应的表进行查询。若是查询条件中不包含分片的字段,则须要将全部的表都扫描一遍而后在进行合并。因此在设计分片的时候咱们通常会选择一个查询频率较高的字段做为分片的依据,后续的分片算法会基于该字段的值进行。例如根据建立时间字段取对应的年份,每一年一张表,取电话号码里面的最后一位进行分表等,这个分片的字段咱们通常会根据查询频率来选择。例如在互金行业,用户的持仓数据,咱们通常选择用户 id 进行分表,而用户的交易订单也会选择用户 id 进行分表,可是若是咱们要查询某个供应商下在某段时间内的全部订单就须要遍历全部的表,因此有时候咱们可能会须要根据多个字段同时进行分片,数据进行冗余存储。

分片算法

分片规则必须保证路由到每张物理表的数据量大体相同,否则上线后某一张表的数据膨胀的特别快,而其余表数据相对不多,这样就失去了分表的意义,后期数据迁移也有很高的复杂度。经过分片字段定位到对应的数据库和物理表有哪些算法呢?(咱们将分表后在数据库上物理存储的表名叫物理表,如 trade_order_01,trade_order_02,将未进行切分前的表名称做逻辑表如 trade_order)大体能够有如下分类:

  • 按日期 如年份,季度,月进行分表,这种维度分表须要注意在边缘点的垮表查询。例如若是是根据建立时间按月进行分片,则查询最近3天的数据可能须要遍历两张表,这种业务比较常见,可是放中间件层处理起来就比较复杂,可能在应用层特殊处理会简单点。

  • 哈希 ,这种是目前比较经常使用的算法,可是这里谨慎推荐,由于他的后期扩容是件很头痛的事情,例如根据用户 ID 对64取模,获得一个0到63的数字,这里最多能够切分64张表{0,1,2,3,4… 63},前期可能用不到这么多,咱们能够借助一致性哈希的算法,每4个连续的数字分红放到一张表里。例如 0,1,2,3 分到00这张表,4,5,6,7分到04这张表,用算法表示 floor(userID % 64 / 4) * 4 假设 floor为取整的效果。


  • 按照一致性哈希算法,当须要进行扩容一倍时须要迁移一半的数据量,虽然不至于迁移全部的数据,若是没有工具也是须要很大的开发量。下图中根据分表字段对16取余后分到4张表中,后面若是要扩容一倍则须要迁移一半的数据。


  • 截取 这种算法将字段中某一段位置的数据截取出来,例如取电话号码里面的尾数,这种方式实现起来简单,但在上线前必定要预测最终的数据分布是否会平均。好比地域,姓氏可能并不平均等,以4结尾的电话号码也相对偏少。

特别注意点

分库和分表算法须要保证不相关,上线前必定要用线上数据作预测。例如分库算法用“用户id%64 分64个库” 分表算法也用 “用户id%64 分64张表”,总计 64 * 64 张表,最终数据都将落在 如下 64张表中 00库00表,01库01表… 63库63表, 其余 64 * 63张表则没有数据。这里能够推荐一个算法,分库用 用户ID/64 % 64 , 分表用 用户ID%64 测试1亿笔用户id发现分布均匀。

在分库分表前须要规划好业务增加量,以预备多大的空间,计算分表后能够支持按某种数据增加速度能够维持多久。

如何实现客户端分片

客户端须要定制 JDBC 协议,在拿到待执行的 sql 后,解析 sql,根据查询条件判断是否存在分片字段。若是存在,再根据分片算法获取到对应的数据库实例和物理表名,重写 sql,而后找到对应的数据库 datasource 并获取物理链接,执行 sql,将结果集进行合并筛选后返回。若是没有分片字段,则须要查询全部的表,注意,即便存在分片字段,可是分片字段在一个范围内,可能也须要查询多个表,针对 select 之外的 sql 若是没有传分片字段建议直接抛出异常。


JDBC 协议

咱们先回顾下一个完整的经过 JDBC 执行一条查询 sql 的流程,其实 druid 也是在 JDBC 上作加强来作监控的,因此咱们也能够适当参考 druid 的实现。

1 @Test
 2 public void testQ() throws SQLException,NamingException{
 3    Context context = new InitialContext();
 4    DataSource dataSource = (DataSource)context.lookup("java:comp/env/jdbc/myDataSource");
 5    Connection connection = dataSource.getConnection();
 6    PreparedStatement preparedStatement = connection.prepareStatement("select * from busi_order where id = ?");
 7    preparedStatement.setString(1,"1");
 8    ResultSet resultSet = preparedStatement.executeQuery();
 9    while(resultSet.next()){
10       String orderNo =  resultSet.getString("order_no");
11       System.out.println(orderNo);
12    }
13
14    preparedStatement.close();
15    connection.close();
16 }
复制代码


  • datasource 须要提供根据分片结果获取对应的数据源的datasource,返回的connection应该是定制后的 connection,由于在执行 sql 前还没法知道是哪一个库哪一个表,因此只能返回一个逻辑意义上的 connection。
  • 哈希 connection 定制的 connection,须要实现获取 statement,执行 sql 关闭。设置auto commit 等方法,在执行 sql 和获取 statement 的时候应该进行路由找到物理表后 在执行操做。因为该 connection 是逻辑意义上的,针对关闭,设置 auto commit 等须要将关联的多个物理 connection 一块儿设置。
  • statement 定制化的 statement,因为和 connection 都提供了执行 sql 的方法,因此咱们能够将执行 sql 都交给一个执行器执行,connection 和 statement 中都经过这个执行器执行sql。在执行器重解析 sql 获取物理链接,结果集处理等操做。
  • resultset resultset 是一个迭代器,遍历的时候数据源由数据库提供,但咱们在某些有排序和 limit 的查询中,可能迭代器直接在内存中遍历数据。

SQL解析

sql 解析通常借助 druid 框架里面的 SQLStatementParser 类。解析好的数据都在 SQLStatement 中,固然有条件的能够本身研究 SQL 解析,不过可能工做量有点大。

  • 解析出 sql 类型,目前生成环境主要仍是4中 sql 类型: SELECT DELETE UPDATE INSERT ,目前是直接解析 sql 是否以上面4个单词开头便可,不区分大小写。

  • insert 类型须要区分,是不是批量插入,解析出 insert 插入的列的字段名称和对应的值,若是插入的列中不包含分片字段,将没法定位到具体插入到哪一个物理表,此时应该抛出异常。

  • delete 和 update 都须要解析 where 后的条件,根据查询条件里的字段,尝试路由到指定的物理表,注意此时可能会出现 where 条件里面 分片字段多是一个范围,或者分片字段存在多个限制。

  • select 和其余类型不一样的是,返回结果是一个 list,而其余三种 sql 直接返回状态和影响行数便可。同时 select 可能出现关联查询,以及针对查询结果进行筛选的操做,例如where 条件中除了普通的判断表达式,还可能存在 limit,order by,group by,having等,select 的结果中也可能包含聚合统计等信息,例如 sum,count,max,min,avg等,这些都须要解析出来方便后续结果集的处理,后续从新生成 sql 主要是替换逻辑表名为物理表名,并获取对应的数据库物理链接。

  • 针对 avg 这种操做,若是涉及查询多个物理表的,可能须要改写 sql 去查询 sum 和count 的数据或者 avg 和 count 的数据,改写须要注意可能原 sql 里面已经包含了count,sum等操做了。

分片路由算法

分片算法,主要经过一个表达式,从分片字段对应的值获取到分片结果,能够提供简单地 EL表达式,就能够实现从值中截取某一段做为分表数据,也能够提供通用的一致性哈希算法的实现,应用方只须要在 xml 或者注解中配置便可,如下为一致性哈希在铜板街的实现。

1 /** 2 * 最大真实节点数 3 */
 4 private int max;
 5
 6 /** 7 * 真实节点的数量 8 */
 9 private int current;
10
11 private int[] bucket;
12
13 private Set suffixSet;
14
15 public void init() {
16    bucket = new int[max];
17    suffixSet = new TreeSet();
18
19    int length = max / current;
20    int lengthIndex = 0;
21
22    int suffix = 0;
23
24    for (int i = 0; i < max; i++) {
25        bucket[i] = suffix;
26        lengthIndex ++;
27        suffixSet.add(suffix);
28        if (lengthIndex == length){
29            lengthIndex = 0;
30            suffix = i + 1;
31        }
32    }
33 }
34
35 public VirtualModFunction(int max, int current){
36    this.current = current;
37    this.max = max;
38    this.init();
39 }
40
41
42 @Override
43 public Integer execute(String columnValue, Map<String, Object> extension) {
44    return bucket[((Long) (Long.valueOf(columnValue) % max)).intValue()];
45 } 
复制代码

这里也能够顺带作一下读写分离,配置一些读操做路由到哪一个实例,写操做路由到哪一个实例,而且作到负载均衡,对应用层透明。

结果集合并

若是须要在多个物理表上执行查询,则须要对结果集进行合并处理,此处须要注意返回是一个迭代器 resultset。

  • 统计类 针对 sum count,max,min 只须要将每一个结果集的返回结果在作一个 max 和min,count 和 sum 直接相加便可,针对 avg 须要经过上面改写的 sql 获取 sum 和count 而后相除计算平均值。

  • 排序类大部分的排序都伴随着 limit 限制查询条数。例如返回结果须要查询最近的2000条记录,而且根据建立时间倒序排序,根据路由结果须要查询全部的物理表,假设是4张表,若是此时4张表的数据没有时间上的排序关系,则须要每张表都查询2000条记录,而且按照建立时间倒序排列,如今要作的就是从4个已经排序好的链表,每一个链表最多2000条数据,从新排序,选择2000条时间最近的,咱们能够经过插入排序的算法,每次分别从每一个链表中取出时间最大的一个,在新的结果集里找到位置并插入,直到结果集中存在2000条记录,可是这里可能存在一个问题,若是某一个链表的数据广泛比其余链表数据偏大,这样每一个链表取500条数据确定排序不许确,因此咱们还须要保证当前全部链表中剩下的数据的最大值比新结果集中的数据小。 而实际上业务层的需求可能并非仅仅取出2000条数据,而是要遍历全部的数据,这种要遍历全部数据集的状况,建议在业务层控制一张表一张表的遍历,若是每次都要去每张表中查询在排序严重影响效率,若是在应用层控制,咱们在后面在聊。

  • 聚合类 group by 应用层须要尽可能避免这种操做,这些需求最好能交给搜索引擎和数据分析平台进行,可是做为一个中间件,对于group by 这种咱们常常须要统计数据的类型仍是应该尽可能支持的,目前的作法是 和统计类处理相似,针对各个子集进行合并处理。

优化

以上流程基本能够实现一个简易版本的数据库分库分表中间件,为了让咱们的中间件更方便开发者使用,为平常工做提供更多地遍历性,咱们还能够从如下几点作优化。

和 spring 集成

针对哪些表须要进行分片,分片规则等,这些须要定制化的配置,咱们能够在程序里面手工编码,可是这样业务层又耦合了分表的逻辑,咱们能够借助 spring 的配置文件,直接将 xml 里的内容映射成对应的 bean 实例。

  1. 咱们首先要设计好对应的配置文件的格式,有哪些节点,每一个节点包含哪些属性,而后设计本身命名空间,和对应的 XSD 校验文件,XSD 文件放在 META-INF下。

  2. 编写 NamespaceHandlerSupport 类,注册每一个节点元素对应的解析器

  3. 1 public class BaymaxNamespaceHandler extends NamespaceHandlerSupport {
     2
     3    //com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler
     4    @Override
     5    public void init() {
     6        registerBeanDefinitionParser("table", new BaymaxBeanDefinitionParser(TableConfig.class, false));
     7        registerBeanDefinitionParser("context", new BaymaxBeanDefinitionParser(BaymaxSpringContext.class, false));
     8        registerBeanDefinitionParser("process", new BaymaxBeanDefinitionParser(ColumnProcess.class, false));
     9    }
    10
    11 }
    复制代码
  4. 在 META-INF 文件中增长配置文件 spring.handlers 中配置 spring遇到某个namespace下的节点后 经过哪一个解析器解析,最终返回配置实例。

    1http\://baymax.tongbanjie.com/schema/baymax-3.0=com.tongbanjie.baymax.spring.BaymaxNamespaceHandler
    复制代码
  5. 在 META-INF 文件中增长配置文件 spring.schema 中配置 spring遇到某个namespace下的节点后 经过哪一个XSD文件进行校验。

  6. 1http\://baymax.tongbanjie.com/schema/baymax-3.0.xsd=META-INF/baymax-3.0.xsd
    复制代码
  7. 能够借助ListableBeanFactory的getBeansOfType(Class clazz) 来获取某个class类型的全部实例,从而得到全部的配置信息。

固然也能够经过自定义注解进行申明,这种方式咱们能够借助 BeanPostProcessor 的时候判断类上是否包含指定的注解,可是这种方式比较笨重,并且所加注解的类必须在spring 容器管理中,也能够借助 ClassPathScanningCandidateComponentProvider 和 AnnotationTypeFilter 实现,或者直接经过 classloader 扫描指定的包路径。

如何支持分布式事务

因为框架自己经过定制 JDBC 协议实现,虽然最终执行 sql 的是经过原生 JDBC,可是对上层应用透明,同时也对上层基于 JDBC 实现的事物透明,spring 的事物管理器能够直接使用。

咱们考虑下如下问题,若是咱们针对多张表在一个线程池内并发的区执行 sql,而后在合并结果,这是否会影响 spring的 事物管理器?

首先 spring 的声明式事物是经过 aop 在切面作加强,事物开始先获取 connection 并设置 setAutocommit 为 fasle,事物结束调用 connection 进行 commit 或者 rollback,经过 threadlocal 保存事物上下文和所使用的 connection 来保证事物内多个 sql共用一个 connection 操做。可是若是咱们在解析 sql 后发现要执行多条 sql 语句,咱们经过线程池并发执行,而后等全部的结果返回后进行合并,(这里先不考虑,多个 sql 可能须要在不一样的数据库实例上执行),虽然经过线程池将致使 threadlocal 失效,可是咱们在 threadlocal 维护的是咱们本身定制的 connection,并非原生的 JDBC 里的 connection ,并且这里并发执行并不会让事物处理器没办法判断是否全部的线程都已经结束,而后进行 commit 或者 rollback 。由于这里的线程池是在咱们定制的 connection 执行 sql 过程当中运用的,确定会等到全部线程处理结束后而且合并数据集才会返回。因此在本地事物层面,经过定制化 JDBC 能够作到对上层事物透明。

若是咱们进行了分库,同一个表可能在多个数据库实例里,这种若是要对不一样实例里的表进行更新,那么将没法在使用本地事物,这里咱们不在讨论分布式事物的实现,因为二阶段提交的各类缺点,目前不多有公司会基于二阶段作分布式事物,因此咱们的中间件也能够根据本身的具体业务考虑是否要实现 XA,目前铜板街大部分分布式事物需求都是经过基于 TCC 的事物补偿作的,这种方式对业务幂等要求较高,同时要基于业务层实现回滚逻辑。

提供一个通用发号器

为何要提供一个发号器,咱们在单表的时候,可能会用到数据库的自增ID,但当分红多表后,每一个表都进行单独的ID自增,这样一个逻辑表内的ID 就会出现重复。

咱们能够提供一个基于逻辑表自增的主键ID 获取方式,若是没有分库只分表,能够在数据库中增长一个表维护每张逻辑表对应的自增ID。每次须要获取ID 的时候都先查询这个标当前的ID 而后加一返回,而后在写入数据库,为了并发获取的状况,咱们能够采用乐观锁,相似于CAS,update的时候传人之前的ID。若是被人修改过则从新获取,固然咱们也能够一次性获取一批ID例如一次获取100个,等这100个用完了在从新获取,为了不这100个还没用完,程序正常或非正常退出,在获取这100个值的时候就将数据库经过CAS更新为已经获取了100个值之和的值。

不推荐用 UUID,无序,太长占内存影响索引效果,不携带任何业务含义。

借助 ZOOKEEPER 的 zone 的版本号来作序列号。

借助 REDIS 的 INCR 命令,进行自增,每台 redis 设置不一样的初始值,可是设置相同的歩长。

1A:1,6,11,16,21
2B:2,7,12,17,22
3C:3,8,13,18,23
4D:4,9,14,19,24
5E:5,10,15,20,25
复制代码

snowflake算法:其核心思想是:使用41bit做为毫秒数,10bit做为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit做为毫秒内的流水号(意味着每一个节点在每毫秒能够产生 4096 个 ID)。

铜板街目前所使用的订单号规则: - 15位时间戳,4位自增序列,2位区分订单类型,7位机器ID,2位分库后缀,2位分表后缀 共32位 - 7位机器ID 经过IP来获取 - 15位时间戳精确到毫秒,4位自增序列,意味着单JVM1毫秒能够生成9999个订单 。


  • 最后4位能够方便的根据订单号定位到物理表,这里须要注意分库分表若是是根据一致性哈希算法,这个地方最好存最大值, 例如 用户id % 64 取余 最多能够分64张表,而目前可能用不到这么多,每相邻4个数字分配到一张表,共16张表,既 userID % 64 / 4 * 4 ,而这个地方存储 userID % 64 便可,没必要存最终分表的结果,这种方式方便后续作扩容,可能分表的结果变动了,可是订单号却没法进行变动。


1 @Override
 2 public String routeDbByUserId(String userId) {
 3    Assert.hasLength(userId, "用户ID不能为空");
 4
 5    Integer userIdInteger = null;
 6    try {
 7        userIdInteger = Integer.parseInt(userId);
 8    } catch (Exception ex) {
 9        logger.error("解析用户ID为整数失败" + userId, ex);
10        throw new RuntimeException("解析用户ID为整数失败");
11    }
12
13    //根据路由规则肯定,具体在哪一个库哪一个表 例如根据分库公式最终结果在0到63之间 若是要分两个库 mod为32 分1个库mod为64 分16个库 mod为4
14    //规律为 64 = mod * (最终的分库数或分表数)
15    int mod = orderSplitConfig.getDbSegment();
16
17    Integer dbSuffixInt = userIdInteger / 64 % 64 / mod * mod ;
18
19    return StringUtils.leftPad(String.valueOf(dbSuffixInt),  2, '0');
20}
21
22
23 @Override
24 public String routeTableByUserId(String userId) {
25
26    Assert.hasLength(userId, "用户ID不能为空");
27
28    Integer userIdInteger = null;
29    try {
30        userIdInteger = Integer.parseInt(userId);
31    } catch (Exception ex) {
32        logger.error("解析用户ID为整数失败" + userId, ex);
33        throw new RuntimeException("解析用户ID为整数失败");
34    }
35
36    //根据路由规则肯定,具体在哪一个库哪一个表 例如根据分表公式最终结果在0到63之间 若是要分两个库 mod为32 分1个库mod为64 分16个库 mod为4
37    //规律为 64 = mod * (最终的分库数或分表数)
38    int mod = orderSplitConfig.getTableSegment();
39
40    Integer tableSuffixInt = userIdInteger % 64 / mod * mod;
41
42    return StringUtils.leftPad( String.valueOf(tableSuffixInt),  2, '0');
43 }    
复制代码

如何实现跨表遍历

若是业务需求是遍历全部知足条件的数据,而不是只是为了取某种条件下前面一批数据,这种建议在应用层实现,一张表一张表的遍历,每次查询结果返回下一次查询的起始位置和物理表名,查询的时候建议根据 大于或小于某一个 ID 进行分页,不要 limit500,500这种,如下为铜板街的实现方式。

1 public List<T> select(String tableName, SelectorParam selectorParam, E realQueryParam) {
 2
 3    List<T> list = new ArrayList<T>();
 4
 5    // 定位到某张表
 6    String suffix = partitionManager.getCurrentSuffix(tableName, selectorParam.getLocationNo());
 7
 8    int originalSize = selectorParam.getLimit();
 9
10    while (true) {
11
12        List<T> ts = this.queryByParam(realQueryParam, selectorParam, suffix);
13
14        if (!CollectionUtils.isEmpty(ts)) {
15            list.addAll(ts);
16        }
17
18        if (list.size() == originalSize) {
19            break;
20        }
21
22        suffix = partitionManager.getNextSuffix(tableName, suffix);
23
24        if (StringUtils.isEmpty(suffix)) {
25            break;
26        }
27
28        // 查询下一张表 不须要定位单号 并且也只须要查剩下的size便可
29        selectorParam.setLimit(originalSize - list.size());
30        selectorParam.setLocationNo(null);
31    }
32
33    return list;
34 }复制代码

提供一个扩容工具和管理控制台作配置可视化和监控

  1. 监控能够借助 druid,也能够在定制的 JDBC 层本身作埋点,将数据以报表的形式进行展现,也能够针对特定的监控指标进行配置,例如执行次数,执行时间大于某个指定时间。

  2. 管理控制台,因为目前配置是在应用层,固然也能够把配置独立出来放在独立的服务器上,因为分片配置基本上没法在线修改,每次修改可能都伴随着数据迁移,因此基本上只能作展现,可是分表后咱们在测试环境执行 sql 去进行逻辑查询的时候,传统的 sql 工具没法帮忙作到自动路由,这样咱们每次查询可能都须要手工计算下分片结果,或者要连续写好几个 sql 以后在聚合,经过这个管理控制台咱们就能够直接根据逻辑表名写 sql,这样咱们在测试环境或者在线上核对数据的时候,就提升了效率。

  3. 扩容工具,笨办法只能先从老表查询在 insert 到新表,等到新表数据彻底同步完后,在切换到新的切片规则,因此咱们设计分片算法的时候,须要考虑到后面扩容,例如一致性哈希就须要迁移一半的数据(扩容一倍的话) 数据迁移若是出现故障,那将是个灾难,若是咱们要在不停机的状况下完成扩容,能够经过配置文件按如下流程来。

  • 准备阶段.将截至到某一刻的历史表数据同步到新表 例如截至2017年10月1日以前的历史数据,这些历史数据最好不会在被修改;

  • 阶段一.访问老表,写入老表;

  • 阶段二.访问老表,写入老表同时写入新表 (插入和修改);

  • 阶段三.将10月1日到首次写入新表之间的数据同步到新表 须要保证此时被迁移的数据所有都是终态;

  • 阶段四.访问新表,写入老表和新表;

  • 阶段五.访问新表,写入新表。

以上流程适用于,订单这种历史数据在达到终态后将不会在被修改,若是历史数据也可能被修改,则可能须要停机,或者经过 canel 进行数据同步。


做者简介

小强,铜板街资金端后台开发工程师,2015年6月加入铜板街。目前负责铜板街资金端清结算相关的开发。

                                        


 本文经过案例重点介绍了若是定制分表中间件,想了解更多关于Java方面内容,请扫码关注 “铜板街科技” 微信公众号,并在后台回复 ”分布式调度、“反射”等关键词获取更多精彩内容。 

相关文章
相关标签/搜索