Mycat数据库中间件上手实践及分布式事务和读写分离实现

前言

现在微服务真是火得不行不行的, 最近开始找工作, 打开Boss直聘一看, 乖乖, 整页整页全是要求会整微服务的, 其中不乏很多小微企业, 搞得好像微服务就是万能的似的, 真不知道是中了谁的毒. 微服务肯定是个好东西, 优点很多, 百度一下一大把, 这里我们就不说了, 先讨论讨论微服务必然会带来几个问题.

  • 通信问题, 微服务间是肯定需要通信的, 不能像以前一样调个方法搞定了.
  • 分布式事务问题暨数据一致性问题, 微服务之间DB操作无法放在一个数据库事务中了.
  • 维护问题, 微服务的维护难度必然是远超过单服务, 如果涉及垂直和水平分库分片更甚.

首先通信问题, 目前主流的做法是RPC和REST, 无论哪种都必然需要目前主流的做法是RPC和REST, 无论哪种都必然需要增加一个接口层的编码, 这带来的工作量增加是无解的.
维护问题亦然, 只要拆分了, 特别是数据库, 那么不仅仅是维护难度成倍增加, 连开发难度都会成倍增加.
这两点是无法逃避的, 但时间能解决, 唯独分布式事务这块是硬伤, 不能解决或解决不好都是会影响到数据健壮性的致命问题, 而本文要讨论的就是如何使用Mycat省时省力且优雅的解决它.

开始正题

第一步 准备工作

老规矩先上传送门

官网传送门
GitHub传送门

我估计好多小伙伴都会条件反射点GitHub传送门, 然后进release里去下载, 嗯, 我也这样.
然后! 然后! 我就被这坑进去了4个多小时!
release里最后的版本是1.6.5! 而这个版本join居然有BUG! 关键是最新release版本已经到1.6.7.1了! 关于BUG后面再讲.
下载地址传送门

下载好了之后解压到你需要它在的位置就可以了, 这货不需要安装, 挺绿色.
配置好后可直接./mycat start启动.

哦对了, Win下直接启动会告诉你需要先注册服务, 按照提示执行注册服务命令就好了.

开始启动会在自身目录生成log文件夹, log里包含两个日志文件warapper.log mycat.log, 第一个是启动日志, 遇到启动失败记得看这个日志, 第二个是执行日志.

又哦对了, 启动时可能会因为权限问题无法创建log文件夹导致报错启动失败, 遇到了可以自行创建解决或者给权限解决.

继续往下阅读, 会涉及到一些主从同步|读写分离的知识, 如果不是很了解的话, 请看刚刚专门为这边Blog写完的这货

第二步 关键配置及理解

server.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 1为开启全局表一致性检测、0为关闭 -->
<property name="useGlobleTableCheck">0</property>

<!-- 0 为本地文件方式,1 为数据库方式,2 为时间戳序列方式,3 为分布式ZK ID 生成器,4 为 zk 递增 id 生成。 -->
<property name="sequnceHandlerType">1</property>
<!-- 数据服务占用端口 -->
<property name="serverPort">8066</property>
<!-- 管理占用端口, 类似show @@cache;等管理命令必须使用此端口登陆才能正常使用. -->
<property name="managerPort">9066</property>
<!-- 连接的空闲超时断开时间 -->
<property name="idleTimeout">300000</property>
<!-- 内网使用默认即可, 应该没人拿来外网访问 -->
<property name="bindIp">0.0.0.0</property>
<!-- 查询返回结果集长度报警阈值, 超过日志输出警告 -->
<property name="frontWriteQueueSize">4096</property>
<!-- 系统可用的线程数,默认值为机器 CPU 核心线程数, 可适当调高 -->
<property name="processors">32</property>
<!-- 分布式事务开关 -->
<property name="handleDistributedTransactions">0</property>

useGlobleTableCheck 含义
全局表用于存放字典类低频增删改及数据量不大的表, 供给join使用.
开启后需要手动在配置成全局表的表结构里增加_mycat_op_time字段, 如果是通过mycat发送的建表语句, 则会自动改写增加该字段.
原理是拦截CUD语句, 自动改写给_mycat_op_time字段加入当前时间戳的值.
然后通过定时器定时执行

1
2
select count(*) as record_count from user;
select max(_mycat_op_time) as max_timestamp from user;

两个查询, 然后对比所有节点上全局表返回结果是否一致, 从而完成一致性检测.
当然, 这个配置只能检测, 不能自动修复, 只是帮助我们及时发现问题.

