概述
Mycat是一个开源的分布式数据库中间件,是个实现了MySQL协议的的Server,也可以理解为是数据库代理。在架构体系中是位于数据库和应用层之间的一个组件,并且对于应用层是透明的,即数据库感受不到mycat的存在,认为是直接连接的mysql数据库。其核心功能是分表分库,即将一个大表水平分割为N个小表,存储在后端MySQL服务器里或者其他数据库里。
原理
Mycat的原理中最重要的一个动词是“拦截”,它拦截了用户发送过来的SQL语句,首先对SQL语句做了一些特定的分析:如分片分析、路由分析、读写分离分析、缓存分析等,然后将此SQL发往后端的真实数据库,并将返回的结果做适当的处理,最终再返回给用户。
orders表被分为三个分片,这三个分片是分布在两台MySQL Server上。
应用场景
- 单纯的读写分离,此时配置最为简单,支持读写分离,主从切换
- 分表分库,对于超过1000万的表进行分片,最大支持1000亿的单表分片
- 替代Hbase,分析大数据
Mycat中的概念
- 逻辑库(schema)
与MySQL中的Database(数据库)对应,一个逻辑库中定义了所包括的Table (逻辑表)。
- 逻辑表
分布式数据库中,对应用来说,读写数据的表就是逻辑表。逻辑表,可以是数据切分后,分布在一个或多个分片库中,也可以不做数据切分,不分片,只有一个表构成。
-
- 分片表
分片表,是指那些原有的很大数据的表,需要切分到多个数据库的表,这样,每个分片都有一部分数据,所有分片构成了完整的数据。
例如在mycat配置中的t_node就属于分片表,数据按照规则被分到dn1,dn2两个分片节点(dataNode)上。
<table name="t_node" primaryKey="id" dataNode="dn1,dn2" rule="rule1" /> |
-
- 非分片表
一个数据库中并不是所有的表都很大,某些表是可以不用进行切分的,非分片是相对分片表来说的,就是那些不需要进行数据切分的表。
<table name="t_node" primaryKey="id" dataNode="dn1" /> |
-
- ER表
关系型数据库是基于实体关系模型(Entity-Relationship Model)之上,通过其描述了真实世界中事物与关系,Mycat中的ER表即是来源于此。根据这一思路,提出了基于E-R关系的数据分片策略,子表的记录与所关联的父表记录存放在同一个数据分片上,即子表依赖于父表,通过表分组(Table Group)保证数据Join不会跨库操作。
表分组(Table Group)是解决跨分片数据join的一种很好的思路,也是数据切分规划的重要一条规则。
<table name="customer" primaryKey="id" dataNode="dn1" > <childTable name="orders" primaryKey="id" joinKey="customer_id" parentKey="id" /> </table> |
-
- 全局表
一个真实的业务系统中,往往存在大量的类似字典表的表,这些表基本上很少变动,对于这类的表,在分片的情况下,当业务表因为规模而进行分片以后,业务表与这些附属的字典表之间的关联,就成了比较棘手的问题,所以Mycat中通过数据冗余来解决这类表的join,即所有的分片都有一份数据的拷贝,所有将字典表或者符合字典表特性的一些表定义为全局表。
数据冗余是解决跨分片数据join的一种很好的思路,也是数据切分规划的另外一条重要规则。
<table name="company" primaryKey="id" type="global" dataNode="dn1,dn2,dn3" /> |
- 分片节点(dataNode)
数据切分后,一个大表被分到不同的分片数据库上面,每个表分片所在的数据库就是分片节点(dataNode)。
<dataNode name="dn1" dataHost="localhost1" database="db1" /> |
- 节点主机(dataHost)
数据切分后,每个分片节点(dataNode)不一定都会独占一台机器,同一机器上面可以有多个分片数据库,这样一个或多个分片节点(dataNode)所在的机器就是节点主机(dataHost),为了规避单节点主机并发数限制,尽 量将读写压力高的分片节点(dataNode)均衡的放在不同的节点主机(dataHost)。
<dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100"> <heartbeat>select user()</heartbeat> <!-- can have multi write hosts --> <writeHost host="hostM1" url="localhost:3306" user="root" password="root"> <!-- can have multi read hosts --> <readHost host="hostS2" url="localhost:3307" user="root" password="root" /> </writeHost> </dataHost> |
- 分片规则(rule)
前面讲了数据切分,一个大表被分成若干个分片表,就需要一定的规则,这样按照某种业务规则把数据分到某个分片的规则就是分片规则,数据切分选择合适的分片规则非常重要,将极大的避免后续数据处理的难度。
Mycat一个逻辑库到物理库(mysql)的映射关系:
分片规则概述
在数据切分处理中,特别是水平切分中,中间件最终要的两个处理过程就是数据的切分、数据的聚合。选择合适的切分规则,至关重要,因为它决定了后续数据聚合的难易程度,甚至可以避免跨库的数据聚合处理。
数据库分片
- 垂直拆分
垂直切分是指按照业务将表进行分类,分布到不同的数据库上面。 - 水平拆分
相对于垂直拆分,水平拆分不是将表做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中包含一部分数据。
分片规则设计架构
分布式数据库系统中,分片规则用于定义数据与分片的路由关系,也就是insert,deleted,update,select 的基本sql操作中,如何将sql路由到对应的分片去执行。
分片规则是最终解析sql到那个分片执行的规则,Mycat分片是根据分片字段来确定数据的分布,即根据预先配置好的分片字段(只有一个)到分片规则中解析该字段对应的值应该路由到哪个分片,然后确认sql到哪个分片执行。
Mycat路由分发流程
从原理上看,可以把mycat看成一个sql转发器。mycat接收前端发过来的sql,然后转发到后台的mysql服务器上去执行。后台有很多台mysql节点如dn1,dn2,dn3,;由mycat按照一定的路由规则转发到具体的节点上。
路由计算流程
无表路由计算:sql路由到任意一个datanode即可。
全局表路由计算:sql路由到所有节点。
单表路由计算:
多表路由计算:
分库分表原则
分表分库虽然能解决大表对数据库系统的压力,但它并不是万能的,也有一些不利之处,因此首要问题是,分不分库,分哪些库,什么规则分,分多少分片。
- 能不分就不分,1000万以内的表,不建议分片,通过合适的索引,读写分离等方式,可以很好的解决性能问题。
- 分片数量尽量少,分片尽量均匀分布在多个DataHost上,因为一个查询SQL跨分片越多,则总体性能越差,虽然要好于所有数据在一个分片的结果,只在必要的时候进行扩容,增加分片数量。
- 分片规则需要慎重选择,分片规则的选择,需要考虑数据的增长模式,数据的访问模式,分片关联性问题,以及分片扩容问题,最近的分片策略为范围分片,枚举分片,一致性Hash分片,这几种分片都有利于扩容。
- 尽量不要在一个事务中的SQL跨越多个分片,分布式事务一直是个不好处理的问题。
- 查询条件尽量优化,尽量避免select * 的方式,大量数据结果集下,会消耗大量带宽和CPU资源,查询尽量避免返回大量结果集,并且尽量为频繁使用的查询语句建立索引。
数据拆分原则
- 达到一定数量级才拆分(800万)
- 不到800万但跟大表(超800万的表)有关联查询的表也要拆分,在此称为大表关联表
- 大表关联表如何拆:小于100万的使用全局表;大于100万小于800万跟大表使用同样的拆分策略;无法跟大表使用相同规则的,可以考虑从java代码上分步骤查询,不用关联查询,或者破例使用全局表。
- 破例的全局表:如item_sku表250万,跟大表关联了,又无法跟大表使用相同拆分策略,也做成了全局表。破例的全局表必须满足的条件:没有太激烈的并发update,如多线程同时update同一条id=1的记录。虽有多线程update,但不是操作同一行记录的不在此列。多线程update全局表的同一行记录会死锁。批量insert没问题。
- 拆分字段是不可修改的
- 拆分字段只能是一个字段,如果想按照两个字段拆分,必须新建一个冗余字段,冗余字段的值使用两个字段的值拼接而成(如大区+年月拼成zone_yyyymm字段)。
- 拆分算法的选择和合理性评判:按照选定的算法拆分后每个库中单表不得超过800万
- 能不拆的就尽量不拆。如果某个表不跟其他表关联查询,数据量又少,直接不拆分,使用单库即可。
Mycat常用的分片规则
分片枚举
通过在配置文件中配置可能的枚举id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省份或区县来做保存,而全国省份区县固定的,这类业务使用本条规则,配置如下:
<tableRule name="sharding-by-intfile"> <rule> <columns>city</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> <property name="type">1</property> <property name="defaultNode">0</property> </function> partition-hash-int.txt 配置: 北京=0 上海=1 DEFAULT_NODE=1 |
上面columns 标识将要分片的表字段,algorithm 分片函数, 其中分片函数配置中,mapFile标识配置文件名称,type默认值为0,0表示Integer,非零表示String, 所有的节点配置都是从0开始,及0代表节点1
范围约定
此分片适用于,提前规划好分片字段某个范围属于哪个分片, start <= range <= end.
range start-end ,data node index
<tableRule name="auto-sharding-long"> <rule> <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> <property name="defaultNode">0</property> </function> autopartition-long.txt 配置: #K=1000条记录,M=10000条记录 0-500M=0 500M-1000M=1 1000M-1500M=2 |
配置说明: mapFile代表配置文件路径 defaultNode 超过范围后的默认节点。 所有的节点配置都是从0开始,及0代表节点1,此配置非常简单,即预先制定可能的id范围到某个分片
固定分片hash算法
本条规则类似于十进制的求模运算,区别在于是二进制的操作,是取id的二进制低10位,即id二进制&1111111111。 此算法的优点在于如果按照10进制取模运算,在连续插入1-10时候1-10会被分到1-10个分片,增大了插入的事务控制难度,而此算法根据二进制则可能会分到连续的分片,减少插入事务事务控制难度。
<tableRule name="rule1"> <rule> <columns>id</columns> <algorithm>func1</algorithm> </rule> </tableRule> <function name="func1" class="io.mycat.route.function.PartitionByLong"> <property name="partitionCount">2,1</property> <property name="partitionLength">256,512</property> </function> |
配置说明: partitionCount 分片个数列表,partitionLength 分片范围列表 分区长度:默认为最大2^n=1024 ,即最大支持1024分区 约束 : count,length两个数组的长度必须是一致的。 1024 = sum((count[i]*length[i])). count和length两个向量点积恒等于1024
用法例子: 本例的分区策略:希望将数据水平分成3份,前两份各占25%,第三份占50%。(故本例非均匀分区)
// |<———————1024———————————>|
// |<—-256—>|<—-256—>|<———-512————->|
// | partition0 | partition1 | partition2 |
取模
此规则为对分片字段求摸运算。
<tableRule name="mod-long"> <rule> <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">3</property> </function> |
配置说明: 此种配置非常明确即根据id进行十进制求模预算,相比固定分片hash,此种在批量插入时可能存在批量插入单事务插入多数据分片,增大事务一致性难度。
按日期(天)分片
此规则为按天分片。
<tableRule name="sharding-by-date"> <rule> <columns>create_time</columns> <algorithm>sharding-by-date</algorithm> </rule> </tableRule> <function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate"> <property name="dateFormat">yyyy-MM-dd</property> <property name="sBeginDate">2018-01-01</property> <property name="sEndDate">2018-01-30</property> <property name="sPartionDay">10</property> </function> |
配置说明: dateFormat :日期格式; sBeginDate :开始日期; sEndDate:结束日期; sPartionDay :分区天数,即默认从开始日期算起,分隔10天一个分区
自然月分片
按月份列分区 ,每个自然月一个分片
<tableRule name="sharding-by-month"> <rule> <columns>create_time</columns> <algorithm>sharding-by-month</algorithm> </rule> </tableRule> <function name="sharding-by-month" class="org.opencloudb.route.function.PartitionByMonth"> <property name="dateFormat">yyyy-MM-dd</property> <property name="sBeginDate">2018-01-01</property> </function> |
取模范围约束
此种规则是取模运算与范围约束的结合,主要为了后续数据迁移做准备,即可以自主决定取模后数据的节点分布。
<tableRule name="sharding-by-pattern"> <rule> <columns>user_id</columns> <algorithm>sharding-by-pattern</algorithm> </rule> </tableRule> <function name="sharding-by-pattern" class="io.mycat.route.function.PartitionByPattern"> <property name="patternValue">256</property> <property name="defaultNode">2</property> <property name="mapFile">partition-pattern.txt</property> </function> partition-pattern.txt 配置: # id partition range start-end ,data node index ###### first host configuration 1-32=0 33-64=1 65-96=2 97-128=3 ######## second host configuration 129-160=4 161-192=5 193-224=6 225-256=7 0-0=7 |
配置说明: patternValue 即求模基数,defaoultNode 默认节点,mapFile 配置文件路径 配置文件中,1-32 即代表id%256后分布的范围,如果在1-32则在分区1,其他类推,如果id非数据,则会分配在defaoultNode 默认节点
应用指定
此规则是在运行阶段有应用自主决定路由到那个分片。
<tableRule name="sharding-by-substring"> <rule> <columns>id</columns> <algorithm>sharding-by-substring</algorithm> </rule> </tableRule> <function name="sharding-by-substring" class="io.mycat.route.function.PartitionDirectBySubString"> <property name="startIndex">0</property><!-- zero-based --> <property name="size">2</property> <property name="partitionCount">8</property> <property name="defaultPartition">0</property> </function> |
配置说明: 此方法为直接根据字符子串(必须是数字)计算分区号(由应用传递参数,显式指定分区号)。
例如id=05-10000 在此配置中代表根据id中从startIndex=0,开始,截取siz=2位数字即05,05就是获取的分区,如果没传默认分配到defaultPartition
截取数字hash解析
此规则是截取字符串中的int数值hash分片。
<tableRule name="sharding-by-stringhash"> <rule> <columns>id</columns> <algorithm>sharding-by-stringhash</algorithm> </rule> </tableRule> <function name="sharding-by-stringhash" class="io.mycat.route.function.PartitionByString"> <property name="partitionLength">512</property><!-- zero-based --> <property name="partitionCount">2</property> <property name="hashSlice">0:2</property> </function> |
配置说明: partitionLength代表字符串hash求模基数,partitionCount分区数,
hashSlice hash预算位,即根据子字符串中int值 hash运算
/**
* “2” -> (0,2)
* “1:2” -> (1,2)
* “1:” -> (1,0)
* “-1:” -> (-1,0)
* “:-1” -> (0,-1)
* “:” -> (0,0)
*/
一致性hash
一致性hash预算有效解决了分布式数据的扩容问题。
<tableRule name="sharding-by-murmur"> <rule> <columns>id</columns> <algorithm>murmur</algorithm> </rule> </tableRule> <function name="murmur" class="io.mycat.route.function.PartitionByMurmurHash"> <property name="seed">0</property><!-- 默认是0--> <property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片--> <property name="virtualBucketTimes">160</property> <!-- 一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍--> </function> |
将数据均匀分布在各个节点中。
对其进行哈希,取值在 0 ~ 232-1 闭环中定位到顺时针第一个节点,将此数据分配其中。
由于节点有限,可能取哈希分布不均。
设置虚拟节点比如160,先将哈希分布在160节点上,然后把对应的节点聚合到真实节点中。