首页 > 代码库 > 架构设计:系统存储(14)——MySQL横向拆分与业务透明化(2)

架构设计:系统存储(14)——MySQL横向拆分与业务透明化(2)

接上文《架构设计:系统存储(13)——MySQL横向拆分与业务透明化(1)》

4-6、主要分片规则

上文提到MyCat的逻辑表支持多种分片规则,表现于schema配置文件中中table标签的rule属性。本节将以MyCat Version 1.6版为基础,介绍几种经常使用的分片规则,这些分片规则都通过rule.xml文件进行定义和配置。

4-6-1、分片枚举sharding-by-intfile

......
<tableRule name="sharding-by-intfile">
    <rule>
        <!-- columns表示分片计算时的取值列,记得设置成您的数据表列名 -->
        <columns>sharding_id</columns>
        <algorithm>hash-int</algorithm>
    </rule>
</tableRule>
......
<function name="hash-int"
 class="io.mycat.route.function.PartitionByFileMap">
    <property name="mapFile">partition-hash-int.txt</property>
</function>
......

实现类io.mycat.route.function.PartitionByFileMap,这个分片规则是直接按照partition-hash-int.txt文件(默认)中定义的固定分片规则进行分片。其中可以设置的属性包括:

  • type:这个分片规则也不支持按照字符串为分片依据,当type属性的值为0时表示Integer,非零表示String,默认的属性值为0。

  • defaultNode:从MyCat Version 1.6版本开始,rule.xml配置文件中就不再设置默认分片节点了。但是这个属性还是存在的,如果碰到不识别的枚举值,就让它路由到默认节点;如果不配置默认节点(defaultNode值小于0表示不配置默认节点),碰到不识别的枚举值就会报错:like this:can’t find datanode for sharding column:column_name val:ffffffff。

  • mapFile:固定枚举分片所设置的枚举规则存储的文件,默认的文件名就是partition-hash-int.txt,并且和rule.xml文件在一个工作目录下。partition-hash-int.txt文件的定义类似如下:

    
    # 如果取值列的值为10000则将数据送入0号分片
    
    10000=0
    10010=1

我们来跟踪一下PartitionByFileMap类的源码,会发现这个分片规则工作很简单——简单有它的好处,不会在分片过程中浪费太多的计算时间计算分片目标:

