单机数据库存在着性能的瓶颈,当数据量非常大时,我们可以通过数据切分来解决这个性能问题,将原本一台数据库中的数据,分散到多台数据库当中去,降低每一个单体数据库的负载。而且一些第三方的软件就已经为我们实现了这样的功能,比如说MyCat和Sharding-Jdbc。
数据切分根据切分的规则大致分为两种类型:垂直切分和水平切分。
垂直切分就是按照不同的表或者Schema切分到不同的数据库中,比如:订单表(order)和商品表(product)在同一个数据库中,而我们现在要对其切分,使得订单表(order)和商品表(product)分别落到不同的物理机中的不同的数据库中,使其完全隔离,从而达到降低数据库负载的效果。
水平切分相比垂直切分,更为复杂。它需要将一个表中的数据,根据某种规则拆分到不同的数据库中,例如:订单尾号为奇数的订单放在了订单数据库1中,而订单尾号为偶数的订单放在了订单数据库2中。这样,原本存在一个数据库中的订单数据,被水平的切分成了两个数据库。在查询订单数据时,我们还要根据订单的尾号,判断这个订单在数据库1中,还是在数据库2中,然后将这条SQL语句发送到正确的数据库中,查出订单。
随着互联网技术的发展,人们发现在互联网的系统应用大部分是一个读多写少的应用,比如电商系统,商品浏览的次数是比下单要多的。数据库承载压力大,主要是由这些读的请求造成的,那么我们是不是可以把读操作和写操作分开,让所有读的请求落到专门负责读的数据库上,所有写的操作落到专门负责写的数据库上,写库的数据同步到读库上,这样保证所有的数据修改都可以在读取时,从读库获得。如果系统的读请求比较多的话,读库可以多部署几台,这样读请求就可以均摊到多台读库上,降低每一个读库上的压力。但是在写数据的时候,数据要落在一个确定,且唯一的写库中。你当然可以部署多个写库,但是数据怎么分片是一个十分重要的问题。
读写分离给我们带来的好处是很多的,从数据流上看,他们的区别是,数据从写入到数据库,到从数据库取出,读写分离的架构多了一个同步的操作。大家想一想,同步操作的时间是多少,延迟如果太大对系统有没有影响,如果同步挂了怎么办?没错,这就是读写分离的弊端,当同步挂掉,或者同步延迟比较大时,写库和读库的数据不一致,这个数据的不一致,用户能不能接受,其他的业务场景能不能接受呢?这个要对不同的业务场景做具体的分析了。
一些对数据实时性要求不高的业务场景,可以考虑使用读写分离。但是对数据实时性要求比较高的场景,比如订单支付状态,还是不建议采用读写分离的,或者你在写程序时,老老实实的从写库去读取数据。网络延迟应该在5ms以内,这个对网络环境要求是非常高的,大家可以ping一下你网络中的其他机器,看看能不能达到这个标准。如果你的网络环境很好,达到了要求,那么使用读写分离是没有问题的,数据几乎是实时同步到读库,根本感觉不到延迟。
Mycat 是数据库中间件,就是介于数据库与应用之间,进行数据处理与交互的中间服务。由于前面讲的对数据进行分片处理之后,从原有的一个库,被切分为多个分片数据库,所有的分片数据库集群构成了整个完整的数据库存储。
如上图所表示,数据被分到多个分片数据库后,应用如果需要读取数据,就要需要处理多个数据源的数据。如果没有数据库中间件,那么应用将直接面对分片集群,数据源切换、事务处理、数据聚合都需要应用直接处理,原本该是专注于业务的应用,将会花大量的工作来处理分片后的问题,最重要的是每个应用处理将是完全的重复造轮子。
所以有了数据库中间件,应用只需要集中与业务处理,大量的通用的数据聚合,事务,数据源切换都由中间件来处理,中间件的性能与处理能力将直接决定应用的读写性能,所以一款好的数据库中间件至关重要。
MyCat发展到现在,使用的场景很丰富,常见的典型的应用场景有:
逻辑库(Schema)
在实际的开发中,开发人员不需要知道数据库中间件的存在,开发人员只需要有数据库的概念就可以了。所以数据库中间件可以被看做是一个或者多个数据库集群构成的逻辑库。
逻辑表(table)
既然有逻辑库,那么就有逻辑表,对于应用系统来说,读写数据的表,就是逻辑表。而逻辑表中的数据,则是被水平切分后,分布在不同的分片库中。假设库中有一张用户表,这个用户表就被称为逻辑表,而用户表又被水平切分为3个表,每一个表中都存储一部分用户数据。业务系统在进行用户数据的读写时,只需要操作逻辑表就可以了,后面的分片细节则由MyCat进行操作,这些对于业务开发人员来说时完全透明的。当然,有些表的数据量没有那么大,完全不需要进行分片,只在一个物理的数据库表中即可。
分片表与非分片表
凡是我们做的数据水平切分的表,我们把它叫做分片表。而数据量比较小,没有进行分片的表,我们叫它非分片表。
全局表
在真实的业务系统中,往往存在着大量的字典表,这些表的数据基本上很少变动,比如:订单状态。我们查询的时候,往往需要关联字典表去查询,比如:查询订单时,需要把订单状态关联查出,如果订单表做了分片,分布在不同的数据库中,而订单状态表由于数据量小,没有做分片,那么我们查询的时候就要跨库关联查询订单状态,增加了不必要的麻烦,不如我们干脆把订单状态表冗余到所有的订单分片库中,这样关联查询就不需要跨库了。我们把这种通过数据冗余方式复制到所有的分片库中的表,叫做全局表。
分片节点(dataNode)
数据被切分后,一张大表被分到不同的分片数据库上面,每个分片表所在的数据库就叫做分片节点。
节点主机(dataHost)
数据切分后,每一个分片节点不一定都会占用一个真正的物理主机,会存在多个分片节点在同一个物理主机上的情况,这些分片节点所在的主机就叫做节点主机。。为了避免单节点并发数的限制,尽量将读写压力高的分片节点放在不同的节点主机上。
分片规则(rule)
一个大表被拆分成多个分片表,就需要一定的规则,按照某种业务逻辑,将数据分到一个确定的分片当中,这个规则就叫做分片规则。数据切分选择合适的分片规则非常重要,这将影响到后的数据处理难度,结合业务,选择合适的分片规则,是对架构师的一个重大考验。对于架构师来说,选择分片规则是一个艰难的,难以抉择的过程。
全局序列号(sequence)
大家有没有想过,数据切分以后,数据库表的中的id怎么办?原来在一张表的时候,我们采用id自增,但是数据分布到多个库怎么办?比如:向用户表插入数据,第一条记录插入了用户库1,它的id为1;第二条记录插入了用户库2,如果是自增,它的id也为1。这样id就混乱了,我们也无法确定一条数据的唯一标识了。这时,我们需要借助外部的机制保证数据的唯一标识,这种保证数据唯一标识的机制,我们叫做全局序列号。
http://www.mycat.org.cn/
下载压缩包:
http://dl.mycat.org.cn/1.6.7.4/Mycat-server-1.6.7.4-release/Mycat-server-1.6.7.4-release-20200105164103-linux.tar.gz
上传linux 系统,解压:
进入/bin 目录,里面可以启动mycat
./mycat start # 后台启动 ./mycat console # 控制台启动
进入/conf 目录,里面可以修改配置文件
文件末端,可以修改逻辑库名、连接的用户名密码
按照上图配置,启动:mycat后,用navicat连接:
可以看到
<?xml version="1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat="http://io.mycat/"> <schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100"> <!-- auto sharding by id (long) --> <!--splitTableNames 启用<table name 属性使用逗号分割配置多个表,即多个表使用这个配置--> <table name="user" dataNode="dn1,dn2" rule="auto-sharding-long" /> <table name="order" dataNode="dn1,dn2" rule="auto-sharding-long"> <childTable name="order_item" joinKey="order_id" parentKey="id" /> </table> </schema> <dataNode name="dn1" dataHost="host139" database="my_test" /> <dataNode name="dn2" dataHost="host141" database="my_test" /> <dataHost name="host139" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1"> <heartbeat>select user()</heartbeat> <!-- can have multi write hosts --> <writeHost host="M1" url="192.168.10.139:3306" user="jsh" password="Admin@123"> <readHost host="S1" url="192.168.10.140:3306" user="jsh" password="Admin@123" /> </writeHost> </dataHost> <dataHost name="host141" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1"> <heartbeat>select user()</heartbeat> <writeHost host="M2" url="192.168.10.141:3306" user="jsh" password="Admin@123"> <readHost host="S1" url="192.168.10.142:3306" user="jsh" password="Admin@123" /> </writeHost> </dataHost> </mycat:schema>
于定义 MyCat 实例中的逻辑库,MyCat 可以有多个逻辑库,每个逻辑库都有自己的相关配置。可以使用 schema 标签来划分这些不同的逻辑库。如果不配置 schema 标签,所有的表配置,会属于同一个默认的逻辑库。
属性名 | 说明 |
---|---|
name | 逻辑库名 |
checkSQLschema | 当该值设置为 true 时,如果我们执行语句select * from TESTDB.travelrecord;则 MyCat 会把语句修改为select * from travelrecord;。即把表示 schema 的字符去掉,避免发送到后端数据库执行时报(ERROR 1146 (42S02): Table ‘testdb.travelrecord’ doesn’t exist)。 |
sqlMaxLimit | 当该值设置为某个数值时。每条执行的 SQL 语句,如果没有加上 limit 语句,MyCat 也会自动的加上所对应的值。例如设置值为 100,执行select * from TESTDB.travelrecord;的效果为和执行select * from TESTDB.travelrecord limit 100;相同。 |
定义了 MyCat 中的数据节点,也就是我们通常说所的数据分片。一个 dataNode 标签就是一个独立的数据分片。
属性名 | 说明 |
---|---|
name | 定义数据节点的名字,这个名字需要是唯一的,我们需要在 table 标签上应用这个名字,来建立表与分片对应的关系。 |
dataHost | 该属性用于定义该分片属于哪个数据库实例的,属性值是引用 dataHost 标签上定义的 name 属性。 |
database | 该属性用于定义该分片属性哪个具体数据库实例上的具体库,因为这里使用两个纬度来定义分片,就是:实例+具体的库。因为每个库上建立的表和表结构是一样的。所以这样做就可以轻松的对表进行水平拆分。 |
定义了 MyCat 中的逻辑表,所有需要拆分的表都需要在这个标签中定义。
属性名 | 说明 |
---|---|
name | 定义逻辑表的表名,这个名字就如同我在数据库中执行 create table 命令指定的名字一样,同个 schema 标签中定义的名字必须唯一。 |
dataNode | 定义这个逻辑表所属的 dataNode, 该属性的值需要和 dataNode 标签中 name 属性的值相互对应。 |
rule | 该属性用于指定表是否绑定分片规则。 |
type | 该属性定义了逻辑表的类型,目前逻辑表只有“全局表”和”普通表”两种类型。对应的配置:全局表:global。普通表:不指定该值为 globla 的所有表 |
定义 E-R 分片的子表。通过标签上的属性与父表进行关联。
属性名 | 说明 |
---|---|
name | 定义子表的表名。 |
joinKey | 标志子表中的列,用于与父表做关联 |
parentKey | 标志父表中的列,与joinKey对应 |
定义了具体的数据库实例、读写分离配置和心跳语句。
属性名 | 说明 |
---|---|
name | 唯一标识 dataHost 标签,供上层的标签使用。 |
maxCon | 指定每个读写实例连接池的最大连接。也就是说,标签内嵌套的 writeHost、readHost 标签都会使用这个属性的值来实例化出连接池的最大连接数。 |
minCon | 指定每个读写实例连接池的最小连接,初始化连接池的大小。 |
balance | 读负载均衡类型:0、1、2、3, 见下方详解 |
writeType | 写负载均衡类型:0、1, 见下方详解 |
dbType | 指定后端连接的数据库类型,目前支持二进制的 mysql 协议,还有其他使用 JDBC 连接的数据库。 |
dbDriver | 指定连接后端数据库使用的 Driver,目前可选的值有 native 和 JDBC。使用 native 的话,因为这个值执行的是二进制的 mysql 协议,所以可以使用 mysql 和 maridb。其他类型的数据库则需要使用 JDBC 驱动来支持。 |
switchType | -1 表示不自动切换;1 默认值,自动切换;2 基于 MySQL 主从同步的状态决定是否切换;3 基于 MySQL galary cluster 的切换机制(适合集群) |
balance
writeType
这个标签内指明用于和后端数据库进行心跳检查的语句。
这两个标签都指定后端数据库的相关配置给 mycat,用于实例化后端连接池。唯一不同的是,writeHost 指定写实例、readHost 指定读实例,组着这些读写实例来满足系统的要求。在一个 dataHost 内可以定义多个 writeHost 和 readHost。但是,如果 writeHost 指定的后端数据库宕机,那么这个 writeHost 绑定的所有 readHost 都将不可用。另一方面,由于这个 writeHost 宕机系统会自动的检测到,并切换到备用的 writeHost 上去。
table 标签的 rule 属性,可以配置数据入库的分片规则。rule属性值必须与配置文件rule.xml中的<tableRule>
标签一致。下面简单介绍下两种常用的分配规则。
通过在配置文件中配置可能的枚举 id,自己配置分片,本规则适用于特定的场景,比如有些业务需要按照省
份或区县来做保存,而全国省份区县固定的,这类业务使用本条规则,rule.xml中配置如下:
<tableRule name="sharding-by-intfile"> <rule> <columns>user_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> <property name="type">0</property> <property name="defaultNode">0</property> </function>
上面 columns 表示将要分片的表字段,algorithm 分片函数,
其中分片函数配置中,mapFile 表示配置文件名称,type 默认值为 0,0 表示 Integer,非零表示 String,
所有的节点配置都是从 0 开始,及 0 代表节点 1。
defaultNode 默认节点:小于 0 表示不设置默认节点,大于等于 0 表示设置默认节点。
默认节点的作用:枚举分片时,如果碰到不识别的枚举值,就让它路由到默认节点。
如果不配置默认节点(defaultNode 值小于 0 表示不配置默认节点),碰到不识别的枚举值就会报错。
partition-hash-int.txt 配置:
10000=0 10010=1 DEFAULT_NODE=1
配置说明:
枚举值10000的数据会进入分片节点0,枚举值10010的数据会进入分片节点1,其他值的数据默认进入节点1。
此规则为对分片字段求摸运算。
<tableRule name="mod-long"> <rule> <columns>user_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>
配置说明:
上面 columns 将要分片的表字段,algorithm 分片函数,
此种配置非常明确即根据 id 进行十进制求模运算,相比固定分片 hash,此种在批量插入时可能存在批量插入单
事务插入多数据分片,增大事务一致性难度。
mycat 有两个端口:
show @@help reload @@config reload @@config_all
整体架构:
TCP/HTTP代理和负载平衡器的高可用性环境。
137和138两个节点相同的安装配置。
搜索Haproxy
yum search haproxy
安装Haproxy
yum -y install haproxy.x86_64
vim /etc/haproxy/haproxy.cfg
修改:
global log 127.0.0.1 local2 chroot /var/lib/haproxy pidfile /var/run/haproxy.pid maxconn 4000 user haproxy group haproxy daemon stats socket /var/lib/haproxy/stats defaults mode tcp log global option tcplog option dontlognull option redispatch retries 3 timeout http-request 10s timeout queue 1m timeout connect 10s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 10s maxconn 3000 frontend main *:5000 default_backend app backend app balance roundrobin server app1 192.168.10.137:8066 check server app2 192.168.10.138:8066 check
haproxy -f /etc/haproxy/haproxy.cfg
搜索Haproxy
yum search keepalived
安装Haproxy
yum -y install keepalived.x86_64
vim /etc/keepalived/keepalived.conf
主137配置:
global_defs { router_id keep_137 } vrrp_script check_haproxy { script "killall -0 haproxy" interval 2 } vrrp_instance VI_1 { state MASTER interface ens33 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.10.101 } track_script { check_haproxy } } virtual_server 192.168.10.101 6000 { delay_loop 6 lb_algo rr lb_kind NET persistence_timeout 50 protocol TCP real_server 192.168.10.137 5000 { weight 1 TCP_CHECK { connect_port 5000 connect_timeout 2 } } }
从138配置
global_defs { router_id keep_138 } vrrp_script check_haproxy { script "killall -0 haproxy" interval 2 } vrrp_instance VI_1 { state BACKUP interface ens33 virtual_router_id 51 priority 90 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.10.101 } track_script { check_haproxy } } virtual_server 192.168.10.101 6000 { delay_loop 6 lb_algo rr lb_kind NET persistence_timeout 50 protocol TCP real_server 192.168.10.138 5000 { weight 1 TCP_CHECK { connect_port 5000 connect_timeout 2 } } }
这里配置了虚拟IP和端口:192.168.10.101:6000
还配置了haproxy的监测脚本 check_haproxy。
keepalived -f /etc/keepalived/keepalived.conf
连接成功!