sequnceHandlerType 含义
全局ID生成方式, 用于数据库自增主键.
使用数据库自带的主键自增功能, 在跨库跨节点分片中可能出现insert后返回不准确的问题.
所以需要使用自增主键的话, 必须使用Mycat提供的自增策略.
0是禁止使用的, 因为每次重启服务后ID会重置.
1是使用数据库表实现, 将起始ID和步长存放在数据库中, 每次读取步长量的ID并更新ID起始值, 用完再来.
2本地时间戳方式, 64位ID, 太长了, 我反正不会用…
3分布式 ZK ID 生成器, 依然是64位ID, 好处是高吞吐量并且保证机房内极限状态获取17年不重复.
4Zk 递增方式, 没用过不知道.

这里只讲一下2的配置, 大部分人的选择.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
CREATE TABLE MYCAT_SEQUENCE (name VARCHAR(50) NOT NULL,current_value INT NOT
NULL,increment INT NOT NULL DEFAULT 100, PRIMARY KEY(name)) ENGINE=InnoDB;

--GLOBAL表示该条数据为所有没有单独配置ID数据的表提供自增主键, 必须全大写.
INSERT INTO MYCAT_SEQUENCE(name,current_value,increment) VALUES (‘GLOBAL’, 100000, 100);
--ORDER表示该数据只为ORDER表提供自增主键, 必须全大写.
INSERT INTO MYCAT_SEQUENCE(name,current_value,increment) VALUES (‘ORDER’, 100000, 100);

--10000为起始ID, 100位步长, 即每次取出数量, insert量大可提高步长降低该表使用量.

--增加三个存储过程, 和表必须放在同一个库中.
DROP FUNCTION IF EXISTS mycat_seq_currval;
DELIMITER
CREATE FUNCTION mycat_seq_currval(seq_name VARCHAR(50)) RETURNS varchar(64) CHARSET utf-8
DETERMINISTIC
BEGIN
DECLARE retval VARCHAR(64);
SET retval=“-999999999,null”;
SELECT concat(CAST(current_value AS CHAR),“,”,CAST(increment AS CHAR)) INTO retval FROM
MYCAT_SEQUENCE WHERE name = seq_name;
RETURN retval;
END
DELIMITER;

DROP FUNCTION IF EXISTS mycat_seq_setval;
DELIMITER
CREATE FUNCTION mycat_seq_setval(seq_name VARCHAR(50),value INTEGER) RETURNS varchar(64)
CHARSET utf-8
DETERMINISTIC
BEGIN
UPDATE MYCAT_SEQUENCE
SET current_value = value
WHERE name = seq_name;
RETURN mycat_seq_currval(seq_name);
END
DELIMITER;

DROP FUNCTION IF EXISTS mycat_seq_nextval;
DELIMITER
CREATE FUNCTION mycat_seq_nextval(seq_name VARCHAR(50)) RETURNS varchar(64)CHARSET utf-8
DETERMINISTIC
BEGIN
UPDATE MYCAT_SEQUENCE
SET current_value = current_value + increment WHERE name = seq_name;
RETURN mycat_seq_currval(seq_name);
END
DELIMITER;

全部删掉sequence_db_conf.properties里的内容, 然后添加

1
2
GLOBAL=test_dn
ORDER=test_dn

这里就是刚刚insert的两条数据里对应的表绑定datahost, datahost是啥后面会讲到.

handleDistributedTransactions
0 为不过滤分布式事务 1 为过滤分布式事务(如果分布式事务内只涉及全局表,则不过滤)2 为不过滤分布式事务,但是记录分布式事务日志
官方说明有点难理解, 害我测试了一下才明白. 过滤等于是禁用的意思.
0就是开启分布式事务, 1是禁止分布式事务, 如果事务内操作的都是全局表则不禁用. 2是开启分布式事务同时开启日志.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<user name="root">
<property name="password">jiangwei</property>
<property name="schemas">userOrder</property>
<property name="readOnly">false</property>

<!-- 表级 DML 权限设置 -->
<!--
<privileges check="false">
<schema name="TESTDB" dml="0110" >
<table name="tb01" dml="0000"></table>
<table name="tb02" dml="1111"></table>
</schema>
</privileges>
-->
</user>

这个用于配置登陆使用的用户, schemas则是填入开放访问的多个分片合并出来的虚拟表的虚拟库, 允许填入多个, 逗号隔开. 里面填入的内容在后面马上会讲到的schema.xml文件里配置.

server.xml的关键配置及含义到这就讲完了.

schema.xml

首先讲一下我模拟实现的需求, 有sys_usersys_rolesys_order三个表.
根据功能进行垂直分库后, 分成了两个库.
sys_usersys_role所在的user库和sys_order所在的order库.
user库放在192.168.1.56(后简称56)这台服务器上, order库放在localhost本地.
然后考虑sys_order表数据量增大, 进行了水平分片, 于是又产生了一个order2库.