......
public class PartitionByFileMap extends AbstractPartionAlgorithm implements RuleAlgorithm {
    ......
    // 默认节点在map中的key
    private static final String DEFAULT_NODE = "DEFAULT_NODE";
    // 在配置文件的配置规则会被初始化到app2Partition对象中
    // 这个Map对象的value表示分片节点编号——从0开始编号
    private Map<Object, Integer> app2Partition;
    ......
    public Integer calculate(String columnValue) {
        // columnValue既是当前将要进行的数据分片所取的列值
        Object value = http://www.mamicode.com/columnValue;"hljs-comment">// 如果配置文件设置了type为0,则需要将值转为数字
        if(type == 0) {
            value = http://www.mamicode.com/Integer.valueOf(columnValue);"hljs-keyword">null;
        // 取得关联的分片,从app2Partition对象中
        // 注意,这个地方的变量命名可能会使阅读者产生歧义
        // 实际上这value对象代表了app2Partition中的key信息。
        Integer pid = app2Partition.get(value);
        if (pid != null) {
            rst = pid;
        }
        // 如果没有取到对应的分片,则试图取默认分片
        else {
            rst =app2Partition.get(DEFAULT_NODE);
        }
        return rst;
    }
}
......

4-6-2、取模的分片方式:mod-long

......
<tableRule name="mod-long">
    <rule>
        <!-- columns表示分片计算时的取值列,记得设置成您的数据表列名 -->
        <columns>id</columns>
        <algorithm>mod-long</algorithm>
    </rule>
</tableRule>
......
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
    <!-- how many data nodes -->
    <property name="count">2</property>
</function>
......

实现类:io.mycat.route.function.PartitionByMod,这个分片规则只适合数字形式的列,并且规则的实现更简单:既是按照将要插入数据的指定列进行取模运算。该规则需要设置一个count属性,这个数据就是现存的分片节点的个数——用于进行取模运算的基数:

......
public class PartionByMod extends AbstractPartionAlgorithm implements RuleAlgorithm  {
    ......
    # 该属性设置当前的分片节点数量
    private int count;
    ......
    public Integer calculate(String columnValue) {
        # 取绝对值
        BigInteger bigNum = new BigInteger(columnValue).abs();
        # 返回取模运算的值
        # 如果您不清楚BigInteger类型的特性,请自行查阅资料
        return (bigNum.mod(BigInteger.valueOf(count))).intValue();
    }
    ......
}
......

4-6-3、约定数字范围auto-sharding-long

......
<tableRule name="auto-sharding-long">
    <rule>
        <!-- columns表示分片计算时的取值列,记得设置成您的数据表列名 -->
        <columns>id</columns>
        <algorithm>rang-long</algorithm>
    </rule>
</tableRule>
......
<function name="rang-long" class="io.mycat.route.function.AutoPartitionByLong">
    <property name="mapFile">autopartition-long.txt</property>
</function>
......

实现类:io.mycat.route.function.AutoPartitionByLong,这个规则下MyCat会使用autopartition-long.txt文件(默认的文件路径)中已经设置好的数字范围决定将数据存储到哪个分片中,autopartition-long.txt文件中的配置类似如下:

# 这个配置文件的含义是
# 指定分片列的数值为0——5000000的数据都在0号分片中
0-500M=0
# 指定分片列的数值为5000000——10000000的数据都在1号分片中
500M-1000M=1
1000M-1500M=2

如果您的业务系统后续考虑方便的增加分片节点,可以考虑优先使用这种分片规则,因为这种固定设置的分片规则既简单又实用。以下是AutoPartitionByLong类的重要判断代码,通过查看代码中主要的分片过程,有助于我们理解这种分片规则:

......
public class AutoPartitionByLong extends AbstractPartionAlgorithm implements RuleAlgorithm {
    private String mapFile;
    // defaultNode设置为-1,表示没有默认的分片节点。
    private int defaultNode = -1;
    ......
    // longRongs在初始化时被设置
    // 在autopartition-long.txt文件中有几行设置范围,就有几个LongRange对象。(参见该类中没有列出的initialize方法)
    private LongRange[] longRongs;
    ......
    public Integer calculate(String columnValue) {
        long value = http://www.mamicode.com/Long.valueOf(columnValue);"hljs-keyword">null;
        for (LongRange longRang : this.longRongs) {
            // 如果条件成立,就说明找到了可以承载columnValue值的那个数据库分片编号
            if (value <= longRang.valueEnd && value >= longRang.valueStart) {
                return longRang.nodeIndx;
            }
        }
        //数据超过范围,暂时使用配置的默认节点(如果有的话)
        if(rst ==null && defaultNode >= 0){
            return defaultNode;
        }
        // 返回null,则说明没有找到可承载columnValue的分片编号
        // 也没有进行默认分片的设置(这是MyCat会报错)
        return rst;
    }

    ......

    // LongRange对象有三个属性:
    static class LongRange {
        // 该属性为当前范围所存储的分片编号
        public final int nodeIndx;
        // 该属性为范围的开始值
        public final long valueStart;
        // 该属性为范围的结束值
        public final long valueEnd;
        ......
        }
    }
    ......
}

注意,在以上代码片段的注释中有一句“暂时使用配置的默认节点”。这句话不是笔者的注释,而是该类的编写者“wuzhi”加的注释,也就是说这里判断成立后的处理代码可能会在后续版本中进行修改。

4-6-4、自然月分片:sharding-by-month

......
<tableRule name="sharding-by-month">
    <rule>
        <!-- columns属性同样表示分片计算时的取值列,记得设置成您的数据表列名 -->
        <columns>create_date</columns>
        <algorithm>partbymonth</algorithm>
    </rule>
</tableRule>
......
<function name="partbymonth" class="io.mycat.route.function.PartitionByMonth">
    <property name="dateFormat">yyyy-MM-dd</property>
    <property name="sBeginDate">2015-01-01</property>