最后为了进一步增加可用性, 又增加了两台服务器分别作为56和localhost的从库, 进行读写分离, 主库只负担写入, 从库只负责读去.

以上就是下面这段配置实现的效果.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<schema name="userOrder" checkSQLschema="false" sqlMaxLimit="1000">

<table name="sys_user" dataNode="user" primaryKey="id" autoIncrement="true" >
<childTable name="orders" joinKey="user_id" parentKey="id"/>
</table>

<table name="sys_role" dataNode="user" primaryKey="id" autoIncrement="true" />

<table name="sys_order" dataNode="order_dn1,order_dn2" autoIncrement="true" rule="mod-long2" primaryKey="id" />

</schema>

<dataNode name="user" dataHost="userHost" database="user" />
<dataNode name="order_dn1" dataHost="orderHost" database="order" />
<dataNode name="order_dn2" dataHost="orderHost" database="order2" />


<dataHost name="userHost" maxCon="1000" minCon="10" balance="3"
writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
<heartbeat>show slave status</heartbeat>

<writeHost host="hostM1" url="192.168.1.56:3306" user="root" password="Your PWD" >
<readHost host="hostS2" url="192.168.1.56:3307" user="root" password="Your PWD" />
</writeHost>

</dataHost>
<!-- rule2 -->
<dataHost name="orderHost" maxCon="1000" minCon="10" balance="3"
writeType="0" dbType="mysql" dbDriver="native" switchType="2" slaveThreshold="100">
<heartbeat>show slave status</heartbeat>

<writeHost host="hostM1" url="localhost:3306" user="root" password="Your PWD" >
<readHost host="hostS2" url="192.168.1.56:3308" user="root" password="Your PWD" />
</writeHost>

</dataHost>

<dataHost /> 表示节点主机, 一个节点主机不代表一台主机, 可以配置多台主机, 即多个<writeHost />.
balance属性表示负载均衡类型, 0为不开启. 1为除了 readHost, 空闲的writeHost也参与Select的负载均衡. 2为Select在所有readHostwriteHost上随机分发. 3为Select操作只在writeHost内的readHost上随机分发.
可见1是用于双主同步模式, 而3才是用于完全读写分离模式, 所以只有3能满足刚刚模拟的需求.

writeType已废弃, 用switchType替代, 默认值即可.

switchType属性表示是否开启自动切换, 需要读写分离的话必须开启, 默认1不开启.
2为开启, 开启后heartbeat心跳检测语句必须改为show slave status, 这里其实就帮你做了可用性验证.
当同步线程出问题挂掉后, 还继续在从库查询必然会出BUG, 所以根据心跳检测返回可决定是否切换从库进行查询.

heartbeat属性上面已经提到过了, 就是字面含义, 用来检测被代理的物理库的活性. 所使用的语句不考虑读写分离的节点, 可以使用select 1这种来替换.

<writeHost />标签用于配置物理库连接数据, 很好理解.
<readHost />子标签用于配置读写分离后负责读的物理库连接数据.

<dataNode /> 表示分片节点, 用于配置和主机节点的绑定关系.
根据之前模拟的需求, 如果sys_order库不进行水平分片的话, 那么<dataNode />节点就只有两个, 一个user、一个order.
sys_order分片之后多出一个物理库order2, 所以有了第三个<dataNode />.

<schema /> 在讲server.xml中提到了一次, 这里就是使用分片组合成虚拟表的虚拟库.
checkSQLschema="false"属性是用来兼容DB工具访问的属性, 设为true才兼容类似Navicat的访问, 我开启后使用Navicat访问依然出现各种问题, 一度让我以为我哪里配置出问题, 浪费了一两个小时, 所以不用理它.

<table/ > 标签就是配置由分片节点组成虚拟表, 如果组成虚拟表的分片节点只有一个, 那么sql的执行就只是进行了一次转发, 如果配置了多个, 那么sql就需要经过Mycat的改写, 比如说根据Id查Order, 因为sys_order表进行了水平分片, 所以没人知道对应这个Id的数据在哪个分片上, 所以需要改写到两个分片上去查询.
不过Mycat加入了热点缓存机制TableID2DataNodeCache, 缓存主键命中的路由, 下次再查询该主键就会直接命中该路由.

更新下TableID2DataNodeCache的解释, 根据最近的测试发现, 它只能根据主键进行单表的路由缓存, 即使我们使用了<childTable />标签来开启ER Join功能,
TableID2DataNodeCache也无法在我们使用ER Join时提供路由缓存服务, 按道理说, 既然单表可以根据主键缓存路由, 那么ER Join 同样可以啊, 既然是ER Join说明是单库的Join而非跨节点的, 那么使用条件中的主键缓存路由是没有问题的.

<table/ > 另外还有一个属性type="global", 可以将一张表设置为全局表, 设置以后macat会保证该表在所有节点上的一致性, 使其在任何节点上需要被join是可以直接在库内提供数据支持, 而不需要跨库跨节点, 从而提升查询效率, 全局表的更新每次都会把数据同步到所有节点且不支持水平分片, 所以一般建议用于数据字典表, 类似省市区表、业务类型表等更新频率低数据量不大的表.

dataNode属性就是用来绑定分片的了, 可绑定多个, 已多次提到.

primaryKey 告知Mycat分片对应物理表的主键, 方便进行路由计算.

autoIncrement 开启主键自增, 主键自增需要的配置上面也讲过了.

<childTable /> 这个子表标签, 我花了些功夫才理解是什么意思.
官方说是用来实现ER Join, 通俗理解就是用来实现分片后的表之间进行的join并保证效率.
试想看看, 当表水平分片后, 数据都不知道在哪个分片里, 然后还要跟另外一个库里的表去join查询, 这特么只能把全表查出来才能通过代码完成join操作了吧, 但是这样还谈什么高可用.
所以官方用了一个比较鸡贼的方式, 还是用之前模拟的需求来举例.
首先根据用户Id对sys_usersys_order分片, 通过配置子表的方式告知从属关系及外键关系, 然后在insert新order时, 会根据用户Id查询找到该用户所在分片, 然后把这个order也insert到同一个分片上去, 从而以后查询用户及用户下所有订单时的join可以直接在同一个库中用最普通的join完成. 这样及实现了join的需求, 也不会有性能问题.

但是又带来了两个个问题, 第一必须两张表都水平分片才能使用该功能. 第二不能使用垂直分片, 即sys_usersys_order的分片必须在同一个库中.

rule属性就是用来配置分片规则的了, 取值对应马上开始讲的最后一个配置文件.

啊啊啊, 终于到了最后一个配置文件.

rule.xml

1
2
3
4
5
6
<tableRule name="rule2">
<rule>
<columns>user_id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>

里面<tableRule/ >标签的name属性就是填入schema.xml<table/ >标签里的rule属性的值.

可以看到该配置文件中<tableRule/ >有很多个, 子标签<columns />里放的是用于分片的字段, <algorithm />则是调用的分片方法. 取值对应文件下方的<function />标签.

sharding-by-intfile 枚举分片, 通过配置partition-hash-int.txt完成按固定条件分片, 例如按国家、省份或地区分片.

"mod-long 求模分片, 按分片字段的值十进制求模分片.

rule1 固定hash分片. 求模分片的高阶版, 用分片字段的二进制低 10 位进行计算, 直接求模如果有10个分片, 当连续insert 1到10时就会依此分片到10个分片里, 使用此规则可避免此情况, 降低跨库跨节点事务的性能消耗.
但是如果你的分片就只有两三个, 就还不如直接求模了, 降低运算量.

auto-sharding-long 按分片字段范围分片, 在autopartition-long.txt配置范围规则, 例如: 0-1000000=0, 表示分片字段值在这个范围内分片到第一个节点.

sharding-by-pattern 求模+范围组合模式, 求模同时控制数据落点. 范围是这是求模之后余数的范围.

sharding-by-prefixpattern 截取指定位数字母转ASCII码再求模+范围分片方式.

sharding-by-substring 使用者自行指定分片, 根据分片字段的值配置截取位置获得的结果来分片, 结果必须是数字.

sharding-by-date 按天分片, 变态增量数据专用之一, 可使用sPartionDay属性设置多少天分一片.

暂时实践到的分片规则就这么多, 常用的也差不多就这些了, 还支持很多其他分片规则, 具体可在官方用户手册中查看.

总结

Mycat功能细化做的很不错, 颗粒度扣得很细, 基本上能想到的业务场景都能够配置出来, 不过这也带来一个问题, 就是需要掌握的配置量就比较庞大了, 不说能记住, 起码都必须过一遍脑子, 知道有这么些个玩意儿和原理, 这样在遇到需求时才能联想到mycat是否能够配得出来, 遇到问题时才能快速分析找到问题点, 或者说最大限度上预防出问题.
苦逼归苦逼, 但是也只有熟练掌握了数据库中间件的原理思维, 并且能熟练运用这些数据库中间件, 你才有资格说你已经能够驾驭高可用微服务了.

辞职了, 最近又要开始找工作了, 最后祝自己顺利吧!Thanks for your reading。

文章目录
  1. 1. 前言
  2. 2. 开始正题
    1. 2.1. 第一步 准备工作
    2. 2.2. 第二步 关键配置及理解
  3. 3. 总结
,