</function>
......

实现类io.mycat.route.function.PartitionByMonth,该分片规则可按照每个自然月为一个分片数据库,也就是说技术团队可以将数据放置到分片数据库中,并且每个月增加一个分片数据库,而不对之前的分片数据库产生影响。该规则有三个属性可以设置:

  • dateFormat:日期数据列的数据格式。该属性必须要设置,否则分区规则将无法进行正常工作。默认的配置文件中,这个数据都存在默认值,就是以上示例中看到的yyyy-MM-dd。

  • sBeginDate:这个属性是指存储的数据中,涉及的分区日期列的开始时间。

  • sEndDate:这个属性是指存储的数据中,涉及的分区日期列的结束时间,该值可以不进行设置。实际上设置sEndDate和不设置sEndDate,PartitionByMonth将按照两种不同的方式工作,详见下文代码分析。

......
public class PartitionByMonth extends AbstractPartitionAlgorithm implements RuleAlgorithm {
    ......
    // 以下三个属性就是技术人员在配置文件中设置的属性
    private String sBeginDate;
    private String dateFormat;
    private String sEndDate;
    // 日期计算对象,专门用于根据设置开始事件和结束时间参与分片计算
    private Calendar beginDate;
    private Calendar endDate;
    // 这个参数很重要,分区总数,后面的代码分析中会进行讲述
    private int nPartition;

    ......

    public void init() {
        ......
        // 关键点在这里,当技术人员设置了sEndDate的值后
        // 该规则将计算一个nPartition的参数值
        if(sEndDate!=null&&!sEndDate.equals("")) {
            endDate = Calendar.getInstance();
            endDate.setTime(new SimpleDateFormat(dateFormat).parse(sEndDate));
            // nPartition 为结束时间和开始时间的年差 * 12
            // 加上结束时间和开始时间的月差 最后再 + 1
            // 如果结束时间为2016-05,开始时间为2015-08则:
            // 12 + 7 - 4 + 1 = 16个月
            nPartition = ((endDate.get(Calendar.YEAR) - beginDate.get(Calendar.YEAR)) * 12
                + endDate.get(Calendar.MONTH) - beginDate.get(Calendar.MONTH)) + 1;
            // nPartition出现小于0的情况,则说明开始时间和结束时间的设置错误。
            if (nPartition <= 0) {
                throw new java.lang.IllegalArgumentException("Incorrect time range for month partitioning!");
            }
        }
        // 如果没有设置结束时间,则nPartition为-1。
        else {
            nPartition = -1;
        }
    }

    ......

    // 正式的分片计算
    public Integer calculate(String columnValue)  {
        try {
            int targetPartition;
            Calendar curTime = Calendar.getInstance();
            curTime.setTime(formatter.get().parse(columnValue));

            // 目标分片的计算方式为:
            // 当前时间和开始时间的年差 * 12 
            // 加上当前时间和开始时间的月差
            // 如果开始时间为2015-08,当前时间为2015-12则:
            // 0 + 11 - 7 = 4,也就是第五个分片库
            targetPartition = ((curTime.get(Calendar.YEAR) - beginDate.get(Calendar.YEAR))
                    * 12 + curTime.get(Calendar.MONTH) - beginDate.get(Calendar.MONTH));

            // 如果设置了结束时间,说明分片数量是有限制的,需要在reCalculatePartition方法中,基于targetPartition进行取余运算
            if (nPartition > 0) {
                targetPartition = reCalculatePartition(targetPartition);
            }
            return targetPartition;
        } catch (ParseException e) {
            throw new IllegalArgumentException(new StringBuilder().append("columnValue:").append(columnValue).append(" Please check if the format satisfied.").toString(),e);
        }
    }

    ......

    // 该私有方法负责基于nPartition进行取余
    private int reCalculatePartition(int targetPartition) {
        if (targetPartition < 0) {
            targetPartition = nPartition - (-targetPartition) % nPartition;
        }

        if (targetPartition >= nPartition) {
            targetPartition =  targetPartition % nPartition;
        }
        return targetPartition;
    }
}
......

4-6-5、其他分区规则和使用建议

在github上可以找到mycat最新版本的源代码(https://github.com/MyCATApache),MyCat Version 1.6的版本所带有的分区规则实现放置在io.mycat.route.function包下,可以看到包括已介绍的PartitionByFileMap、AutoPartitionByLong、PartitionByMod、PartitionByMonth实现在内的更多分片实现类(除了AbstractPartitionAlgorithm、PureJavaCrc32、NumberParseUtil等父类和工具类)。技术团队可以查看官方文档或者源码,然后根据自己的业务形态选择合适的分片规则:

技术分享

  • PartitionByMurmurHash:横量一个哈希算法的优劣,主要看其哈希值的散列均匀效果以及对Hash碰撞的兼容度。MurmurHash算法(有多个版本version 1/2/3)是一种公认的快速hash算法,并且散列均匀度也很不错。MyCat中的PartitionByMurmurHash类就是一个基于MurmurHash Version 3实现的一致性Hash算法,它通过一个红黑树结构(java.util.TreeMap,该数据结构的特性请参见其它资料)构造了一个一致性哈希环,并使用MurmurHash3算法将设置的分片节点分布到这个哈希环上,为了保证分布均匀该类中还使用了允许为不同分片节点设置不同的权重值,以及多次分布的办法。这是MyCat推荐的一种一致性哈希环分片规则。

  • PartitionByString:这个算法可以根据给定的字符串中的某一个子串(例如X字符串前两位、后两位)计算一个hash值,并根据这个Hash值确认存储的数据分片节点。

MyCat中自带的分片规则应该是比较完善了,虽然本文无法一一介绍,但是作为架构是应该清楚一些主要的分片处理逻辑。这里给出一些使用上的经验:

  • 除非有特别的技术层面要求,否则分片规则的选择一定不能和本身的业务形态脱离,所以不建议首先选用基于任何Hash算法的分片规则。试想一下这样的业务场景:业务团队预计将有超过5000万的用户基础数据会写入MySQL,所以设计了至少五组MyCat分片节点。为了保证整个数据的可维护度保持一定的简单性,至少应该让运维人员非常清楚某个ID的用户数据存放在哪个分片中,那么使用ID进行取模、用户名字符串的字串进行取模、用户生日月份进行分片都可以达到这样的效果,唯独Hash算法无法做到这一点。

  • MyCat数据库中间件有很好的横向扩展性,当进行分片规则选择时也需要考虑以后如何进行扩展。同样是存储用户基础数据的场景,业务团队首期计划的用户规模为100万,ID从1开始编号。那么这样的业务情况可以在首期准备一组MySQL读写分离集群,甚至可以不使用任何数据库中间件只做一组读分离。当技术团队开始第二期开发,预测用户量将达到1000万时,再上MyCat。而这时的分库规则可以采用基于长整形的固定范围,并且把首期的存储集群作为第一个分片组——这样甚至可以免去数据割接的繁琐工作。

  • MyCat作为数据库中间件本身不存储数据,在进行数据查询时只是重新分析SQL查询结构,并协调下层真实存储数据的分片节点完成查询动作,并最后在MyCat服务节点上完成整合过程(后文会提到一种跨分片的表关联模式,工作过程更为复杂)。这个过程显然要比直接在单个节点上查询数据的过程要长,且耗时更多。这是一个必然现象,所以您在选择这种分库分表方案时就要对大量数据单次的查询效率有心理准备。如果您的业务系统追求极致的查询响应时间,那么就有必要重新考虑一下您的基础架构设计了。

5、其它MySQL数据库集群方案

5-1、MySQL Proxy / MySQL Router

MySQL Router的前身是一个MySQL Proxy,后者是一个永远只放出了alpha版本的奇葩产品。MySQL Router官网对它的描述是:

MySQL Router is lightweight middleware that provides transparent routing between your application and any backend MySQL Servers. It can be used for a wide variety of use cases, such as providing high availability and scalability by effectively routing database traffic to appropriate backend MySQL Servers. The pluggable architecture also enables developers to extend MySQL Router for custom use cases.

简单来说呢,它是一个轻量级的数据库中间件。没有MyCat中复杂的分库分片规则、处理逻辑和事务性控制。从官网上下载的MySQL Router只支持技术团队做读写分离集群。但是它开放了它的“路由”接口,所以实际上技术团队可以基于它的工作模式自己做一套分库分表逻辑。

5-2、Mysql Galera Cluster

之前我们介绍的基于LVS的MySQL读写分离方案、基于编程框架的MySQL读写分离方案、基于MyCat的读写分离和分库分表方案,其中都依赖于一个基本技术,就是MySQL Replication日志复制技术,这些集群都需要使用它完成各读写节点的数据同步工作。而MySQL Replication日志复制技术由于本身的工作过程,存在一定的延迟问题——不严重但确实有延迟。

官方(http://galeracluster.com/products)对Mysql Galera Cluster的描述是:

Galera Cluster for MySQL is a true Multimaster Cluster based on synchronous replication. Galera Cluster is an easy-to-use, high-availability solution, which provides high system uptime, no data loss and scalability for future growth.

  • 同步复制:本专题中,包括前几篇文章在内的所有介绍MySQL日志同步的内容,提到了两种基于MySQL Binlog的日志同步方式。第一种是MySQL原生支持的异步数据复制方式;另一种是一个依靠第三方组件实现的半同步数据复制方式。后者在前者基础上增加了一个apply动作,保证所有Binlog完成更新后,再切换Salve节点状态。Galera提供了另一种主从复制技术,称为Galera Replication。这种复制技术最大的特点是可以保证主从节点数据的完全一致,并且不会大幅降低整个集群的I/O性能。

  • 真正的多主集群:Galera Cluster for MySQL支持多个节点同时作为数据写节点进行工作。MyCat的写节点和读节点分工非常明确,只有当写节点出现故障时另一个节点备用的写节点才会替换其工作。虽然在MyCat之前的版本中提供了多个写节点轮询的工作方式, 但是在后续的版本中也不再推荐使用这种工作模式。Galera Cluster for MySQL的多个节点可以同时从当写节点的角色,并且不会造成数据一致性问题。这实际上还是得益于它的数据复制技术。

  • 高可用高扩展且还算好用:Galera Cluster for MySQL推荐的集群节点数据最少为3个。当然两个节点的集群也可以工作,但是只要有其中一个节点崩溃,整个集群就会不可用。各位读者可以实际使用一下,再决定是否将它应用到自己的生产环境下。Galera Cluster最好的使用伴侣实际上不是MySQL,而是我们在之前文章中提到的一个分支产品MariaDB。

5-3、Amoeba

在MyCat Version 1.6 官方使用手册上,对TDDL、Amoeba、Cobar等几款数据库中间件进行了比较。Amoeba是一个以MySQL为底层数据存储,并对应用提供MySQL协议接口的proxy。注意proxy这个单词说的是代理,Amoeba的作用主要作用的请求路由转发、负载均衡,事实上包括Amoeba在内的TDDL、Cobar社区已经处于停滞状态,最后稳定发布的Amoeba版本依然不提供分库分表功能的支持。

6、其他参考资料和后文预告

关于MyCat技术手册可参见其官方的权威发布(基于version 1。6版本):http://www.mycat.org.cn/document/Mycat_V1.6.0.pdf。不过要注意其中一些描述有一定的时效性,例如其中对分片规则的讲述还是使用的迁移到GitHub之前的类命名方式。

MySQL相关的话题在本专题中就告一段落了,我们通过十篇文章介绍了MySQL中最常用的InnoDB数据引擎的工作原理和优化经验,接着有介绍了MySQL集群的几种搭建方案,包括读写分离性质的集群和分库分表的集群。在后续的文章中,我们将转向NoSQL部分的知识点讨论,首先要讨论的就是Redis。另外,本专题还没有讨论的MyCat集群中对分布式事务的支持,将在下一次整理中补充。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    架构设计:系统存储(14)——MySQL横向拆分与业务透明化(2)